You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2280 lines
66 KiB

3 months ago
1 month ago
3 months ago
3 months ago
1 month ago
3 months ago
3 months ago
3 months ago
3 months ago
2 months ago
3 months ago
3 months ago
1 month ago
2 months ago
1 month ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
1 month ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
1 month ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
1 month ago
3 months ago
3 months ago
3 months ago
1 month ago
3 months ago
3 months ago
3 months ago
2 months ago
3 months ago
1 month ago
3 months ago
1 month ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
1 month ago
3 months ago
2 months ago
1 month ago
1 month ago
1 month ago
3 months ago
2 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
1 month ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
1 month ago
3 months ago
3 months ago
2 months ago
3 months ago
3 months ago
2 months ago
3 months ago
1 month ago
3 months ago
1 month ago
3 months ago
1 month ago
3 months ago
1 month ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
2 months ago
1 month ago
2 months ago
1 month ago
3 months ago
3 months ago
1 month ago
3 months ago
1 month ago
3 months ago
1 month ago
3 months ago
1 month ago
3 months ago
1 month ago
3 months ago
1 month ago
3 months ago
1 month ago
3 months ago
1 month ago
3 months ago
1 month ago
1 month ago
3 months ago
1 month ago
1 month ago
3 months ago
1 month ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
1 month ago
3 months ago
3 months ago
3 months ago
1 month ago
3 months ago
1 month ago
3 months ago
1 month ago
3 months ago
1 month ago
3 months ago
1 month ago
1 month ago
1 month ago
3 months ago
1 month ago
3 months ago
1 month ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
3 months ago
3 months ago
2 months ago
1 month ago
2 months ago
3 months ago
3 months ago
3 months ago
3 months ago
  1. <template>
  2. <div class="mod-config exp-apply-page" v-loading="sendLoading"
  3. element-loading-text="正在发送邮件,请稍候..."
  4. element-loading-spinner="el-icon-loading"
  5. element-loading-background="rgba(0, 0, 0, 0.6)">
  6. <!-- 查询条件表单 -->
  7. <el-form :inline="true" label-position="top" class="query-form">
  8. <el-form-item label="试验单号">
  9. <el-input v-model="queryHeaderData.applyNo" placeholder="支持模糊查询" clearable style="width: 150px"></el-input>
  10. </el-form-item>
  11. <el-form-item label="试验负责人">
  12. <el-input v-model="queryHeaderData.projectLeader" placeholder="支持模糊查询" clearable style="width: 120px"></el-input>
  13. </el-form-item>
  14. <el-form-item label="事业部">
  15. <el-select v-model="queryHeaderData.buNo" placeholder="请选择" clearable style="width: 120px">
  16. <el-option label="全部" value=""></el-option>
  17. <el-option
  18. v-for="i in buList"
  19. :key="i.buNo"
  20. :label="i.buDesc"
  21. :value="i.buNo">
  22. </el-option>
  23. </el-select>
  24. </el-form-item>
  25. <el-form-item label="试验类型">
  26. <el-select v-model="queryHeaderData.experimentType" placeholder="请选择" clearable style="width: 120px">
  27. <el-option label="全部" value=""></el-option>
  28. <el-option label="High Risk" value="High Risk"></el-option>
  29. <el-option label="Low Risk" value="Low Risk"></el-option>
  30. </el-select>
  31. </el-form-item>
  32. <el-form-item label="试验名称">
  33. <el-input v-model="queryHeaderData.title" placeholder="支持模糊查询" clearable style="width: 150px"></el-input>
  34. </el-form-item>
  35. <el-form-item label="项目编号">
  36. <el-input v-model="queryHeaderData.projectNo" placeholder="支持模糊查询" clearable style="width: 150px"></el-input>
  37. </el-form-item>
  38. <!--
  39. <el-form-item label="产品型号">
  40. <el-input v-model="queryHeaderData.productType" placeholder="支持模糊查询" clearable style="width: 150px"></el-input>
  41. </el-form-item>-->
  42. <el-form-item label="状态">
  43. <el-select v-model="queryHeaderData.status" placeholder="请选择" clearable style="width: 120px">
  44. <el-option label="全部" value=""></el-option>
  45. <el-option label="草稿" value="草稿"></el-option>
  46. <el-option label="已下达" value="已下达"></el-option>
  47. <el-option label="已批准" value="已批准"></el-option>
  48. <el-option label="生产中" value="生产中"></el-option>
  49. <el-option label="样品确认" value="样品确认"></el-option>
  50. <el-option label="已完成" value="已完成"></el-option>
  51. <el-option label="已取消" value="已取消"></el-option>
  52. </el-select>
  53. </el-form-item>
  54. <el-form-item label="创建日期">
  55. <el-date-picker
  56. v-model="queryHeaderData.createStartDate"
  57. type="date"
  58. format="yyyy-MM-dd"
  59. value-format="yyyy-MM-dd"
  60. placeholder="开始日期"
  61. style="width: 140px">
  62. </el-date-picker>
  63. </el-form-item>
  64. <el-form-item label="至">
  65. <el-date-picker
  66. v-model="queryHeaderData.createEndDate"
  67. type="date"
  68. format="yyyy-MM-dd"
  69. value-format="yyyy-MM-dd"
  70. placeholder="结束日期"
  71. style="width: 140px">
  72. </el-date-picker>
  73. </el-form-item>
  74. <el-form-item label=" " style="margin-top: -11px">
  75. <el-button @click="getDataList('Y')" type="primary" plain class="search-btn">查询</el-button>
  76. <el-button @click="resetQuery()" plain class="reset-btn">重置</el-button>
  77. </el-form-item>
  78. <el-form-item label=" " style="margin-top: -11px">
  79. <el-button @click="openCreateDialog()" type="success" v-if="isAuth('erf:apply:add')" plain class="add-btn">新增试验单</el-button>
  80. <el-button
  81. @click="deleteApplyFromToolbar()"
  82. :disabled="!currentRow.applyNo || currentRow.status !== '草稿'"
  83. type="danger"
  84. plain
  85. class="delete-btn">删除</el-button>
  86. <el-button
  87. @click="withdrawApplyFromToolbar()"
  88. :disabled="!currentRow.applyNo || currentRow.status === '草稿' || currentRow.status === '已完成' || currentRow.status === '已驳回' || currentRow.status === '已取消'"
  89. type="warning"
  90. plain
  91. class="withdraw-btn">撤回</el-button>
  92. <el-button
  93. @click="cancelApplyFromToolbar()"
  94. :disabled="!currentRow.applyNo || currentRow.status === '草稿' || currentRow.status === '已完成' || currentRow.status === '已取消'"
  95. type="info"
  96. plain
  97. class="cancel-btn">取消</el-button>
  98. <el-button
  99. @click="openCopyDialog()"
  100. :disabled="!currentRow.applyNo"
  101. type="primary"
  102. plain
  103. class="copy-btn">复制</el-button>
  104. <el-button
  105. @click="urgeApproval()"
  106. :disabled="!currentRow.applyNo || ['草稿', '已完成', '已取消', '已驳回'].includes(currentRow.status)"
  107. :loading="urgeLoading"
  108. type="warning"
  109. plain
  110. class="urge-btn">催办</el-button>
  111. </el-form-item>
  112. </el-form>
  113. <!-- 数据表格 -->
  114. <el-table
  115. ref="dataTable"
  116. @row-click="handleRowClick"
  117. highlight-current-row
  118. :data="dataList"
  119. v-loading="dataListLoading"
  120. border
  121. class="data-table"
  122. style="width: 100%;"
  123. :height="tableHeight">
  124. <el-table-column
  125. label="操作"
  126. width="150"
  127. align="center"
  128. header-align="center">
  129. <template slot-scope="scope">
  130. <a v-if="scope.row.status === '草稿' || scope.row.status === '已驳回'"
  131. @click="editApply(scope.row)">修改</a>
  132. <a
  133. v-if="scope.row.status === '草稿' || scope.row.status === '已驳回'"
  134. @click="submitApply(scope.row)">{{ scope.row.status === '已驳回' ? '重新下达' : '下达' }}</a>
  135. <a
  136. v-if="showSampleConfirmBtn(scope.row)"
  137. @click="openSampleConfirm(scope.row)">样品确认</a>
  138. </template>
  139. </el-table-column>
  140. <el-table-column
  141. prop="buDesc"
  142. label="事业部"
  143. width="70"
  144. align="center"
  145. header-align="center">
  146. </el-table-column>
  147. <el-table-column
  148. prop="applyNo"
  149. label="试验单号"
  150. width="130"
  151. align="center"
  152. header-align="center"
  153. show-overflow-tooltip>
  154. </el-table-column>
  155. <el-table-column
  156. prop="status"
  157. label="状态"
  158. width="80"
  159. align="center"
  160. header-align="center">
  161. </el-table-column>
  162. <!-- <el-table-column
  163. prop="currentStep"
  164. label="当前步骤"
  165. width="90"
  166. align="center"
  167. header-align="center">
  168. </el-table-column>-->
  169. <el-table-column
  170. prop="experimentType"
  171. label="试验类型"
  172. width="100"
  173. align="center" show-overflow-tooltip
  174. header-align="center">
  175. </el-table-column>
  176. <el-table-column
  177. prop="title"
  178. label="试验名称"
  179. min-width="120"
  180. align="left"
  181. header-align="center"
  182. show-overflow-tooltip>
  183. </el-table-column>
  184. <el-table-column
  185. prop="projectNo"
  186. label="项目编号"
  187. width="100"
  188. align="center"
  189. header-align="center"
  190. show-overflow-tooltip>
  191. </el-table-column>
  192. <el-table-column
  193. prop="umName"
  194. label="计量单位"
  195. min-width="80"
  196. align="left"
  197. header-align="center"
  198. show-overflow-tooltip>
  199. </el-table-column>
  200. <!-- <el-table-column
  201. prop="productType"
  202. label="产品型号"
  203. min-width="220"
  204. align="center"
  205. header-align="center"
  206. show-overflow-tooltip>
  207. </el-table-column>-->
  208. <el-table-column
  209. prop="expectedFinishDate"
  210. label="期望完成日期"
  211. width="120"
  212. align="center"
  213. header-align="center">
  214. </el-table-column>
  215. <el-table-column
  216. prop="actualFinishDate"
  217. label="最终完成日期"
  218. width="120"
  219. align="center"
  220. header-align="center">
  221. </el-table-column>
  222. <el-table-column
  223. prop="finalQuantity"
  224. label="入库数量"
  225. width="80"
  226. align="center"
  227. header-align="center">
  228. </el-table-column>
  229. <el-table-column
  230. prop="projectLeader"
  231. label="试验负责人"
  232. width="100"
  233. align="center"
  234. header-align="center">
  235. </el-table-column>
  236. <el-table-column
  237. prop="createTime"
  238. label="创建时间"
  239. min-width="100"
  240. align="center" show-overflow-tooltip
  241. header-align="center">
  242. </el-table-column>
  243. </el-table>
  244. <!-- 分页组件 -->
  245. <el-pagination
  246. @size-change="sizeChangeHandle"
  247. @current-change="currentChangeHandle"
  248. :current-page="pageIndex"
  249. :page-sizes="[20, 50, 100]"
  250. :page-size="pageSize"
  251. :total="totalPage"
  252. layout="total, sizes, prev, pager, next, jumper"
  253. style="margin-top: 10px; text-align: right;">
  254. </el-pagination>
  255. <!-- 下方Tab区域 -->
  256. <el-tabs class="customer-tab"
  257. v-model="activeName"
  258. type="border-card"
  259. style="margin-top: 10px; min-height: 280px;"
  260. @tab-click="handleTabClick">
  261. <!-- Tab 1: 项目详情 -->
  262. <!-- <el-tab-pane :label="getProjectDetailLabel()" name="projectDetail">
  263. <exp-project-detail
  264. v-if="currentRow.applyNo"
  265. ref="projectDetail"
  266. :apply-no="currentRow.applyNo"
  267. :status="currentRow.status"
  268. :height="detailHeight"
  269. @refresh="handleDetailRefresh">
  270. </exp-project-detail>
  271. <div v-else class="empty-tip">
  272. 请在上方表格中选择一条试验单记录
  273. </div>
  274. </el-tab-pane>-->
  275. <el-tab-pane label="附件上传" name="attachment">
  276. <erf-attachment-manager
  277. v-if="currentRow.applyNo"
  278. ref="attachmentManager"
  279. :apply-no="currentRow.applyNo"
  280. :created-by="currentRow.projectLeader"
  281. :disabled="false"
  282. :height="detailHeight">
  283. </erf-attachment-manager>
  284. <div v-else class="empty-tip">
  285. 请在上方表格中选择一条试验单记录
  286. </div>
  287. </el-tab-pane>
  288. <!-- Tab: 三方确认 -->
  289. <el-tab-pane label="三方确认" v-if="currentRow.experimentType==='High Risk'" name="triConfirm">
  290. <exp-tri-confirm
  291. v-if="currentRow.applyNo&&currentRow.experimentType==='High Risk'"
  292. ref="triConfirm"
  293. :apply-no="currentRow.applyNo"
  294. :bu-no="currentRow.buNo"
  295. :experiment-type="currentRow.experimentType"
  296. :height="detailHeight">
  297. </exp-tri-confirm>
  298. <div v-else class="empty-tip">
  299. 请在上方表格中选择一条试验单记录
  300. </div>
  301. </el-tab-pane>
  302. <!-- Tab: 原材料清单 -->
  303. <el-tab-pane label="原材料清单" name="rawMaterial">
  304. <exp-raw-material-list
  305. v-if="currentRow.applyNo"
  306. ref="rawMaterialList"
  307. :apply-no="currentRow.applyNo"
  308. :site="currentRow.site || $store.state.user.site"
  309. :buNo="currentRow.buNo"
  310. :disabled="currentRow.status === '已完成' || currentRow.status === '已取消'"
  311. :height="detailHeight">
  312. </exp-raw-material-list>
  313. <div v-else class="empty-tip">
  314. <i class="el-icon-document" style="font-size: 40px; color: #C0C4CC; margin-bottom: 10px"></i>
  315. <p style="font-size: 13px">请选择试验单查看原材料清单</p>
  316. </div>
  317. </el-tab-pane>
  318. <!-- Tab: 审批状态和日志 -->
  319. <el-tab-pane label="审批状态和日志" name="approvalStatus">
  320. <div v-if="currentRow.applyNo" :style="{height: detailHeight + 'px', overflowY: 'auto', padding: '0px 10px'}">
  321. <!-- 两栏布局审批流程 | 审批日志 -->
  322. <div class="two-column-layout">
  323. <!-- 左栏审批流程 -->
  324. <div class="stages-column">
  325. <div class="column-header">
  326. <i class="el-icon-s-order"></i>
  327. <span>审批流程</span>
  328. <span class="progress-badge">{{ flowStatus.progressPercent || 0 }}%</span>
  329. </div>
  330. <div class="stages-list">
  331. <div
  332. v-for="(stage, index) in flowStatus.stages"
  333. :key="index"
  334. class="stage-item"
  335. :class="'stage-' + stage.status">
  336. <div class="stage-icon">
  337. <i :class="getStageIcon(stage.status)"></i>
  338. </div>
  339. <div class="stage-content">
  340. <div class="stage-name">{{ stage.nodeName }}</div>
  341. <div class="stage-meta">
  342. <el-tag :type="getStageTagType(stage.status)" size="mini" effect="plain">
  343. {{ getStageStatusText(stage.status) }}
  344. </el-tag>
  345. <span v-if="stage.completeTime" class="stage-time">{{ stage.completeTime }}</span>
  346. </div>
  347. </div>
  348. </div>
  349. </div>
  350. </div>
  351. <!-- 右栏审批日志表格 -->
  352. <div class="logs-column">
  353. <div class="column-header">
  354. <i class="el-icon-tickets"></i>
  355. <span>审批日志</span>
  356. <span class="logs-count">{{ flowStatus.approvalLogs ? flowStatus.approvalLogs.length : 0 }}</span>
  357. </div>
  358. <div class="logs-table-wrapper">
  359. <el-table
  360. :data="flowStatus.approvalLogs"
  361. size="small"
  362. class="approval-logs-table"
  363. style="width: 100%"
  364. height="34vh">
  365. <el-table-column prop="logTime" label="时间" width="150" align="center">
  366. <template slot-scope="scope">
  367. <span style="font-size: 12px">{{ formatDateTime(scope.row.logTime) }}</span>
  368. </template>
  369. </el-table-column>
  370. <el-table-column prop="action" label="操作" width="90" align="center">
  371. <template slot-scope="scope">
  372. <el-tag :type="getActionTagType(scope.row.action)" size="mini">
  373. {{ scope.row.action }}
  374. </el-tag>
  375. </template>
  376. </el-table-column>
  377. <el-table-column prop="nodeCode" label="节点" width="150" align="center">
  378. <template slot-scope="scope">
  379. <span style="font-size: 12px">{{ scope.row.nodeCode }}</span>
  380. </template>
  381. </el-table-column>
  382. <el-table-column prop="operatorName" label="操作人" width="100" align="center">
  383. <template slot-scope="scope">
  384. <span style="font-size: 12px">{{ scope.row.operatorName }}</span>
  385. </template>
  386. </el-table-column>
  387. <el-table-column prop="comment" label="备注" min-width="180" show-overflow-tooltip>
  388. <template slot-scope="scope">
  389. <span style="font-size: 12px; color: #606266">{{ scope.row.comment || '-' }}</span>
  390. </template>
  391. </el-table-column>
  392. </el-table>
  393. </div>
  394. </div>
  395. </div>
  396. </div>
  397. <div v-else class="empty-tip">
  398. <i class="el-icon-document" style="font-size: 40px; color: #C0C4CC; margin-bottom: 10px"></i>
  399. <p style="font-size: 13px">请选择试验单查看审批状态</p>
  400. </div>
  401. </el-tab-pane>
  402. <!-- Tab: 备注 -->
  403. <el-tab-pane label="备注" name="remark">
  404. <div v-if="currentRow.applyNo" style="padding: 16px;">
  405. <el-input
  406. type="textarea"
  407. v-model="remarkText"
  408. :rows="10"
  409. placeholder="请输入备注内容"
  410. style="width: 100%"
  411. ></el-input>
  412. <div style="margin-top: 12px; text-align: left;">
  413. <el-button
  414. type="primary"
  415. size="small"
  416. :loading="remarkSaving"
  417. @click="saveRemarkHandle">保存备注</el-button>
  418. </div>
  419. </div>
  420. <div v-else class="empty-tip">
  421. <i class="el-icon-edit-outline" style="font-size: 40px; color: #C0C4CC; margin-bottom: 10px"></i>
  422. <p style="font-size: 13px">请选择试验单填写备注</p>
  423. </div>
  424. </el-tab-pane>
  425. </el-tabs>
  426. <!-- 新增/编辑弹窗 -->
  427. <el-dialog
  428. :title="dialogTitle"
  429. :visible.sync="dialogVisible"
  430. width="550px"
  431. :close-on-click-modal="false"
  432. v-drag>
  433. <exp-apply-form
  434. v-if="dialogVisible"
  435. ref="applyForm"
  436. :apply-data="currentApply"
  437. :readonly="dialogReadonly">
  438. </exp-apply-form>
  439. <el-footer style="height: 40px; margin-top: 50px; text-align: center">
  440. <el-button type="primary" @click="saveApply" v-if="!dialogReadonly" :loading="saveLoading">
  441. {{ saveLoading ? '保存中...' : '保存' }}
  442. </el-button>
  443. <el-button type="primary" @click="dialogVisible = false">关闭</el-button>
  444. </el-footer>
  445. </el-dialog>
  446. <!-- 样品确认弹窗 -->
  447. <el-dialog
  448. title="样品确认"
  449. :visible.sync="sampleConfirmVisible"
  450. width="320px"
  451. :close-on-click-modal="false">
  452. <div style="margin-top: 1px;margin-left: 10px; color: #909399; font-size: 12px">
  453. 不输入或输入0表示样品报废大于0表示正常入库
  454. </div>
  455. <el-form :model="sampleConfirmData" label-width="100px" size="small">
  456. <el-form-item label="试验单号">
  457. <el-tag type="primary">{{ sampleConfirmData.applyNo }}</el-tag>
  458. </el-form-item>
  459. <el-form-item label="样品数量" required>
  460. <el-input
  461. v-model="sampleConfirmData.sampleQuantity"
  462. :min="0"
  463. :precision="0"
  464. placeholder="请输入样品数量"
  465. style="width: 80%">
  466. </el-input>
  467. </el-form-item>
  468. <el-form-item required>
  469. <span style="cursor: pointer" slot="label" @click="getBaseList(510)"><a herf="#">计量单位</a></span>
  470. <el-input v-model="sampleConfirmData.umid" style="width: 128px"></el-input>
  471. </el-form-item>
  472. <el-form-item label="完成日期" required>
  473. <el-date-picker
  474. v-model="sampleConfirmData.finalFinishDate"
  475. type="date"
  476. format="yyyy-MM-dd"
  477. value-format="yyyy-MM-dd"
  478. placeholder="请选择最终完成日期"
  479. style="width: 80%">
  480. </el-date-picker>
  481. </el-form-item>
  482. <el-form-item label="样品状态">
  483. <el-tag :type="getSampleStatusTagType()">
  484. {{ getSampleStatusText() }}
  485. </el-tag>
  486. </el-form-item>
  487. </el-form>
  488. <div slot="footer" class="dialog-footer">
  489. <el-button type="primary" @click="confirmSampleSubmit" :loading="sampleConfirmLoading">
  490. {{ sampleConfirmLoading ? '确认中...' : '确认' }}
  491. </el-button>
  492. <el-button @click="sampleConfirmVisible = false">关闭</el-button>
  493. </div>
  494. </el-dialog>
  495. <!-- 复制试验单弹窗 -->
  496. <el-dialog
  497. title="复制试验单"
  498. :visible.sync="copyDialogVisible"
  499. width="400px"
  500. :close-on-click-modal="false">
  501. <div style="margin-bottom: 15px;margin-left: 40px; color: #606266; font-size: 13px">
  502. <i class="el-icon-info" style="color: #409EFF"></i>
  503. 选择需要复制的内容将创建一个新的草稿试验单
  504. </div>
  505. <el-form :model="copyData" label-width="120px" size="small">
  506. <el-form-item label="源试验单号">
  507. <el-tag type="primary">{{ copyData.sourceApplyNo }}</el-tag>
  508. </el-form-item>
  509. <el-form-item label="试验单号">
  510. <el-input v-model="copyData.applyNo" placeholder="请输入试验单号"></el-input>
  511. </el-form-item>
  512. <el-form-item label="复制选项">
  513. <el-checkbox-group v-model="copyData.copyOptions" class="checkbox-vertical">
  514. <el-checkbox v-if="currentRow.experimentType === 'High Risk'" label="triConfirm">三方确认信息</el-checkbox>
  515. <el-checkbox label="rawMaterialList">原材料清单</el-checkbox>
  516. <el-checkbox label="attachment">附件</el-checkbox>
  517. </el-checkbox-group>
  518. </el-form-item>
  519. </el-form>
  520. <div slot="footer" class="dialog-footer">
  521. <el-button type="primary" @click="confirmCopy" :loading="copyLoading">
  522. {{ copyLoading ? '复制中...' : '确认复制' }}
  523. </el-button>
  524. <el-button @click="copyDialogVisible = false">取消</el-button>
  525. </div>
  526. </el-dialog>
  527. <!-- 下达确认弹窗 - 选择审批人 -->
  528. <el-dialog
  529. title="确认审批人信息"
  530. :visible.sync="submitDialogVisible"
  531. width="500px"
  532. :close-on-click-modal="false">
  533. <el-form :model="submitData" label-width="80px" size="small">
  534. <!-- 技术经理 -->
  535. <el-form-item label="技术经理">
  536. <el-tag type="success" size="medium">
  537. {{ submitData.techManagerName }}
  538. </el-tag>
  539. <span style="margin-left: 10px; color: #909399; font-size: 12px">
  540. (根据发起人角色自动分配)
  541. </span>
  542. </el-form-item>
  543. <!-- 生产经理 -->
  544. <el-form-item label="生产经理" required>
  545. <el-select class="manager-select"
  546. v-model="submitData.prodManagerIds"
  547. multiple
  548. placeholder="请选择生产经理"
  549. style="width: 100%">
  550. <el-option
  551. v-for="manager in prodManagerList"
  552. :key="manager.userId"
  553. :label="manager.userDisplay || manager.username"
  554. :value="manager.userId">
  555. </el-option>
  556. </el-select>
  557. <div style="margin-top: 1px; color: #909399; font-size: 12px">
  558. 可多选所有选中的生产经理都必须审批通过后才会流转到计划员排产
  559. </div>
  560. </el-form-item>
  561. <!-- 质量经理 -->
  562. <el-form-item label="质量经理" required>
  563. <el-select class="manager-select"
  564. v-model="submitData.qualityManagerIds"
  565. multiple
  566. placeholder="请选择质量经理"
  567. style="width: 100%">
  568. <el-option
  569. v-for="manager in qualityManagerList"
  570. :key="manager.userId"
  571. :label="manager.userDisplay || manager.username"
  572. :value="manager.userId">
  573. </el-option>
  574. </el-select>
  575. <div style="margin-top: 1px; color: #909399; font-size: 12px">
  576. 可多选所有选中的质量经理都必须审批通过后才会流转到计划员排产
  577. </div>
  578. </el-form-item>
  579. <!-- 计划员 -->
  580. <el-form-item label="计划员" required>
  581. <el-select class="manager-select"
  582. v-model="submitData.plannerIds"
  583. placeholder="请选择计划员(可多选)"
  584. style="width: 100%"
  585. multiple
  586. clearable
  587. @remove-tag="handlePlannerRemoveTag">
  588. <el-option
  589. v-for="planner in plannerList"
  590. :key="planner.userId"
  591. :label="planner.userDisplay || planner.username"
  592. :value="planner.userId">
  593. </el-option>
  594. </el-select>
  595. <div style="margin-top: 1px; color: #909399; font-size: 12px">
  596. 经理审批全部通过后任意选定的计划员均可排产
  597. </div>
  598. </el-form-item>
  599. </el-form>
  600. <div slot="footer" class="dialog-footer">
  601. <el-button @click="submitDialogVisible = false">取消</el-button>
  602. <el-button type="primary" @click="confirmSubmit" :loading="submitLoading">
  603. {{ submitLoading ? '下达中...' : '确认下达' }}
  604. </el-button>
  605. </div>
  606. </el-dialog>
  607. <!-- chooseList模态框 -->
  608. <Chooselist ref="baseList" @getBaseData="getBaseData"></Chooselist>
  609. </div>
  610. </template>
  611. <script>
  612. import { searchExpApplyList, submitExpApply, deleteExpApply, withdrawExpApply, cancelExpApply, copyExpApply, getSubmitApprovers, getFlowStatus, getTriConfirmList, confirmSample, saveRemark, urgeApproval } from '@/api/erf/erf'
  613. import { getBuList } from '@/api/factory/site'
  614. import { queryOss } from '@/api/oss/oss'
  615. import ExpApplyForm from './components/expApplyForm.vue'
  616. import ExpProjectDetail from './components/expProjectDetail.vue'
  617. import ExpTriConfirm from './components/expTriConfirm.vue'
  618. import ErfAttachmentManager from './components/erfAttachmentManager.vue'
  619. import ExpRawMaterialList from './components/expRawMaterialList.vue'
  620. import Chooselist from '@/views/modules/common/Chooselist_eam'
  621. export default {
  622. name: 'ExpApplyList',
  623. components: {
  624. Chooselist,
  625. ExpApplyForm,
  626. ExpProjectDetail,
  627. ExpTriConfirm,
  628. ErfAttachmentManager,
  629. ExpRawMaterialList
  630. },
  631. data() {
  632. return {
  633. tagNo:'',
  634. sendLoading: false, // 发送邮件的加载状态
  635. buList: [],
  636. // 查询条件
  637. queryHeaderData: {
  638. applyNo: '',
  639. buNo: '',
  640. experimentType: '',
  641. status: '',
  642. creatorName: '',
  643. createStartDate: '',
  644. createEndDate: '',
  645. page: 1,
  646. limit: 20
  647. },
  648. // 数据列表
  649. dataList: [],
  650. // 分页参数
  651. pageIndex: 1,
  652. pageSize: 20,
  653. totalPage: 0,
  654. dataListLoading: false,
  655. // 弹窗相关
  656. dialogVisible: false,
  657. dialogTitle: '新增试验单',
  658. dialogReadonly: false,
  659. currentApply: {},
  660. saveLoading: false,
  661. // 当前选中行
  662. currentRow: {},
  663. // 备注
  664. remarkText: '',
  665. remarkSaving: false,
  666. // Tab页签
  667. activeName: 'attachment',
  668. // 表格高度
  669. tableHeight: (window.innerHeight - 260)/2,
  670. detailHeight: '35vh',
  671. // 下达确认弹窗
  672. submitDialogVisible: false,
  673. submitLoading: false,
  674. submitData: {
  675. applyNo: '',
  676. buNo: '',
  677. techManagerId: null,
  678. techManagerName: '',
  679. prodManagerIds: [],
  680. qualityManagerIds: [],
  681. plannerIds: []
  682. },
  683. prodManagerList: [], // 生产经理候选列表
  684. qualityManagerList: [], // 质量经理候选列表
  685. plannerList: [], // 计划员候选列表
  686. lockedPlannerUserId: null, // 默认计划员(郑宇),不可移除
  687. // 流程状态数据
  688. flowStatus: {
  689. applyNo: '',
  690. currentStatus: '',
  691. currentStep: '',
  692. progressPercent: 0,
  693. stages: [],
  694. approvalLogs: []
  695. },
  696. // 样品确认弹窗
  697. sampleConfirmVisible: false,
  698. sampleConfirmLoading: false,
  699. sampleConfirmData: {
  700. applyNo: '',
  701. sampleQuantity: null,
  702. finalFinishDate: '',
  703. umid:''
  704. },
  705. // 复制试验单弹窗
  706. copyDialogVisible: false,
  707. copyLoading: false,
  708. copyData: {
  709. sourceApplyNo: '',
  710. applyNo: '',
  711. copyOptions: ['triConfirm','rawMaterialList'] // 默认全选
  712. },
  713. // 催办
  714. urgeLoading: false
  715. }
  716. },
  717. activated() {
  718. this.loadBuList()
  719. this.getDataList()
  720. },
  721. methods: {
  722. // ======== chooseList相关方法 ========
  723. /**
  724. * 获取基础数据列表S
  725. * @param val
  726. * @param type
  727. */
  728. getBaseList (val, type) {
  729. this.tagNo = val
  730. this.$nextTick(() => {
  731. let strVal = ''
  732. let conSql = ''
  733. if (val === 510) {
  734. strVal = this.sampleConfirmData.umid?this.sampleConfirmData.umid:''
  735. conSql = " and site = '" + (this.currentRow.buNo[1]?this.currentRow.buNo[1]:'1') + "'"
  736. }
  737. this.$refs.baseList.init(val, strVal, conSql)
  738. })
  739. },
  740. /**
  741. * 列表方法的回调
  742. * @param val
  743. */
  744. getBaseData (val) {
  745. if (this.tagNo === 510) {
  746. this.sampleConfirmData.umid = val.UMID
  747. }
  748. },
  749. /**
  750. * 加载事业部列表
  751. */
  752. loadBuList() {
  753. const tempData = { site: this.$store.state.user.site }
  754. getBuList(tempData).then(({data}) => {
  755. if (data.code === 0) {
  756. this.buList = data.row1
  757. }
  758. })
  759. },
  760. /**
  761. * 获取试验单列表
  762. */
  763. getDataList(flag) {
  764. if (flag === 'Y') {
  765. this.pageIndex = 1
  766. }
  767. this.queryHeaderData.page = this.pageIndex
  768. this.queryHeaderData.limit = this.pageSize
  769. this.dataListLoading = true
  770. searchExpApplyList(this.queryHeaderData).then(({data}) => {
  771. this.dataListLoading = false
  772. if (data && data.code === 0) {
  773. this.dataList = data.page.list || []
  774. this.totalPage = data.page.totalCount || 0
  775. // 如果有数据,处理行选中
  776. if (this.dataList.length > 0) {
  777. // 尝试找到之前选中的行
  778. if (this.currentRow.applyNo) {
  779. const selectedRow = this.dataList.find(item => item.applyNo === this.currentRow.applyNo)
  780. if (selectedRow) {
  781. // 如果找到之前选中的行,重新高亮
  782. this.$nextTick(() => {
  783. this.handleRowClick(selectedRow)
  784. })
  785. } else {
  786. // 如果没找到,选中第一行
  787. this.handleRowClick(this.dataList[0])
  788. }
  789. } else {
  790. // 如果之前没有选中任何行,默认选中第一行
  791. this.handleRowClick(this.dataList[0])
  792. }
  793. } else {
  794. // 没有数据,清空当前行
  795. this.currentRow = {}
  796. }
  797. } else {
  798. this.dataList = []
  799. this.totalPage = 0
  800. this.currentRow = {}
  801. this.$message.error(data.msg || '查询失败')
  802. }
  803. }).catch(error => {
  804. this.dataListLoading = false
  805. this.$message.error('查询异常')
  806. })
  807. },
  808. /**
  809. * 重置查询条件
  810. */
  811. resetQuery() {
  812. this.queryHeaderData = {
  813. applyNo: '',
  814. buNo: '',
  815. experimentType: '',
  816. status: '',
  817. creatorName: '',
  818. createStartDate: '',
  819. createEndDate: '',
  820. page: 1,
  821. limit: 20
  822. }
  823. this.getDataList('Y')
  824. },
  825. /**
  826. * 打开新增对话框
  827. */
  828. openCreateDialog() {
  829. this.dialogTitle = '新增试验单'
  830. this.dialogReadonly = false
  831. this.currentApply = {}
  832. this.dialogVisible = true
  833. },
  834. /**
  835. * 查看详情
  836. */
  837. viewDetail(row) {
  838. this.dialogTitle = '查看试验单'
  839. this.dialogReadonly = true
  840. this.currentApply = { ...row }
  841. this.dialogVisible = true
  842. },
  843. /**
  844. * 编辑试验单
  845. */
  846. editApply(row) {
  847. this.dialogTitle = '修改试验单'
  848. this.dialogReadonly = false
  849. this.currentApply = { ...row }
  850. this.dialogVisible = true
  851. },
  852. /**
  853. * 保存试验单
  854. */
  855. saveApply() {
  856. const formData = this.$refs.applyForm.getFormData()
  857. if (!formData) {
  858. return
  859. }
  860. this.saveLoading = true
  861. this.$refs.applyForm.save().then(() => {
  862. this.saveLoading = false
  863. this.$message.success('保存成功')
  864. this.dialogVisible = false
  865. this.getDataList()
  866. }).catch(error => {
  867. this.saveLoading = false
  868. })
  869. },
  870. /**
  871. * 下达试验单 - 打开审批人确认弹窗
  872. */
  873. submitApply(row) {
  874. // 第一步:所有试验单都必须验证附件
  875. console.log('🔍 开始验证附件...')
  876. this.validateAttachmentByApi(row.applyNo).then(hasAttachment => {
  877. if (!hasAttachment) {
  878. this.$alert('下达前必须至少上传一个附件', '操作提示', {
  879. confirmButtonText: '确定',
  880. type: 'warning'
  881. })
  882. return
  883. }
  884. // 第二步:如果是High Risk,继续验证工序
  885. if (row.experimentType === 'High Risk') {
  886. this.validateHighRiskProcessByApi(row.applyNo).then(isValid => {
  887. if (!isValid) {
  888. this.$alert('High Risk试验单必须至少有一条完整的工序(包含车间、质量、技术三个负责人)才能下达', '操作提示', {
  889. confirmButtonText: '确定',
  890. type: 'warning'
  891. })
  892. return
  893. }
  894. this.proceedSubmit(row)
  895. }).catch(error => {
  896. this.$message.error('验证工序失败,请重试')
  897. })
  898. } else {
  899. // Low Risk 附件验证通过后直接下达
  900. this.proceedSubmit(row)
  901. }
  902. }).catch(error => {
  903. this.$message.error('验证附件失败,请重试')
  904. })
  905. },
  906. /**
  907. * 通过API验证试验单是否已上传附件
  908. *
  909. * @param {String} applyNo 试验单号
  910. * @return {Promise<Boolean>} 验证结果
  911. */
  912. validateAttachmentByApi(applyNo) {
  913. return new Promise((resolve, reject) => {
  914. queryOss({
  915. orderRef1: 'ERF',
  916. orderRef2: applyNo,
  917. orderRef6: 'EXP_APPLY'
  918. }).then(({data}) => {
  919. if (data && data.code === 0) {
  920. const fileList = data.rows || []
  921. console.log('📎 查询到附件数量:', fileList.length)
  922. resolve(fileList.length > 0)
  923. } else {
  924. reject(new Error(data.msg || '查询附件列表失败'))
  925. }
  926. }).catch(error => {
  927. reject(error)
  928. })
  929. })
  930. },
  931. /**
  932. * 通过API验证High Risk工序
  933. *
  934. * @param {String} applyNo 试验单号
  935. * @return {Promise<Boolean>} 验证结果
  936. */
  937. validateHighRiskProcessByApi(applyNo) {
  938. return new Promise((resolve, reject) => {
  939. // 调用API查询工序列表
  940. getTriConfirmList({ applyNo: applyNo }).then(({data}) => {
  941. if (data && data.code === 0) {
  942. const processList = data.list || []
  943. console.log('📊 查询到工序数量:', processList.length)
  944. // 检查是否至少有一条完整的工序
  945. const completeProcesses = processList.filter(process => {
  946. const isComplete = process.prodApproverName &&
  947. process.qaApproverName &&
  948. process.techApproverName
  949. if (isComplete) {
  950. console.log('✅ 完整工序:', process.processStep, {
  951. 车间: process.prodApproverName,
  952. 质量: process.qaApproverName,
  953. 技术: process.techApproverName
  954. })
  955. }
  956. return isComplete
  957. })
  958. console.log(`📈 完整工序数量: ${completeProcesses.length}/${processList.length}`)
  959. // 至少需要一条完整的工序
  960. resolve(completeProcesses.length > 0)
  961. } else {
  962. reject(new Error(data.msg || '查询工序列表失败'))
  963. }
  964. }).catch(error => {
  965. reject(error)
  966. })
  967. })
  968. },
  969. /**
  970. * 执行下达流程
  971. */
  972. proceedSubmit(row) {
  973. // 一次性获取所有审批人信息
  974. getSubmitApprovers({
  975. userId: this.$store.state.user.id,
  976. buNo: row.buNo
  977. }).then(({data}) => {
  978. if (data && data.code === 0) {
  979. // 设置生产经理、质量经理、计划员列表
  980. this.prodManagerList = data.prodManagers || []
  981. this.qualityManagerList = data.qualityManagers || []
  982. this.plannerList = data.planners || []
  983. // 记录默认计划员郑宇的 userId,用于锁定不可移除
  984. const joyce = this.plannerList.find(p => p.userDisplay && p.userDisplay.includes('郑宇'))
  985. this.lockedPlannerUserId = joyce ? joyce.userId : null
  986. // 根据事业部设置默认生产经理(使用user_display模糊匹配)
  987. const defaultProdManagerIds = []
  988. if (row.buDesc === 'RFID') {
  989. // RFID默认Tony
  990. const tony = this.prodManagerList.find(m =>
  991. m.userDisplay && m.userDisplay.toLowerCase().includes('tony')
  992. )
  993. if (tony) defaultProdManagerIds.push(tony.userId)
  994. } else if (row.buDesc === 'RF') {
  995. // RF默认Charles
  996. const charles = this.prodManagerList.find(m =>
  997. m.userDisplay && m.userDisplay.toLowerCase().includes('charles')
  998. )
  999. if (charles) defaultProdManagerIds.push(charles.userId)
  1000. }
  1001. // 根据事业部设置默认质量经理(使用user_display模糊匹配)
  1002. const defaultQualityManagerIds = []
  1003. if (row.buDesc === 'RFID') {
  1004. // RFID默认Victor
  1005. const victor = this.qualityManagerList.find(m =>
  1006. m.userDisplay && m.userDisplay.toLowerCase().includes('victor')
  1007. )
  1008. if (victor) defaultQualityManagerIds.push(victor.userId)
  1009. } else if (row.buDesc === 'RF') {
  1010. // RF默认尹君
  1011. const yin = this.qualityManagerList.find(m =>
  1012. m.userDisplay && (m.userDisplay.includes('尹君') || m.userDisplay.toLowerCase().includes('yin'))
  1013. )
  1014. if (yin) defaultQualityManagerIds.push(yin.userId)
  1015. }
  1016. // 设置表单数据
  1017. this.submitData = {
  1018. applyNo: row.applyNo,
  1019. buNo: row.buNo,
  1020. techManagerId: data.techManager.managerId,
  1021. techManagerName: data.techManager.managerName,
  1022. prodManagerIds: defaultProdManagerIds,
  1023. qualityManagerIds: defaultQualityManagerIds,
  1024. plannerIds: (() => {
  1025. // 默认选中Joyce(中文名郑宇);若无郑宇则单人列表时默认全选
  1026. const joyce = this.plannerList.find(p =>
  1027. p.userDisplay && p.userDisplay.toLowerCase().includes('郑宇')
  1028. )
  1029. if (joyce) return [joyce.userId]
  1030. return this.plannerList.length === 1 ? [this.plannerList[0].userId] : []
  1031. })()
  1032. }
  1033. // 显示弹窗
  1034. this.submitDialogVisible = true
  1035. } else {
  1036. this.$message.error(data.msg || '获取审批人信息失败')
  1037. }
  1038. }).catch(() => {
  1039. this.$message.error('获取审批人信息异常')
  1040. })
  1041. },
  1042. /**
  1043. * 计划员移除标签拦截默认计划员不允许被移除
  1044. */
  1045. handlePlannerRemoveTag(removedId) {
  1046. if (this.lockedPlannerUserId && removedId === this.lockedPlannerUserId) {
  1047. this.$nextTick(() => {
  1048. if (!this.submitData.plannerIds.includes(this.lockedPlannerUserId)) {
  1049. this.submitData.plannerIds = [this.lockedPlannerUserId, ...this.submitData.plannerIds]
  1050. }
  1051. })
  1052. this.$message.warning('该计划员不可移除')
  1053. }
  1054. },
  1055. /**
  1056. * 确认下达
  1057. */
  1058. confirmSubmit() {
  1059. // 验证必填项
  1060. if (!this.submitData.techManagerId) {
  1061. this.$message.warning('技术经理未分配')
  1062. return
  1063. }
  1064. if (!this.submitData.prodManagerIds || this.submitData.prodManagerIds.length === 0) {
  1065. this.$message.warning('请至少选择一位生产经理')
  1066. return
  1067. }
  1068. if (!this.submitData.qualityManagerIds || this.submitData.qualityManagerIds.length === 0) {
  1069. this.$message.warning('请至少选择一位质量经理')
  1070. return
  1071. }
  1072. if (!this.submitData.plannerIds || this.submitData.plannerIds.length === 0) {
  1073. this.$message.warning('请至少选择一位计划员')
  1074. return
  1075. }
  1076. this.submitLoading = true
  1077. // 调用下达API
  1078. submitExpApply({
  1079. applyNo: this.submitData.applyNo,
  1080. techManagerId: this.submitData.techManagerId,
  1081. prodManagerIds: this.submitData.prodManagerIds,
  1082. qualityManagerIds: this.submitData.qualityManagerIds,
  1083. plannerUserIds: this.submitData.plannerIds
  1084. }).then(({data}) => {
  1085. this.submitLoading = false
  1086. if (data && data.code === 0) {
  1087. this.$message.success('下达成功,已通知相关审批人')
  1088. this.submitDialogVisible = false
  1089. this.getDataList()
  1090. // 触发全局审批通知检查(通知审批人)
  1091. this.triggerApprovalNotification()
  1092. } else {
  1093. this.$message.error(data.msg || '下达失败')
  1094. }
  1095. }).catch(() => {
  1096. this.submitLoading = false
  1097. })
  1098. },
  1099. /**
  1100. * 触发全局审批通知检查
  1101. * 在试验单下达成功后调用立即通知审批人
  1102. */
  1103. triggerApprovalNotification() {
  1104. try {
  1105. // 通过 $root 访问 App.vue 的方法
  1106. if (this.$root && this.$root.$children && this.$root.$children[0]) {
  1107. const app = this.$root.$children[0]
  1108. if (app.checkApprovalNotifications) {
  1109. // 延迟1秒后检查,确保后端数据已更新
  1110. setTimeout(() => {
  1111. //app.checkApprovalNotifications()
  1112. console.log('[审批通知] 已触发手动检查')
  1113. }, 1000)
  1114. }
  1115. }
  1116. } catch (error) {
  1117. console.log('[审批通知] 触发通知检查失败:', error.message)
  1118. }
  1119. },
  1120. /**
  1121. * 删除试验单
  1122. */
  1123. deleteApply(row) {
  1124. this.$confirm('确定删除该试验单?', '操作提示', {
  1125. confirmButtonText: '确定',
  1126. cancelButtonText: '取消',
  1127. type: 'warning'
  1128. }).then(() => {
  1129. deleteExpApply({ applyNo: row.applyNo }).then(({data}) => {
  1130. if (data && data.code === 0) {
  1131. this.$message.success('删除成功')
  1132. this.getDataList()
  1133. } else {
  1134. this.$message.error(data.msg || '删除失败')
  1135. }
  1136. })
  1137. })
  1138. },
  1139. /**
  1140. * 撤回试验单
  1141. */
  1142. withdrawApply(row) {
  1143. this.$confirm('确定撤回该试验单?撤回后将变为草稿状态', '操作提示', {
  1144. confirmButtonText: '确定',
  1145. cancelButtonText: '取消',
  1146. type: 'warning'
  1147. }).then(() => {
  1148. withdrawExpApply({
  1149. applyNo: row.applyNo,
  1150. currentUserId: this.$store.state.user.id
  1151. }).then(({data}) => {
  1152. if (data && data.code === 0) {
  1153. this.$message.success('撤回成功')
  1154. this.getDataList()
  1155. } else {
  1156. this.$message.error(data.msg || '撤回失败')
  1157. }
  1158. })
  1159. })
  1160. },
  1161. /**
  1162. * 获取状态类型
  1163. */
  1164. getStatusType(status) {
  1165. const types = {
  1166. '草稿': 'info',
  1167. '已下达': 'warning',
  1168. '已批准': 'success',
  1169. '生产中': '',
  1170. '已完成': 'success',
  1171. '已取消': 'danger',
  1172. '已驳回': 'danger'
  1173. }
  1174. return types[status] || 'info'
  1175. },
  1176. /**
  1177. * 获取状态文本直接返回中文状态
  1178. */
  1179. getStatusText(status) {
  1180. return status || ''
  1181. },
  1182. /**
  1183. * 分页大小改变
  1184. */
  1185. sizeChangeHandle(val) {
  1186. this.pageSize = val
  1187. this.pageIndex = 1
  1188. this.getDataList()
  1189. },
  1190. /**
  1191. * 当前页改变
  1192. */
  1193. currentChangeHandle(val) {
  1194. this.pageIndex = val
  1195. this.getDataList()
  1196. },
  1197. /**
  1198. * 保存备注
  1199. */
  1200. saveRemarkHandle() {
  1201. if (!this.currentRow.applyNo) {
  1202. this.$message.warning('请先选择试验单')
  1203. return
  1204. }
  1205. this.remarkSaving = true
  1206. saveRemark({ applyNo: this.currentRow.applyNo, remark: this.remarkText }).then(({data}) => {
  1207. this.remarkSaving = false
  1208. if (data && data.code === 0) {
  1209. this.$message.success('备注保存成功')
  1210. this.getDataList('Y')
  1211. //this.currentRow.remark = this.remarkText
  1212. } else {
  1213. this.$message.error(data.msg || '保存失败')
  1214. }
  1215. }).catch(() => {
  1216. this.remarkSaving = false
  1217. this.$message.error('保存异常')
  1218. })
  1219. },
  1220. /**
  1221. * 行点击事件
  1222. */
  1223. handleRowClick(row) {
  1224. this.currentRow = JSON.parse(JSON.stringify(row))
  1225. this.remarkText = row.remark || ''
  1226. // 设置表格当前行高亮
  1227. this.$nextTick(() => {
  1228. this.$refs.dataTable.setCurrentRow(row)
  1229. })
  1230. // 根据当前tab刷新对应的数据,切换行后保持在当前tab
  1231. if (this.activeName === 'approvalStatus') {
  1232. this.loadFlowStatus()
  1233. } else if (this.activeName === 'triConfirm') {
  1234. this.$nextTick(() => {
  1235. if (this.$refs.triConfirm && this.$refs.triConfirm.loadProcessList) {
  1236. this.$refs.triConfirm.loadProcessList()
  1237. }
  1238. })
  1239. }
  1240. },
  1241. /**
  1242. * Tab切换事件
  1243. */
  1244. handleTabClick(tab) {
  1245. // Tab切换时可以做一些特殊处理
  1246. console.log('当前Tab:', tab.name)
  1247. // 切换到审批状态tab时加载流程状态
  1248. if (tab.name === 'approvalStatus' && this.currentRow.applyNo) {
  1249. this.loadFlowStatus()
  1250. }
  1251. // 切换到三方确认tab时刷新数据
  1252. if (tab.name === 'triConfirm' && this.currentRow.applyNo) {
  1253. this.$nextTick(() => {
  1254. if (this.$refs.triConfirm && this.$refs.triConfirm.loadProcessList) {
  1255. console.log('🔄 Tab切换,刷新三方确认数据')
  1256. this.$refs.triConfirm.loadProcessList()
  1257. }
  1258. })
  1259. }
  1260. },
  1261. /**
  1262. * 项目详情刷新事件
  1263. */
  1264. handleDetailRefresh() {
  1265. // 刷新当前行数据
  1266. this.getDataList()
  1267. },
  1268. /**
  1269. * 获取项目详情Tab标签
  1270. */
  1271. getProjectDetailLabel() {
  1272. if (this.currentRow.applyNo) {
  1273. return `项目详情 [${this.currentRow.applyNo}]`
  1274. }
  1275. return '项目详情'
  1276. },
  1277. /**
  1278. * 加载流程状态
  1279. */
  1280. loadFlowStatus() {
  1281. if (!this.currentRow.applyNo) {
  1282. return
  1283. }
  1284. getFlowStatus({ applyNo: this.currentRow.applyNo }).then(({data}) => {
  1285. if (data && data.code === 0) {
  1286. this.flowStatus = data.data || {
  1287. stages: [],
  1288. approvalLogs: []
  1289. }
  1290. }
  1291. })
  1292. },
  1293. /**
  1294. * 获取阶段颜色
  1295. */
  1296. getStageColor(status) {
  1297. const colors = {
  1298. 'pending': '#C0C4CC',
  1299. 'current': '#409EFF',
  1300. 'completed': '#67C23A',
  1301. 'rejected': '#F56C6C'
  1302. }
  1303. return colors[status] || '#C0C4CC'
  1304. },
  1305. /**
  1306. * 获取阶段图标
  1307. */
  1308. getStageIcon(status) {
  1309. const icons = {
  1310. 'pending': 'el-icon-time',
  1311. 'current': 'el-icon-loading',
  1312. 'completed': 'el-icon-success',
  1313. 'rejected': 'el-icon-error'
  1314. }
  1315. return icons[status] || 'el-icon-time'
  1316. },
  1317. /**
  1318. * 获取阶段标签类型
  1319. */
  1320. getStageTagType(status) {
  1321. const types = {
  1322. 'pending': 'info',
  1323. 'current': '',
  1324. 'completed': 'success',
  1325. 'rejected': 'danger'
  1326. }
  1327. return types[status] || 'info'
  1328. },
  1329. /**
  1330. * 获取阶段状态文本
  1331. */
  1332. getStageStatusText(status) {
  1333. const texts = {
  1334. 'pending': '未开始',
  1335. 'current': '进行中',
  1336. 'completed': '已完成',
  1337. 'rejected': '已驳回'
  1338. }
  1339. return texts[status] || '未知'
  1340. },
  1341. /**
  1342. * 获取操作类型标签
  1343. */
  1344. getActionTagType(action) {
  1345. const types = {
  1346. '下达': 'primary',
  1347. '批准': 'success',
  1348. '确认': 'success',
  1349. '驳回': 'danger',
  1350. '撤回': 'warning',
  1351. '提醒': 'info',
  1352. '已排产': 'success',
  1353. '录入最终结果': 'success',
  1354. '样品确认': 'success',
  1355. }
  1356. return types[action] || 'info'
  1357. },
  1358. /**
  1359. * 格式化日期时间
  1360. */
  1361. formatDateTime(dateTime) {
  1362. if (!dateTime) {
  1363. return ''
  1364. }
  1365. if (typeof dateTime === 'string') {
  1366. return dateTime
  1367. }
  1368. const date = new Date(dateTime)
  1369. const year = date.getFullYear()
  1370. const month = String(date.getMonth() + 1).padStart(2, '0')
  1371. const day = String(date.getDate()).padStart(2, '0')
  1372. const hours = String(date.getHours()).padStart(2, '0')
  1373. const minutes = String(date.getMinutes()).padStart(2, '0')
  1374. const seconds = String(date.getSeconds()).padStart(2, '0')
  1375. return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
  1376. },
  1377. /**
  1378. * 判断是否显示样品确认按钮
  1379. * Low Risk: 计划员排产完成后(currentStep="已完成"且status="生产中")显示
  1380. * High Risk: 三方确认阶段(currentStep="三方确认"且status="生产中")显示
  1381. */
  1382. showSampleConfirmBtn(row) {
  1383. if (row.experimentType === 'Low Risk') {
  1384. return row.currentStep === '样品确认'
  1385. } else if (row.experimentType === 'High Risk') {
  1386. return row.currentStep === '样品确认'
  1387. }
  1388. return false
  1389. },
  1390. /**
  1391. * 打开样品确认对话框
  1392. */
  1393. openSampleConfirm(row) {
  1394. this.sampleConfirmData = {
  1395. applyNo: row.applyNo,
  1396. umid: row.umid,
  1397. sampleQuantity: null,
  1398. finalFinishDate: ''
  1399. }
  1400. this.sampleConfirmVisible = true
  1401. console.log('打开样品确认对话框:'+this.sampleConfirmData)
  1402. },
  1403. /**
  1404. * 获取样品状态标签类型
  1405. */
  1406. getSampleStatusTagType() {
  1407. if (!this.sampleConfirmData.sampleQuantity || this.sampleConfirmData.sampleQuantity === 0) {
  1408. return 'danger'
  1409. }
  1410. return 'success'
  1411. },
  1412. /**
  1413. * 获取样品状态文本
  1414. */
  1415. getSampleStatusText() {
  1416. if (!this.sampleConfirmData.sampleQuantity || this.sampleConfirmData.sampleQuantity === 0) {
  1417. return '报废'
  1418. }
  1419. return '正常入库'
  1420. },
  1421. /**
  1422. * 提交样品确认
  1423. */
  1424. confirmSampleSubmit() {
  1425. // 验证最终完成日期必填
  1426. if (!this.sampleConfirmData.finalFinishDate) {
  1427. this.$message.warning('请选择最终完成日期')
  1428. return
  1429. }
  1430. // 确定样品状态
  1431. const finalStatus = (!this.sampleConfirmData.sampleQuantity || this.sampleConfirmData.sampleQuantity === 0)
  1432. ? '报废'
  1433. : '正常入库'
  1434. const finalQuantity = this.sampleConfirmData.sampleQuantity || 0
  1435. this.sampleConfirmLoading = true
  1436. confirmSample({
  1437. applyNo: this.sampleConfirmData.applyNo,
  1438. finalQuantity: finalQuantity,
  1439. finalStatus: finalStatus,
  1440. actualFinishDate: this.sampleConfirmData.finalFinishDate,
  1441. umid: this.sampleConfirmData.umid
  1442. }).then(({data}) => {
  1443. this.sampleConfirmLoading = false
  1444. if (data && data.code === 0) {
  1445. this.$message.success('样品确认成功')
  1446. this.sampleConfirmVisible = false
  1447. this.getDataList()
  1448. } else {
  1449. this.$message.error(data.msg || '样品确认失败')
  1450. }
  1451. }).catch(() => {
  1452. this.sampleConfirmLoading = false
  1453. this.$message.error('样品确认异常')
  1454. })
  1455. },
  1456. /**
  1457. * 工具栏删除试验单
  1458. */
  1459. deleteApplyFromToolbar() {
  1460. if (!this.currentRow.applyNo) {
  1461. this.$message.warning('请先选择一条试验单记录')
  1462. return
  1463. }
  1464. if (this.currentRow.status !== '草稿') {
  1465. this.$message.warning('只能删除草稿状态的试验单')
  1466. return
  1467. }
  1468. this.deleteApply(this.currentRow)
  1469. },
  1470. /**
  1471. * 工具栏撤回试验单
  1472. */
  1473. withdrawApplyFromToolbar() {
  1474. if (!this.currentRow.applyNo) {
  1475. this.$message.warning('请先选择一条试验单记录')
  1476. return
  1477. }
  1478. if (this.currentRow.status === '草稿' || this.currentRow.status === '已完成' ||
  1479. this.currentRow.status === '已驳回' || this.currentRow.status === '已取消') {
  1480. this.$message.warning('该状态下无法撤回')
  1481. return
  1482. }
  1483. this.withdrawApply(this.currentRow)
  1484. },
  1485. /**
  1486. * 工具栏取消试验单
  1487. */
  1488. cancelApplyFromToolbar() {
  1489. if (!this.currentRow.applyNo) {
  1490. this.$message.warning('请先选择一条试验单记录')
  1491. return
  1492. }
  1493. if (this.currentRow.status === '草稿') {
  1494. this.$message.warning('草稿状态的试验单请直接删除')
  1495. return
  1496. }
  1497. if (this.currentRow.status === '已完成' || this.currentRow.status === '已取消') {
  1498. this.$message.warning('该状态下无法取消')
  1499. return
  1500. }
  1501. this.$confirm('确定取消该试验单?取消后将终止所有待审批流程,状态变更为已取消', '操作提示', {
  1502. confirmButtonText: '确定',
  1503. cancelButtonText: '取消',
  1504. type: 'warning'
  1505. }).then(() => {
  1506. cancelExpApply({
  1507. applyNo: this.currentRow.applyNo,
  1508. currentUserId: this.$store.state.user.id
  1509. }).then(({data}) => {
  1510. if (data && data.code === 0) {
  1511. this.$message.success('取消成功')
  1512. this.getDataList()
  1513. } else {
  1514. this.$message.error(data.msg || '取消失败')
  1515. }
  1516. }).catch(() => {
  1517. this.$message.error('取消异常')
  1518. })
  1519. })
  1520. },
  1521. /**
  1522. * 打开复制试验单弹窗
  1523. */
  1524. openCopyDialog() {
  1525. if (!this.currentRow.applyNo) {
  1526. this.$message.warning('请先选择一条试验单记录')
  1527. return
  1528. }
  1529. // 根据试验类型判断是否显示三方确认选项
  1530. const defaultOptions = ['rawMaterialList']
  1531. if (this.currentRow.experimentType === 'High Risk') {
  1532. defaultOptions.unshift('triConfirm')
  1533. }
  1534. this.copyData = {
  1535. sourceApplyNo: this.currentRow.applyNo,
  1536. copyOptions: defaultOptions
  1537. }
  1538. this.copyDialogVisible = true
  1539. },
  1540. /**
  1541. * 确认复制试验单
  1542. */
  1543. confirmCopy() {
  1544. if (!this.copyData.applyNo) {
  1545. this.$message.warning('请输入新的试验单号')
  1546. return
  1547. }
  1548. this.copyLoading = true
  1549. const copyParams = {
  1550. sourceApplyNo: this.copyData.sourceApplyNo,
  1551. applyNo: this.copyData.applyNo,
  1552. copyTriConfirm: this.copyData.copyOptions.includes('triConfirm'),
  1553. copyAttachment: this.copyData.copyOptions.includes('attachment'),
  1554. copyRawMaterialList: this.copyData.copyOptions.includes('rawMaterialList'),
  1555. currentUserId: this.$store.state.user.id
  1556. }
  1557. copyExpApply(copyParams).then(({data}) => {
  1558. this.copyLoading = false
  1559. if (data && data.code === 0) {
  1560. this.$message.success('复制成功,已创建新的草稿试验单:' + data.newApplyNo)
  1561. this.copyDialogVisible = false
  1562. this.getDataList()
  1563. } else {
  1564. this.$message.error(data.msg || '复制失败')
  1565. }
  1566. }).catch(() => {
  1567. this.copyLoading = false
  1568. this.$message.error('复制异常')
  1569. })
  1570. },
  1571. /**
  1572. * 催办向该试验单所有未确认的审批人技术经理生产经理质量经理计划员发送催办邮件
  1573. */
  1574. urgeApproval() {
  1575. if (!this.currentRow.applyNo) {
  1576. this.$message.warning('请先选择一条试验单记录')
  1577. return
  1578. }
  1579. this.$confirm(
  1580. `确定向试验单【${this.currentRow.applyNo}】的所有未确认审批人发送催办邮件?`,
  1581. '催办确认',
  1582. {
  1583. confirmButtonText: '确定发送',
  1584. cancelButtonText: '取消',
  1585. type: 'warning'
  1586. }
  1587. ).then(() => {
  1588. this.urgeLoading = true
  1589. this.sendLoading = true
  1590. urgeApproval({ applyNo: this.currentRow.applyNo }).then(({data}) => {
  1591. this.urgeLoading = false
  1592. this.sendLoading = false
  1593. if (data && data.code === 0) {
  1594. this.$message.success(data.msg || '催办邮件已发送')
  1595. } else {
  1596. this.$message.error(data.msg || '催办失败')
  1597. }
  1598. }).catch(() => {
  1599. this.urgeLoading = false
  1600. this.sendLoading = false
  1601. this.$message.error('催办请求异常')
  1602. })
  1603. }).catch(() => {})
  1604. }
  1605. }
  1606. }
  1607. </script>
  1608. <style scoped>
  1609. .mod-config {
  1610. }
  1611. .el-form {
  1612. margin-bottom: 10px;
  1613. }
  1614. .dialog-footer {
  1615. text-align: center;
  1616. }
  1617. .empty-tip {
  1618. padding: 40px;
  1619. text-align: center;
  1620. color: #999;
  1621. font-size: 14px;
  1622. }
  1623. /* ==================== 页面专属样式 - 不影响全局 ==================== */
  1624. /* 查询表单样式 */
  1625. .exp-apply-page .query-form {
  1626. background-color: #FFFFFF;
  1627. padding: 15px 15px 5px 15px;
  1628. border-radius: 4px;
  1629. }
  1630. .exp-apply-page .query-form >>> .el-form-item__label {
  1631. color: #333333;
  1632. font-size: 13px;
  1633. padding-bottom: 5px;
  1634. }
  1635. .exp-apply-page .query-form >>> .el-input__inner {
  1636. height: 32px;
  1637. line-height: 32px;
  1638. border-radius: 4px;
  1639. border: 1px solid #DCDFE6;
  1640. font-size: 13px;
  1641. }
  1642. .exp-apply-page .query-form >>> .el-input__inner::placeholder {
  1643. color: #C0C4CC;
  1644. font-size: 13px;
  1645. }
  1646. .exp-apply-page .query-form >>> .el-select .el-input__inner {
  1647. border-radius: 4px;
  1648. }
  1649. .exp-apply-page .query-form >>> .el-date-editor .el-input__inner {
  1650. border-radius: 4px;
  1651. }
  1652. /* 按钮样式 - 扁平化风格 */
  1653. .exp-apply-page >>> .el-button {
  1654. height: 32px;
  1655. padding: 0 15px;
  1656. font-size: 13px;
  1657. border-radius: 4px;
  1658. }
  1659. /* 查询按钮 - 蓝色扁平 */
  1660. .exp-apply-page .search-btn {
  1661. background-color: #ECF5FF;
  1662. border-color: #B3D8FF;
  1663. color: #409EFF;
  1664. }
  1665. .exp-apply-page .search-btn:hover {
  1666. background-color: #409EFF;
  1667. border-color: #409EFF;
  1668. color: #FFFFFF;
  1669. }
  1670. /* 重置按钮 - 灰色扁平 */
  1671. .exp-apply-page .reset-btn {
  1672. background-color: #F5F7FA;
  1673. border-color: #D3D4D6;
  1674. color: #606266;
  1675. }
  1676. .exp-apply-page .reset-btn:hover {
  1677. background-color: #909399;
  1678. border-color: #909399;
  1679. color: #FFFFFF;
  1680. }
  1681. /* 新增按钮 - 绿色扁平 */
  1682. .exp-apply-page .add-btn {
  1683. background-color: #F0F9FF;
  1684. border-color: #C0E6C7;
  1685. color: #67C23A;
  1686. }
  1687. .exp-apply-page .add-btn:hover {
  1688. background-color: #67C23A;
  1689. border-color: #67C23A;
  1690. color: #FFFFFF;
  1691. }
  1692. /* 删除按钮 - 红色扁平 */
  1693. .exp-apply-page .delete-btn {
  1694. background-color: #FEF0F0;
  1695. border-color: #FAB6B6;
  1696. color: #F56C6C;
  1697. }
  1698. .exp-apply-page .delete-btn:hover:not(:disabled) {
  1699. background-color: #F56C6C;
  1700. border-color: #F56C6C;
  1701. color: #FFFFFF;
  1702. }
  1703. /* 撤回按钮 - 橙色扁平 */
  1704. .exp-apply-page .withdraw-btn {
  1705. background-color: #FDF6EC;
  1706. border-color: #F5DAB1;
  1707. color: #E6A23C;
  1708. }
  1709. .exp-apply-page .withdraw-btn:hover:not(:disabled) {
  1710. background-color: #E6A23C;
  1711. border-color: #E6A23C;
  1712. color: #FFFFFF;
  1713. }
  1714. /* 取消按钮 - 灰色扁平 */
  1715. .exp-apply-page .cancel-btn {
  1716. background-color: #F4F4F5;
  1717. border-color: #D3D4D6;
  1718. color: #909399;
  1719. }
  1720. .exp-apply-page .cancel-btn:hover:not(:disabled) {
  1721. background-color: #909399;
  1722. border-color: #909399;
  1723. color: #FFFFFF;
  1724. }
  1725. /* 复制按钮 - 蓝色扁平 */
  1726. .exp-apply-page .copy-btn {
  1727. background-color: #ECF5FF;
  1728. border-color: #B3D8FF;
  1729. color: #409EFF;
  1730. }
  1731. .exp-apply-page .copy-btn:hover:not(:disabled) {
  1732. background-color: #409EFF;
  1733. border-color: #409EFF;
  1734. color: #FFFFFF;
  1735. }
  1736. /* 催办按钮 - 橙黄色扁平 */
  1737. .exp-apply-page .urge-btn {
  1738. background-color: #FDF6EC;
  1739. border-color: #FAECD8;
  1740. color: #E6A23C;
  1741. }
  1742. .exp-apply-page .urge-btn:hover:not(:disabled) {
  1743. background-color: #E6A23C;
  1744. border-color: #E6A23C;
  1745. color: #FFFFFF;
  1746. }
  1747. /* 禁用按钮样式 */
  1748. .exp-apply-page .el-button:disabled {
  1749. opacity: 0.5;
  1750. cursor: not-allowed;
  1751. }
  1752. /* 数据表格样式 */
  1753. .exp-apply-page .data-table {
  1754. background-color: #FFFFFF;
  1755. border-radius: 4px;
  1756. }
  1757. .exp-apply-page .data-table >>> .el-table__header-wrapper th {
  1758. background-color: #F5F7FA !important;
  1759. color: #333333;
  1760. font-weight: 600;
  1761. font-size: 16px;
  1762. border-color: #EBEEF5;
  1763. padding: 10px 0;
  1764. height: auto;
  1765. }
  1766. /* 固定列表头也使用相同背景色 */
  1767. .exp-apply-page .data-table >>> .el-table__fixed-header-wrapper th {
  1768. background-color: #F5F7FA !important;
  1769. color: #333333;
  1770. font-weight: 600;
  1771. font-size: 16px;
  1772. border-color: #EBEEF5;
  1773. padding: 12px 0;
  1774. height: auto;
  1775. }
  1776. .exp-apply-page .data-table >>> .el-table__header-wrapper .cell {
  1777. text-align: center;
  1778. padding: 0 10px;
  1779. //line-height: 2;
  1780. overflow: hidden;
  1781. text-overflow: ellipsis;
  1782. white-space: nowrap;
  1783. font-size: 13px !important;
  1784. }
  1785. .exp-apply-page .data-table >>> .el-table__fixed-header-wrapper .cell {
  1786. text-align: center;
  1787. padding: 0 10px;
  1788. //line-height: 2;
  1789. overflow: hidden;
  1790. text-overflow: ellipsis;
  1791. white-space: nowrap;
  1792. }
  1793. .exp-apply-page .data-table >>> .el-table__body-wrapper td {
  1794. border-color: #EBEEF5;
  1795. padding: 12px 0;
  1796. font-size: 16px;
  1797. color: #606266;
  1798. height: auto;
  1799. }
  1800. /* 固定列数据行也使用相同样式 */
  1801. .exp-apply-page .data-table >>> .el-table__fixed-body-wrapper td {
  1802. border-color: #EBEEF5;
  1803. padding: 12px 0;
  1804. font-size: 16px;
  1805. color: #606266;
  1806. height: auto;
  1807. }
  1808. .exp-apply-page .data-table >>> .el-table__body-wrapper .cell {
  1809. padding: 0 10px;
  1810. //line-height: 2;
  1811. overflow: hidden;
  1812. text-overflow: ellipsis;
  1813. white-space: nowrap;
  1814. font-size: 13px !important;
  1815. }
  1816. .exp-apply-page .data-table >>> .el-table__fixed-body-wrapper .cell {
  1817. padding: 0 10px;
  1818. //line-height: 2;
  1819. overflow: hidden;
  1820. text-overflow: ellipsis;
  1821. white-space: nowrap;
  1822. }
  1823. /* 操作列链接样式 - 统一蓝色样式 */
  1824. .exp-apply-page .data-table >>> .el-table__body-wrapper a {
  1825. text-decoration: none;
  1826. cursor: pointer;
  1827. font-size: 13px;
  1828. color: #409EFF !important;
  1829. white-space: nowrap;
  1830. }
  1831. .exp-apply-page .data-table >>> .el-table__body-wrapper a:hover {
  1832. color: #66B1FF !important;
  1833. text-decoration: underline;
  1834. }
  1835. .exp-apply-page .data-table >>> .el-table__body tr:hover > td {
  1836. background-color: #F5F7FA !important;
  1837. }
  1838. .exp-apply-page .data-table >>> .el-table__body tr.current-row > td {
  1839. background-color: #ECF5FF !important;
  1840. }
  1841. /* Tab样式优化 - 两栏布局 */
  1842. /* 两栏布局容器 */
  1843. .two-column-layout {
  1844. display: flex;
  1845. gap: 10px;
  1846. height: 100%;
  1847. }
  1848. /* 统一的列头样式 */
  1849. .column-header {
  1850. display: flex;
  1851. align-items: center;
  1852. gap: 6px;
  1853. font-size: 13px;
  1854. font-weight: 600;
  1855. color: #606266;
  1856. padding: 8px 12px;
  1857. background: #f5f7fa;
  1858. border-bottom: 2px solid #409EFF;
  1859. }
  1860. .column-header i {
  1861. color: #409EFF;
  1862. font-size: 14px;
  1863. }
  1864. .progress-badge {
  1865. margin-left: auto;
  1866. font-size: 14px;
  1867. color: #409EFF;
  1868. font-weight: bold;
  1869. }
  1870. /* ==================== 左栏:审批流程 ==================== */
  1871. .stages-column {
  1872. flex: 0 0 380px;
  1873. background: white;
  1874. border: 1px solid #e4e7ed;
  1875. border-radius: 6px;
  1876. overflow: hidden;
  1877. display: flex;
  1878. flex-direction: column;
  1879. }
  1880. .stages-list {
  1881. padding: 8px;
  1882. overflow-y: auto;
  1883. flex: 1;
  1884. }
  1885. .stage-item {
  1886. display: flex;
  1887. align-items: flex-start;
  1888. gap: 8px;
  1889. padding: 8px;
  1890. margin-bottom: 6px;
  1891. border-radius: 6px;
  1892. background: #fafafa;
  1893. transition: all 0.2s;
  1894. }
  1895. .stage-item:last-child {
  1896. margin-bottom: 0;
  1897. }
  1898. .stage-item:hover {
  1899. background: #f0f2f5;
  1900. transform: translateX(2px);
  1901. }
  1902. .stage-icon {
  1903. width: 28px;
  1904. height: 28px;
  1905. border-radius: 50%;
  1906. display: flex;
  1907. align-items: center;
  1908. justify-content: center;
  1909. font-size: 14px;
  1910. flex-shrink: 0;
  1911. margin-top: 2px;
  1912. }
  1913. .stage-pending .stage-icon {
  1914. background: #e8e8e8;
  1915. color: #909399;
  1916. }
  1917. .stage-current .stage-icon {
  1918. background: #409EFF;
  1919. color: white;
  1920. animation: pulse-stage 2s infinite;
  1921. }
  1922. @keyframes pulse-stage {
  1923. 0%, 100% {
  1924. box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.3);
  1925. }
  1926. 50% {
  1927. box-shadow: 0 0 0 5px rgba(64, 158, 255, 0.1);
  1928. }
  1929. }
  1930. .stage-completed .stage-icon {
  1931. background: #67C23A;
  1932. color: white;
  1933. }
  1934. .stage-rejected .stage-icon {
  1935. background: #F56C6C;
  1936. color: white;
  1937. }
  1938. .stage-content {
  1939. flex: 1;
  1940. min-width: 0;
  1941. }
  1942. .stage-name {
  1943. font-size: 13px;
  1944. font-weight: 600;
  1945. color: #303133;
  1946. margin-bottom: 5px;
  1947. }
  1948. .stage-meta {
  1949. display: flex;
  1950. align-items: center;
  1951. gap: 6px;
  1952. }
  1953. .stage-time {
  1954. font-size: 11px;
  1955. color: #909399;
  1956. }
  1957. .stage-current {
  1958. background: #ecf5ff !important;
  1959. border: 1px solid #b3d8ff;
  1960. }
  1961. .stage-completed {
  1962. background: #f0f9ff !important;
  1963. }
  1964. .stage-rejected {
  1965. background: #fef0f0 !important;
  1966. border: 1px solid #fbc4c4;
  1967. }
  1968. /* ==================== 右栏:审批日志(表格) ==================== */
  1969. .logs-column {
  1970. flex: 1;
  1971. background: white;
  1972. border: 1px solid #e4e7ed;
  1973. border-radius: 6px;
  1974. overflow: hidden;
  1975. display: flex;
  1976. flex-direction: column;
  1977. }
  1978. .logs-column .column-header {
  1979. display: flex;
  1980. justify-content: space-between;
  1981. align-items: center;
  1982. flex-shrink: 0;
  1983. }
  1984. .logs-count {
  1985. font-size: 11px;
  1986. color: #909399;
  1987. font-weight: normal;
  1988. margin-left: auto;
  1989. }
  1990. .logs-table-wrapper {
  1991. flex: 1;
  1992. overflow: hidden;
  1993. }
  1994. /* ==================== 审批日志表格专用样式 ==================== */
  1995. /* 表头样式 - 深灰色背景,白色文字 */
  1996. .approval-logs-table >>> .el-table__header-wrapper th,
  1997. .approval-logs-table >>> .el-table__header-wrapper .el-table__cell {
  1998. background-color: #F5F7FA !important;
  1999. color: #606266 !important;
  2000. font-size: 12px;
  2001. font-weight: 600;
  2002. }
  2003. /* 数据行样式 - 更高的行高防止遮挡 */
  2004. .approval-logs-table >>> .el-table__body-wrapper .el-table__row {
  2005. height: 40px !important;
  2006. }
  2007. .approval-logs-table >>> .el-table__body-wrapper td,
  2008. .approval-logs-table >>> .el-table__body-wrapper .el-table__cell {
  2009. height: 40px !important;
  2010. padding: 0 !important;
  2011. }
  2012. /* 关键修复:cell容器要有足够的padding和overflow可见 */
  2013. .approval-logs-table >>> .el-table__body-wrapper .cell {
  2014. padding: 1px 12px !important;
  2015. line-height: 24px !important;
  2016. overflow: visible !important;
  2017. height: auto !important;
  2018. }
  2019. /* 标签样式 - 确保不被遮挡 */
  2020. .approval-logs-table >>> .el-tag {
  2021. vertical-align: middle !important;
  2022. display: inline-block !important;
  2023. margin: 2px 0 !important;
  2024. }
  2025. /* 悬停效果 */
  2026. .approval-logs-table >>> .el-table__body tr:hover > td {
  2027. background-color: #f5f7fa !important;
  2028. }
  2029. /* 空状态 */
  2030. .empty-tip {
  2031. display: flex;
  2032. flex-direction: column;
  2033. align-items: center;
  2034. justify-content: center;
  2035. padding: 50px 20px;
  2036. color: #909399;
  2037. }
  2038. .empty-tip p {
  2039. margin: 0;
  2040. }
  2041. /deep/ .manager-select .el-input__inner {
  2042. height: 30px !important;
  2043. line-height: 30px !important;
  2044. }
  2045. /deep/ .checkbox-vertical .el-checkbox {
  2046. display: block;
  2047. margin-left: 0;
  2048. margin-bottom: 8px;
  2049. }
  2050. </style>