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.

1533 lines
40 KiB

5 months ago
5 months ago
5 months ago
5 months ago
4 months ago
5 months ago
3 months ago
2 months ago
5 months ago
2 months ago
5 months ago
2 months ago
5 months ago
2 months ago
5 months ago
2 months ago
5 months ago
2 months ago
5 months ago
2 months ago
5 months ago
2 months ago
5 months ago
2 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
3 months ago
5 months ago
3 months ago
5 months ago
3 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
2 months ago
5 months ago
3 months ago
5 months ago
5 months ago
3 months ago
5 months ago
5 months ago
2 months ago
5 months ago
2 months ago
5 months ago
5 months ago
3 months ago
5 months ago
5 months ago
5 months ago
5 months ago
2 months ago
5 months ago
5 months ago
3 months ago
5 months ago
5 months ago
3 months ago
5 months ago
5 months ago
5 months ago
3 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
3 months ago
5 months ago
3 months ago
5 months ago
3 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
4 months ago
5 months ago
  1. <template>
  2. <div
  3. class="approval-container"
  4. v-loading="approvalLoading"
  5. element-loading-text="审批提交中..."
  6. element-loading-background="rgba(255, 255, 255, 0.7)">
  7. <!-- 页面标题和统计 -->
  8. <div class="page-header">
  9. <div class="header-left">
  10. <h2 class="page-title">
  11. <i class="el-icon-tickets"></i> 经理审批
  12. </h2>
  13. <p class="page-subtitle">试验申请审批管理</p>
  14. </div>
  15. <div class="header-right">
  16. <div class="stat-cards">
  17. <div class="stat-card stat-total">
  18. <div class="stat-icon"><i class="el-icon-document"></i></div>
  19. <div class="stat-content">
  20. <div class="stat-value">{{ totalPage }}</div>
  21. <div class="stat-label">待处理总数</div>
  22. </div>
  23. </div>
  24. <div class="stat-card stat-high-risk">
  25. <div class="stat-icon"><i class="el-icon-warning"></i></div>
  26. <div class="stat-content">
  27. <div class="stat-value">{{ highRiskCount }}</div>
  28. <div class="stat-label">高风险试验</div>
  29. </div>
  30. </div>
  31. <div class="stat-card stat-low-risk">
  32. <div class="stat-icon"><i class="el-icon-success"></i></div>
  33. <div class="stat-content">
  34. <div class="stat-value">{{ lowRiskCount }}</div>
  35. <div class="stat-label">低风险试验</div>
  36. </div>
  37. </div>
  38. </div>
  39. </div>
  40. </div>
  41. <!-- 查询条件表单 -->
  42. <div class="search-section">
  43. <el-collapse class="no-arrow" v-model="searchExpanded">
  44. <el-collapse-item name="1">
  45. <template slot="title">
  46. <i class="el-icon-search"></i>
  47. <span style="margin-left: 8px; font-weight: 500;">筛选条件</span>
  48. </template>
  49. <el-form :inline="true" label-position="top" class="search-form">
  50. <el-form-item label="试验单号">
  51. <el-input v-model="searchData.applyNo" placeholder="支持模糊查询" clearable style="width: 150px"></el-input>
  52. </el-form-item>
  53. <el-form-item label="事业部">
  54. <el-select v-model="searchData.buNo" placeholder="请选择" clearable style="width: 120px">
  55. <el-option label="全部" value=""></el-option>
  56. <el-option
  57. v-for="i in buList"
  58. :key="i.buNo"
  59. :label="i.buDesc"
  60. :value="i.buNo">
  61. </el-option>
  62. </el-select>
  63. </el-form-item>
  64. <el-form-item label="试验类型">
  65. <el-select v-model="searchData.experimentType" placeholder="请选择" clearable style="width: 120px">
  66. <el-option label="全部" value=""></el-option>
  67. <el-option label="High Risk" value="High Risk"></el-option>
  68. <el-option label="Low Risk" value="Low Risk"></el-option>
  69. </el-select>
  70. </el-form-item>
  71. <el-form-item label="试验名称">
  72. <el-input v-model="searchData.title" placeholder="支持模糊查询" clearable style="width: 150px"></el-input>
  73. </el-form-item>
  74. <el-form-item label="项目编号">
  75. <el-input v-model="searchData.projectNo" placeholder="支持模糊查询" clearable style="width: 150px"></el-input>
  76. </el-form-item>
  77. <el-form-item label="产品型号">
  78. <el-input v-model="searchData.productType" placeholder="支持模糊查询" clearable style="width: 150px"></el-input>
  79. </el-form-item>
  80. <el-form-item label="试验负责人">
  81. <el-input v-model="searchData.projectLeader" placeholder="支持模糊查询" clearable style="width: 120px"></el-input>
  82. </el-form-item>
  83. <el-form-item label="申请开始日期">
  84. <el-date-picker
  85. v-model="searchData.createStartDate"
  86. type="date"
  87. placeholder="选择日期"
  88. value-format="yyyy-MM-dd"
  89. style="width: 150px">
  90. </el-date-picker>
  91. </el-form-item>
  92. <el-form-item label="申请结束日期">
  93. <el-date-picker
  94. v-model="searchData.createEndDate"
  95. type="date"
  96. placeholder="选择日期"
  97. value-format="yyyy-MM-dd"
  98. style="width: 150px">
  99. </el-date-picker>
  100. </el-form-item>
  101. <el-form-item label=" ">
  102. <el-button @click="getDataList('Y')" type="primary" icon="el-icon-search">查询</el-button>
  103. <el-button @click="resetQuery()" type="default" icon="el-icon-refresh-left">重置</el-button>
  104. </el-form-item>
  105. </el-form>
  106. </el-collapse-item>
  107. </el-collapse>
  108. </div>
  109. <!-- 卡片列表 -->
  110. <div class="cards-container" v-loading="dataListLoading">
  111. <transition-group name="card-list" tag="div" class="cards-grid">
  112. <div
  113. v-for="item in dataList"
  114. :key="item.applyNo"
  115. class="approval-card"
  116. :class="{'high-risk': item.experimentType === 'High Risk'}"
  117. @click="openApprovalDialog(item)">
  118. <!-- 卡片头部 -->
  119. <div class="card-header">
  120. <div class="card-title-area">
  121. <el-tag
  122. :type="item.experimentType === 'High Risk' ? 'danger' : 'success'"
  123. size="middle"
  124. effect="dark"
  125. class="risk-tag">
  126. <i :class="item.experimentType === 'High Risk' ? 'el-icon-warning' : 'el-icon-success'"></i>
  127. {{ item.experimentType }}
  128. </el-tag>
  129. <span class="apply-no">{{ item.applyNo }}</span>
  130. </div>
  131. <el-badge
  132. :value="item.attachmentCount || 0"
  133. :hidden="!item.attachmentCount"
  134. class="attachment-badge">
  135. <el-button
  136. type="text"
  137. icon="el-icon-paperclip"
  138. class="attachment-btn"
  139. @click.stop="openAttachmentDialog(item)">
  140. 附件
  141. </el-button>
  142. </el-badge>
  143. </div>
  144. <!-- 卡片主体内容 -->
  145. <div class="card-body">
  146. <h3 class="experiment-title">
  147. <i class="el-icon-document"></i>
  148. {{ item.title }}
  149. </h3>
  150. <div class="card-details">
  151. <div class="detail-row">
  152. <span class="detail-label">
  153. <i class="el-icon-box"></i>
  154. 项目编号
  155. </span>
  156. <span class="detail-value">{{ item.projectNo || '-' }}</span>
  157. </div>
  158. <div class="detail-row">
  159. <span class="detail-label">
  160. <i class="el-icon-goods"></i>
  161. 产品型号
  162. </span>
  163. <span class="detail-value" :title="item.productType">{{ item.productType || '-' }}</span>
  164. </div>
  165. <div class="detail-row">
  166. <span class="detail-label">
  167. <i class="el-icon-user"></i>
  168. 试验负责人
  169. </span>
  170. <span class="detail-value">{{ item.projectLeader }}</span>
  171. </div>
  172. <div class="detail-row">
  173. <span class="detail-label">
  174. <i class="el-icon-time"></i>
  175. 申请时间
  176. </span>
  177. <span class="detail-value">{{ item.createTime }}</span>
  178. </div>
  179. </div>
  180. </div>
  181. <!-- 卡片底部 -->
  182. <div class="card-footer">
  183. <div class="current-step">
  184. <i class="el-icon-position"></i>
  185. {{ item.currentStep }}
  186. </div>
  187. <div class="action-buttons">
  188. <el-button
  189. type="success"
  190. size="small"
  191. plain
  192. icon="el-icon-check"
  193. class="approve-btn"
  194. @click.stop="quickApprove(item)">
  195. 通过
  196. </el-button>
  197. <el-button
  198. type="danger"
  199. size="small"
  200. plain
  201. icon="el-icon-close"
  202. class="reject-btn"
  203. @click.stop="openRejectDialog(item)">
  204. 驳回
  205. </el-button>
  206. </div>
  207. </div>
  208. </div>
  209. </transition-group>
  210. <!-- 空状态 -->
  211. <div v-if="!dataListLoading && dataList.length === 0" class="empty-state">
  212. <i class="el-icon-document-checked empty-icon"></i>
  213. <p class="empty-text">暂无待办事项</p>
  214. <p class="empty-subtext">您已完成所有审批任务</p>
  215. </div>
  216. </div>
  217. <!-- 分页组件 -->
  218. <div class="pagination-wrapper" v-if="dataList.length > 0">
  219. <el-pagination
  220. @size-change="sizeChangeHandle"
  221. @current-change="currentChangeHandle"
  222. :current-page="pageIndex"
  223. :page-sizes="[20, 50, 100]"
  224. :page-size="pageSize"
  225. :total="totalPage"
  226. layout="total, sizes, prev, pager, next, jumper"
  227. background>
  228. </el-pagination>
  229. </div>
  230. <!-- 审批弹窗 -->
  231. <el-dialog
  232. title="审批试验单"
  233. :visible.sync="approvalDialogVisible"
  234. width="1100px"
  235. :close-on-click-modal="false"
  236. v-drag>
  237. <!-- Tabs切换 -->
  238. <el-tabs v-model="activeTab" type="border-card" style="font-size: 12px; min-height: 200px">
  239. <!-- 试验单基本信息 -->
  240. <el-tab-pane label="试验单信息" name="basic">
  241. <el-descriptions :column="2" border size="small">
  242. <el-descriptions-item label="试验单号">{{ currentApply.applyNo }}</el-descriptions-item>
  243. <el-descriptions-item label="事业部">{{ currentApply.buNo }}</el-descriptions-item>
  244. <el-descriptions-item label="试验类型">
  245. <el-tag :type="currentApply.experimentType === 'High Risk' ? 'danger' : 'success'">
  246. {{ currentApply.experimentType }}
  247. </el-tag>
  248. </el-descriptions-item>
  249. <el-descriptions-item label="项目编号">{{ currentApply.projectNo }}</el-descriptions-item>
  250. <el-descriptions-item label="试验名称" :span="2">{{ currentApply.title }}</el-descriptions-item>
  251. <el-descriptions-item label="试验目的" :span="2">{{ currentApply.purpose }}</el-descriptions-item>
  252. <el-descriptions-item label="验证方法" :span="2">{{ currentApply.justification }}</el-descriptions-item>
  253. <el-descriptions-item label="产品型号">{{ currentApply.productType }}</el-descriptions-item>
  254. <el-descriptions-item label="申请数量">{{ currentApply.quantityReq }}</el-descriptions-item>
  255. <el-descriptions-item label="期望完成日期">{{ currentApply.expectedFinishDate }}</el-descriptions-item>
  256. <el-descriptions-item label="试验负责人">{{ currentApply.projectLeader }}</el-descriptions-item>
  257. <el-descriptions-item label="申请时间" :span="2">{{ currentApply.createTime }}</el-descriptions-item>
  258. </el-descriptions>
  259. </el-tab-pane>
  260. <!-- 审批历史 -->
  261. <el-tab-pane label="审批历史" name="history">
  262. <approval-history
  263. v-if="activeTab === 'history' && currentApply.applyNo"
  264. :apply-no="currentApply.applyNo">
  265. </approval-history>
  266. </el-tab-pane>
  267. </el-tabs>
  268. <!-- 审批操作 -->
  269. <el-divider content-position="left">审批操作</el-divider>
  270. <el-form :model="approvalData" label-position="top" style="margin-left: 5px; margin-top: -5px;">
  271. <el-row :gutter="20">
  272. <el-col :span="24">
  273. <el-form-item label="审批意见" required>
  274. <el-input
  275. v-model="approvalData.comment"
  276. type="textarea"
  277. :rows="2"
  278. placeholder="请输入审批意见">
  279. </el-input>
  280. </el-form-item>
  281. </el-col>
  282. </el-row>
  283. </el-form>
  284. <el-footer style="height: 40px; margin-top: 40px; text-align: center">
  285. <el-button type="success" @click="approveApply" :loading="approvalLoading">
  286. {{ approvalLoading ? '审批中...' : '批准' }}
  287. </el-button>
  288. <el-button type="danger" @click="rejectApply" :loading="approvalLoading">
  289. {{ approvalLoading ? '驳回中...' : '驳回' }}
  290. </el-button>
  291. <el-button type="primary" @click="approvalDialogVisible = false" :disabled="approvalLoading">关闭</el-button>
  292. </el-footer>
  293. </el-dialog>
  294. <!-- 驳回原因弹窗 -->
  295. <el-dialog
  296. title="驳回申请"
  297. :visible.sync="rejectDialogVisible"
  298. width="500px"
  299. :close-on-click-modal="false">
  300. <el-form label-position="top">
  301. <el-form-item label="驳回原因" required>
  302. <el-input
  303. v-model="rejectReason"
  304. type="textarea"
  305. :rows="3"
  306. placeholder="请输入驳回原因"
  307. maxlength="500"
  308. show-word-limit>
  309. </el-input>
  310. </el-form-item>
  311. </el-form>
  312. <div slot="footer" class="dialog-footer" style="height: 40px; margin-top: 50px; text-align: center">
  313. <el-button @click="rejectDialogVisible = false">取消</el-button>
  314. <el-button type="danger" @click="confirmReject" :loading="approvalLoading">
  315. {{ approvalLoading ? '驳回中...' : '确认驳回' }}
  316. </el-button>
  317. </div>
  318. </el-dialog>
  319. <!-- 附件查看弹窗 -->
  320. <el-dialog
  321. :title="`附件列表 - ${currentAttachmentApplyNo}`"
  322. :visible.sync="attachmentDialogVisible"
  323. width="900px"
  324. :close-on-click-modal="false"
  325. append-to-body>
  326. <div v-loading="attachmentLoading">
  327. <el-table
  328. :data="attachmentList"
  329. border class="approval-logs-table"
  330. stripe
  331. style="width: 100%"
  332. max-height="500px">
  333. <el-table-column
  334. type="index"
  335. label="序号"
  336. width="60"
  337. align="center">
  338. </el-table-column>
  339. <el-table-column
  340. prop="fileName"
  341. label="文件名"
  342. min-width="200"
  343. align="left"
  344. header-align="center"
  345. show-overflow-tooltip>
  346. <template slot-scope="scope">
  347. <i :class="getFileIcon(scope.row.fileType)" style="margin-right: 5px;"></i>
  348. {{ scope.row.fileName }}
  349. </template>
  350. </el-table-column>
  351. <el-table-column
  352. prop="fileType"
  353. label="文件类型"
  354. width="100"
  355. align="center"
  356. header-align="center">
  357. <template slot-scope="scope">
  358. <el-tag size="small" type="info">{{ scope.row.fileType }}</el-tag>
  359. </template>
  360. </el-table-column>
  361. <el-table-column
  362. prop="createdBy"
  363. label="上传人"
  364. width="100"
  365. align="center"
  366. header-align="center">
  367. </el-table-column>
  368. <el-table-column
  369. prop="createDate"
  370. label="上传时间"
  371. width="160"
  372. align="center"
  373. header-align="center">
  374. </el-table-column>
  375. <el-table-column
  376. label="操作"
  377. width="150"
  378. align="center"
  379. header-align="center">
  380. <template slot-scope="scope">
  381. <a
  382. type="text"
  383. size="small"
  384. icon="el-icon-view"
  385. @click="previewAttachment(scope.row)">
  386. 预览
  387. </a>
  388. <a
  389. type="text"
  390. size="small"
  391. icon="el-icon-download"
  392. @click="downloadAttachment(scope.row)">
  393. 下载
  394. </a>
  395. </template>
  396. </el-table-column>
  397. </el-table>
  398. <!-- 空状态 -->
  399. <div v-if="!attachmentLoading && attachmentList.length === 0" class="empty-attachment">
  400. <i class="el-icon-folder-opened" style="font-size: 48px; color: #c0c4cc;"></i>
  401. <p style="margin-top: 10px; color: #909399;">暂无附件</p>
  402. </div>
  403. </div>
  404. <div slot="footer" class="dialog-footer">
  405. <el-button type="primary" @click="attachmentDialogVisible = false">关闭</el-button>
  406. </div>
  407. </el-dialog>
  408. </div>
  409. </template>
  410. <script>
  411. import { getPendingApplyList, approveExpApply, getCurrentNodeCode } from '@/api/erf/erf'
  412. import { getBuList } from '@/api/factory/site'
  413. import { queryOss, previewOssFileById, previewOssFileById2 } from '@/api/oss/oss'
  414. import ApprovalHistory from './components/approvalHistory.vue'
  415. export default {
  416. name: 'ExpApplyApproval',
  417. components: {
  418. ApprovalHistory
  419. },
  420. data() {
  421. return {
  422. buList: [],
  423. activeTab: 'basic',
  424. searchExpanded: ['0'], // 搜索条件默认展开
  425. // 查询条件
  426. searchData: {
  427. applyNo: '',
  428. buNo: '',
  429. experimentType: '',
  430. title: '',
  431. projectNo: '',
  432. productType: '',
  433. creatorName: '',
  434. createStartDate: '',
  435. createEndDate: '',
  436. currentUserId: this.$store.state.user.id,
  437. pendingStatus: '已下达', // 审批节点只查询已下达状态
  438. page: 1,
  439. limit: 20
  440. },
  441. // 数据列表
  442. dataList: [],
  443. // 分页参数
  444. pageIndex: 1,
  445. pageSize: 20,
  446. totalPage: 0,
  447. dataListLoading: false,
  448. // 审批弹窗
  449. approvalDialogVisible: false,
  450. currentApply: {},
  451. approvalData: {
  452. applyNo: '',
  453. nodeCode: '',
  454. action: '',
  455. comment: '',
  456. operatorUserId: this.$store.state.user.id,
  457. operatorName: this.$store.state.user.userDisplay
  458. },
  459. approvalLoading: false,
  460. // 驳回弹窗
  461. rejectDialogVisible: false,
  462. rejectReason: '',
  463. rejectApplyData: {}, // 暂存待驳回的试验单信息
  464. // 附件查看弹窗
  465. attachmentDialogVisible: false,
  466. attachmentLoading: false,
  467. attachmentList: [],
  468. currentAttachmentApplyNo: ''
  469. }
  470. },
  471. computed: {
  472. /**
  473. * 计算高风险试验数量
  474. */
  475. highRiskCount() {
  476. return this.dataList.filter(item => item.experimentType === 'High Risk').length
  477. },
  478. /**
  479. * 计算低风险试验数量
  480. */
  481. lowRiskCount() {
  482. return this.dataList.filter(item => item.experimentType === 'Low Risk').length
  483. }
  484. },
  485. activated() {
  486. this.loadBuList()
  487. this.getDataList()
  488. },
  489. methods: {
  490. /**
  491. * 加载事业部列表
  492. */
  493. loadBuList() {
  494. const tempData = { site: this.$store.state.user.site }
  495. getBuList(tempData).then(({data}) => {
  496. if (data.code === 0) {
  497. this.buList = data.row1
  498. }
  499. })
  500. },
  501. /**
  502. * 获取待办列表
  503. */
  504. getDataList(flag) {
  505. if (flag === 'Y') {
  506. this.pageIndex = 1
  507. }
  508. this.searchData.page = this.pageIndex
  509. this.searchData.limit = this.pageSize
  510. this.searchData.currentUserId = this.$store.state.user.id
  511. this.searchData.pageType = 'MANAGER' // 经理审批页面,只查询经理审批节点
  512. this.dataListLoading = true
  513. getPendingApplyList(this.searchData).then(({data}) => {
  514. this.dataListLoading = false
  515. if (data && data.code === 0) {
  516. this.dataList = data.page.list || []
  517. this.totalPage = data.page.totalCount || 0
  518. // 为每个试验单查询附件数量
  519. this.loadAttachmentCounts()
  520. } else {
  521. this.dataList = []
  522. this.totalPage = 0
  523. this.$message.error(data.msg || '查询失败')
  524. }
  525. }).catch(error => {
  526. this.dataListLoading = false
  527. this.$message.error('查询异常')
  528. })
  529. },
  530. /**
  531. * 批量加载附件数量
  532. */
  533. loadAttachmentCounts() {
  534. this.dataList.forEach(item => {
  535. const params = {
  536. orderRef1: 'ERF',
  537. orderRef2: item.applyNo,
  538. orderRef6: 'EXP_APPLY'
  539. }
  540. queryOss(params).then(({data}) => {
  541. if (data && data.code === 0) {
  542. this.$set(item, 'attachmentCount', data.rows ? data.rows.length : 0)
  543. }
  544. }).catch(() => {
  545. this.$set(item, 'attachmentCount', 0)
  546. })
  547. })
  548. },
  549. /**
  550. * 重置查询条件
  551. */
  552. resetQuery() {
  553. this.searchData.applyNo = ''
  554. this.searchData.buNo = ''
  555. this.searchData.experimentType = ''
  556. this.searchData.title = ''
  557. this.searchData.projectNo = ''
  558. this.searchData.productType = ''
  559. this.searchData.creatorName = ''
  560. this.searchData.createStartDate = ''
  561. this.searchData.createEndDate = ''
  562. this.getDataList('Y')
  563. },
  564. /**
  565. * 打开审批对话框
  566. */
  567. openApprovalDialog(row) {
  568. this.currentApply = { ...row }
  569. // 查询当前流程实例,获取正确的节点编码
  570. this.loadCurrentNodeCode(row.applyNo)
  571. },
  572. /**
  573. * 快速通过 - 不需要输入原因
  574. */
  575. quickApprove(row) {
  576. this.$confirm('确认通过该试验单?', '操作提示', {
  577. confirmButtonText: '确定通过',
  578. cancelButtonText: '取消',
  579. type: 'success'
  580. }).then(() => {
  581. this.approvalLoading = true
  582. // 获取当前节点编码并执行通过操作
  583. getCurrentNodeCode({ applyNo: row.applyNo }).then(({data}) => {
  584. if (data && data.code === 0) {
  585. const approvalData = {
  586. applyNo: row.applyNo,
  587. nodeCode: data.nodeCode,
  588. action: '批准',
  589. comment: '批准通过', // 默认审批意见
  590. operatorUserId: this.$store.state.user.id,
  591. operatorName: this.$store.state.user.userDisplay
  592. }
  593. approveExpApply(approvalData).then(({data}) => {
  594. this.approvalLoading = false
  595. if (data && data.code === 0) {
  596. this.$message.success('审批成功')
  597. this.getDataList()
  598. } else {
  599. this.$message.error(data.msg || '审批失败')
  600. }
  601. }).catch(error => {
  602. this.approvalLoading = false
  603. this.$message.error('审批异常')
  604. })
  605. } else {
  606. this.approvalLoading = false
  607. this.$message.error('获取节点信息失败')
  608. }
  609. }).catch(error => {
  610. this.approvalLoading = false
  611. this.$message.error('获取节点信息异常')
  612. })
  613. }).catch(() => {
  614. // 取消操作
  615. })
  616. },
  617. /**
  618. * 打开驳回弹框
  619. */
  620. openRejectDialog(row) {
  621. this.rejectApplyData = { ...row }
  622. this.rejectReason = ''
  623. this.rejectDialogVisible = true
  624. },
  625. /**
  626. * 确认驳回
  627. */
  628. confirmReject() {
  629. if (!this.rejectReason || this.rejectReason.trim() === '') {
  630. this.$message.warning('请输入驳回原因')
  631. return
  632. }
  633. // 获取当前节点编码并执行驳回操作
  634. getCurrentNodeCode({ applyNo: this.rejectApplyData.applyNo }).then(({data}) => {
  635. if (data && data.code === 0) {
  636. const approvalData = {
  637. applyNo: this.rejectApplyData.applyNo,
  638. nodeCode: data.nodeCode,
  639. action: '驳回',
  640. comment: this.rejectReason,
  641. operatorUserId: this.$store.state.user.id,
  642. operatorName: this.$store.state.user.userDisplay
  643. }
  644. this.approvalLoading = true
  645. approveExpApply(approvalData).then(({data}) => {
  646. this.approvalLoading = false
  647. if (data && data.code === 0) {
  648. this.$message.success('驳回成功')
  649. this.rejectDialogVisible = false
  650. this.getDataList()
  651. } else {
  652. this.$message.error(data.msg || '驳回失败')
  653. }
  654. }).catch(error => {
  655. this.approvalLoading = false
  656. this.$message.error('驳回异常')
  657. })
  658. } else {
  659. this.$message.error('获取节点信息失败')
  660. }
  661. }).catch(error => {
  662. this.$message.error('获取节点信息异常')
  663. })
  664. },
  665. /**
  666. * 加载当前节点编码并打开审批弹窗
  667. */
  668. loadCurrentNodeCode(applyNo) {
  669. // 调用API获取流程实例的当前节点
  670. getCurrentNodeCode({ applyNo: applyNo }).then(({data}) => {
  671. if (data && data.code === 0) {
  672. this.approvalData = {
  673. applyNo: applyNo,
  674. nodeCode: data.nodeCode, // 使用流程实例的当前节点编码
  675. action: '',
  676. comment: '',
  677. operatorUserId: this.$store.state.user.id,
  678. operatorName: this.$store.state.user.userDisplay
  679. }
  680. this.approvalDialogVisible = true
  681. } else {
  682. this.$message.error('获取节点信息失败')
  683. }
  684. }).catch(error => {
  685. this.$message.error('获取节点信息异常')
  686. })
  687. },
  688. /**
  689. * 批准操作
  690. */
  691. approveApply() {
  692. this.approvalData.action = '批准'
  693. this.doApproval()
  694. },
  695. /**
  696. * 驳回操作
  697. */
  698. rejectApply() {
  699. if (!this.approvalData.comment) {
  700. this.$message.warning('请输入审批意见')
  701. return
  702. }
  703. this.approvalData.action = '驳回'
  704. this.doApproval()
  705. },
  706. /**
  707. * 执行审批
  708. */
  709. doApproval() {
  710. this.approvalLoading = true
  711. // 如果审批通过没有输入审批意见,默认设置为“批准通过”
  712. if (this.approvalData.action === '批准' && (!this.approvalData.comment || this.approvalData.comment.trim() === '')) {
  713. this.approvalData.comment = '批准通过'
  714. }
  715. approveExpApply(this.approvalData).then(({data}) => {
  716. this.approvalLoading = false
  717. if (data && data.code === 0) {
  718. this.$message.success('审批成功')
  719. this.approvalDialogVisible = false
  720. this.getDataList()
  721. } else {
  722. this.$message.error(data.msg || '审批失败')
  723. }
  724. }).catch(error => {
  725. this.approvalLoading = false
  726. this.$message.error('审批异常')
  727. })
  728. },
  729. /**
  730. * 分页大小改变
  731. */
  732. sizeChangeHandle(val) {
  733. this.pageSize = val
  734. this.pageIndex = 1
  735. this.getDataList()
  736. },
  737. /**
  738. * 当前页改变
  739. */
  740. currentChangeHandle(val) {
  741. this.pageIndex = val
  742. this.getDataList()
  743. },
  744. /**
  745. * 打开附件查看弹窗智能判断单个附件直接预览多个附件打开列表
  746. */
  747. openAttachmentDialog(row) {
  748. this.currentAttachmentApplyNo = row.applyNo
  749. // 先查询附件列表
  750. const params = {
  751. orderRef1: 'ERF',
  752. orderRef2: row.applyNo,
  753. orderRef6: 'EXP_APPLY'
  754. }
  755. this.attachmentLoading = true
  756. queryOss(params).then(({data}) => {
  757. this.attachmentLoading = false
  758. if (data && data.code === 0) {
  759. const attachmentList = data.rows || []
  760. if (attachmentList.length === 0) {
  761. // 没有附件
  762. this.$message.warning('该试验单暂无附件')
  763. } else if (attachmentList.length === 1) {
  764. // 只有一个附件,直接预览
  765. this.previewAttachment(attachmentList[0])
  766. } else {
  767. // 多个附件,打开列表弹窗
  768. this.attachmentList = attachmentList
  769. this.attachmentDialogVisible = true
  770. }
  771. } else {
  772. this.$message.warning(data.msg || '查询附件失败')
  773. }
  774. })
  775. },
  776. /**
  777. * 加载附件列表用于弹窗内刷新
  778. */
  779. loadAttachments(applyNo) {
  780. const params = {
  781. orderRef1: 'ERF',
  782. orderRef2: applyNo,
  783. orderRef6: 'EXP_APPLY'
  784. }
  785. this.attachmentLoading = true
  786. queryOss(params).then(({data}) => {
  787. this.attachmentLoading = false
  788. if (data && data.code === 0) {
  789. this.attachmentList = data.rows || []
  790. } else {
  791. this.attachmentList = []
  792. this.$message.warning(data.msg || '查询附件失败')
  793. }
  794. }).catch(error => {
  795. this.attachmentLoading = false
  796. this.$message.error('查询附件异常')
  797. })
  798. },
  799. /**
  800. * 获取文件图标
  801. */
  802. getFileIcon(fileType) {
  803. const type = fileType.toLowerCase()
  804. // 图片类型
  805. if (['jpg', 'jpeg', 'png', 'gif', 'bmp'].includes(type)) {
  806. return 'el-icon-picture-outline'
  807. }
  808. // PDF类型
  809. if (type === 'pdf') {
  810. return 'el-icon-document'
  811. }
  812. // Word类型
  813. if (['doc', 'docx'].includes(type)) {
  814. return 'el-icon-document'
  815. }
  816. // Excel类型
  817. if (['xls', 'xlsx'].includes(type)) {
  818. return 'el-icon-tickets'
  819. }
  820. // CAD类型
  821. if (['dwg', 'dxf'].includes(type)) {
  822. return 'el-icon-edit-outline'
  823. }
  824. // 默认文件图标
  825. return 'el-icon-document'
  826. },
  827. /**
  828. * 预览附件
  829. */
  830. previewAttachment(row) {
  831. let type = ''
  832. let fileType = row.fileType.toLowerCase()
  833. // 图片类型
  834. let image = ['jpg', 'jpeg', 'png', 'gif', 'bmp']
  835. if (image.includes(fileType)) {
  836. type = 'image/' + fileType
  837. }
  838. // PDF类型
  839. if (fileType === 'pdf') {
  840. type = 'application/pdf;charset-UTF-8'
  841. }
  842. // Excel类型
  843. if (fileType === 'xlsx' || fileType === 'xls') {
  844. type = 'excel'
  845. }
  846. // Word类型(doc 和 docx 均转 PDF 预览)
  847. if (fileType === 'docx' || fileType === 'doc') {
  848. type = 'word'
  849. }
  850. // Office文件不支持预览
  851. if (fileType === 'ppt' || fileType === 'pptx') {
  852. this.$message.warning('该文件格式暂不支持预览,请下载后查看')
  853. return
  854. }
  855. // CAD文件不支持预览
  856. if (fileType === 'dwg' || fileType === 'dxf') {
  857. this.$message.warning('CAD文件暂不支持预览,请下载后使用CAD软件查看')
  858. return
  859. }
  860. if (type === '') {
  861. this.$message.warning('该文件格式暂不支持预览')
  862. return
  863. }
  864. let params = {
  865. id: row.id,
  866. fileType: type
  867. }
  868. previewOssFileById2(params).then(({data}) => {
  869. if (type === 'excel' || type === 'word') {
  870. type = 'application/pdf;charset-UTF-8'
  871. }
  872. const blob = new Blob([data], { type: type })
  873. const fileURL = URL.createObjectURL(blob)
  874. // 在新标签页中打开文件预览
  875. window.open(fileURL, '_blank')
  876. }).catch(error => {
  877. this.$message.error('预览失败')
  878. })
  879. },
  880. /**
  881. * 下载附件
  882. */
  883. downloadAttachment(row) {
  884. let params = {
  885. id: row.id
  886. }
  887. previewOssFileById(params).then((response) => {
  888. const blob = new Blob([response.data], { type: response.headers['content-type'] })
  889. const link = document.createElement('a')
  890. link.href = URL.createObjectURL(blob)
  891. link.setAttribute('download', row.fileName)
  892. link.target = '_blank'
  893. link.click()
  894. URL.revokeObjectURL(link.href)
  895. this.$message.success('下载成功')
  896. }).catch(error => {
  897. this.$message.error('下载失败')
  898. })
  899. }
  900. }
  901. }
  902. </script>
  903. <style scoped>
  904. /* 整体容器 */
  905. .approval-container {
  906. padding: 15px;
  907. background: #f5f7fa;
  908. min-height: calc(100vh - 80px);
  909. }
  910. /* ===== 页面头部 ===== */
  911. .page-header {
  912. display: flex;
  913. justify-content: space-between;
  914. align-items: center;
  915. margin-bottom: 15px;
  916. padding: 5px 20px;
  917. background: #FFFFFF;
  918. border-radius: 4px;
  919. border: 1px solid #EBEEF5;
  920. }
  921. .header-left {
  922. color: #303133;
  923. }
  924. .page-title {
  925. margin: 0;
  926. font-size: 20px;
  927. font-weight: 600;
  928. color: #303133;
  929. display: flex;
  930. align-items: center;
  931. gap: 10px;
  932. }
  933. .page-title i {
  934. font-size: 22px;
  935. color: #409EFF;
  936. }
  937. .page-subtitle {
  938. margin: 6px 0 0 0;
  939. font-size: 13px;
  940. color: #909399;
  941. }
  942. .header-right {
  943. display: flex;
  944. gap: 12px;
  945. }
  946. /* ===== 统计卡片 ===== */
  947. .stat-cards {
  948. display: flex;
  949. gap: 12px;
  950. }
  951. .stat-card {
  952. display: flex;
  953. align-items: center;
  954. gap: 12px;
  955. padding: 12px 20px;
  956. background: #FFFFFF;
  957. border-radius: 4px;
  958. border: 1px solid #EBEEF5;
  959. transition: all 0.3s ease;
  960. cursor: pointer;
  961. min-width: 140px;
  962. }
  963. .stat-card:hover {
  964. border-color: #409EFF;
  965. box-shadow: 0 2px 8px rgba(64, 158, 255, 0.15);
  966. }
  967. .stat-icon {
  968. width: 40px;
  969. height: 40px;
  970. display: flex;
  971. align-items: center;
  972. justify-content: center;
  973. border-radius: 4px;
  974. font-size: 20px;
  975. color: white;
  976. }
  977. .stat-total .stat-icon {
  978. background: #409EFF;
  979. }
  980. .stat-high-risk .stat-icon {
  981. background: #F56C6C;
  982. }
  983. .stat-low-risk .stat-icon {
  984. background: #67C23A;
  985. }
  986. .stat-content {
  987. display: flex;
  988. flex-direction: column;
  989. }
  990. .stat-value {
  991. font-size: 24px;
  992. font-weight: 600;
  993. color: #303133;
  994. line-height: 1;
  995. }
  996. .stat-label {
  997. font-size: 12px;
  998. color: #909399;
  999. margin-top: 4px;
  1000. }
  1001. /* ===== 搜索区域 ===== */
  1002. .search-section {
  1003. margin-bottom: 15px;
  1004. background: white;
  1005. border-radius: 4px;
  1006. border: 1px solid #EBEEF5;
  1007. overflow: hidden;
  1008. }
  1009. .search-section >>> .el-collapse-item__header {
  1010. padding: 0 15px;
  1011. height: 45px;
  1012. line-height: 45px;
  1013. background: white;
  1014. border-bottom: 1px solid #ebeef5;
  1015. font-size: 13px;
  1016. color: #303133;
  1017. font-weight: 500;
  1018. }
  1019. .search-section >>> .el-collapse-item__content {
  1020. padding: 15px;
  1021. background: #FFFFFF;
  1022. }
  1023. .search-form {
  1024. margin: 0;
  1025. }
  1026. .search-form >>> .el-form-item {
  1027. margin-bottom: 10px;
  1028. }
  1029. /* ===== 卡片容器 ===== */
  1030. .cards-container {
  1031. min-height: 400px;
  1032. position: relative;
  1033. }
  1034. .cards-grid {
  1035. display: grid;
  1036. grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
  1037. gap: 15px;
  1038. margin-bottom: 15px;
  1039. }
  1040. /* ===== 审批卡片 ===== */
  1041. .approval-card {
  1042. background: white;
  1043. border-radius: 4px;
  1044. padding: 18px;
  1045. border: 1px solid #EBEEF5;
  1046. transition: all 0.3s ease;
  1047. cursor: pointer;
  1048. position: relative;
  1049. }
  1050. .approval-card::before {
  1051. content: '';
  1052. position: absolute;
  1053. top: 0;
  1054. left: 0;
  1055. width: 1px;
  1056. height: 100%;
  1057. background: #67C23A;
  1058. transition: all 0.3s ease;
  1059. }
  1060. .approval-card.high-risk::before {
  1061. background: #F56C6C;
  1062. }
  1063. .approval-card:hover {
  1064. border-color: #409EFF;
  1065. box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
  1066. }
  1067. /* 卡片头部 */
  1068. .card-header {
  1069. display: flex;
  1070. justify-content: space-between;
  1071. align-items: center;
  1072. margin-bottom: 14px;
  1073. padding-bottom: 10px;
  1074. border-bottom: 1px solid #EBEEF5;
  1075. }
  1076. .card-title-area {
  1077. display: flex;
  1078. align-items: center;
  1079. gap: 10px;
  1080. }
  1081. .risk-tag {
  1082. font-weight: 500;
  1083. padding: 0px 10px;
  1084. font-size: 14px;
  1085. }
  1086. .risk-tag i {
  1087. margin-right: 4px;
  1088. }
  1089. .apply-no {
  1090. font-size: 13px;
  1091. color: #606266;
  1092. font-family: 'Courier New', monospace;
  1093. font-weight: 500;
  1094. }
  1095. /* 附件徽章 */
  1096. .attachment-badge {
  1097. cursor: pointer;
  1098. }
  1099. .attachment-btn {
  1100. padding: 4px 12px;
  1101. font-size: 12px;
  1102. color: #909399;
  1103. transition: all 0.3s ease;
  1104. }
  1105. .attachment-btn:hover {
  1106. color: #409EFF;
  1107. }
  1108. .attachment-btn i {
  1109. margin-right: 4px;
  1110. font-size: 14px;
  1111. }
  1112. /* 附件弹窗空状态 */
  1113. .empty-attachment {
  1114. text-align: center;
  1115. padding: 60px 20px;
  1116. }
  1117. /* 卡片主体 */
  1118. .card-body {
  1119. //margin-bottom: 14px;
  1120. }
  1121. .experiment-title {
  1122. font-size: 15px;
  1123. font-weight: 600;
  1124. color: #303133;
  1125. margin: 0 0 14px 0;
  1126. line-height: 1.5;
  1127. display: flex;
  1128. align-items: flex-start;
  1129. gap: 6px;
  1130. //min-height: 45px;
  1131. }
  1132. .experiment-title i {
  1133. color: #409EFF;
  1134. margin-top: 2px;
  1135. font-size: 16px;
  1136. }
  1137. .card-details {
  1138. display: flex;
  1139. flex-direction: column;
  1140. gap: 8px;
  1141. }
  1142. .detail-row {
  1143. display: flex;
  1144. justify-content: space-between;
  1145. align-items: center;
  1146. font-size: 13px;
  1147. padding: 6px 10px;
  1148. background: #F5F7FA;
  1149. border-radius: 3px;
  1150. transition: all 0.2s ease;
  1151. }
  1152. .detail-row:hover {
  1153. background: #ECF5FF;
  1154. }
  1155. .detail-label {
  1156. color: #909399;
  1157. display: flex;
  1158. align-items: center;
  1159. gap: 5px;
  1160. font-weight: 500;
  1161. }
  1162. .detail-label i {
  1163. color: #409EFF;
  1164. font-size: 14px;
  1165. }
  1166. .detail-value {
  1167. color: #606266;
  1168. font-weight: 500;
  1169. text-align: right;
  1170. max-width: 250px;
  1171. overflow: hidden;
  1172. text-overflow: ellipsis;
  1173. white-space: nowrap;
  1174. }
  1175. /* 卡片底部 */
  1176. .card-footer {
  1177. display: flex;
  1178. justify-content: space-between;
  1179. align-items: center;
  1180. padding-top: 10px;
  1181. border-top: 1px solid #EBEEF5;
  1182. }
  1183. .current-step {
  1184. font-size: 12px;
  1185. color: #909399;
  1186. display: flex;
  1187. align-items: center;
  1188. gap: 5px;
  1189. }
  1190. .current-step i {
  1191. color: #409EFF;
  1192. }
  1193. .action-buttons {
  1194. display: flex;
  1195. gap: 8px;
  1196. }
  1197. .approve-btn {
  1198. background: #f0f9ff;
  1199. border-color: #b3e19d;
  1200. color: #67C23A;
  1201. font-weight: 500;
  1202. padding: 7px 16px;
  1203. font-size: 13px;
  1204. transition: all 0.3s ease;
  1205. }
  1206. .approve-btn:hover {
  1207. background: #67C23A;
  1208. border-color: #67C23A;
  1209. color: #FFFFFF;
  1210. }
  1211. .reject-btn {
  1212. background: #fef0f0;
  1213. border-color: #fbc4c4;
  1214. color: #F56C6C;
  1215. font-weight: 500;
  1216. padding: 7px 16px;
  1217. font-size: 13px;
  1218. transition: all 0.3s ease;
  1219. }
  1220. .reject-btn:hover {
  1221. background: #F56C6C;
  1222. border-color: #F56C6C;
  1223. color: #FFFFFF;
  1224. }
  1225. /* ===== 空状态 ===== */
  1226. .empty-state {
  1227. text-align: center;
  1228. padding: 80px 20px;
  1229. background: white;
  1230. border-radius: 4px;
  1231. border: 1px solid #EBEEF5;
  1232. }
  1233. .empty-icon {
  1234. font-size: 64px;
  1235. color: #c0c4cc;
  1236. margin-bottom: 12px;
  1237. }
  1238. .empty-text {
  1239. font-size: 15px;
  1240. color: #606266;
  1241. margin: 0 0 6px 0;
  1242. font-weight: 500;
  1243. }
  1244. .empty-subtext {
  1245. font-size: 13px;
  1246. color: #909399;
  1247. margin: 0;
  1248. }
  1249. /* ===== 分页 ===== */
  1250. .pagination-wrapper {
  1251. display: flex;
  1252. justify-content: center;
  1253. padding: 15px;
  1254. background: white;
  1255. border-radius: 4px;
  1256. border: 1px solid #EBEEF5;
  1257. }
  1258. /* ===== 卡片动画 ===== */
  1259. .card-list-enter-active {
  1260. animation: cardFadeIn 0.4s ease;
  1261. }
  1262. .card-list-leave-active {
  1263. animation: cardFadeOut 0.3s ease;
  1264. }
  1265. @keyframes cardFadeIn {
  1266. from {
  1267. opacity: 0;
  1268. transform: translateY(15px);
  1269. }
  1270. to {
  1271. opacity: 1;
  1272. transform: translateY(0);
  1273. }
  1274. }
  1275. @keyframes cardFadeOut {
  1276. from {
  1277. opacity: 1;
  1278. transform: scale(1);
  1279. }
  1280. to {
  1281. opacity: 0;
  1282. transform: scale(0.95);
  1283. }
  1284. }
  1285. /* ===== 响应式设计 ===== */
  1286. @media screen and (max-width: 1600px) {
  1287. .cards-grid {
  1288. grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
  1289. }
  1290. }
  1291. @media screen and (max-width: 1200px) {
  1292. .cards-grid {
  1293. grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  1294. }
  1295. .page-header {
  1296. flex-direction: column;
  1297. gap: 15px;
  1298. align-items: flex-start;
  1299. }
  1300. .stat-cards {
  1301. width: 100%;
  1302. overflow-x: auto;
  1303. }
  1304. }
  1305. @media screen and (max-width: 768px) {
  1306. .cards-grid {
  1307. grid-template-columns: 1fr;
  1308. }
  1309. .stat-cards {
  1310. flex-direction: column;
  1311. }
  1312. .stat-card {
  1313. width: 100%;
  1314. }
  1315. }
  1316. /* ===== 弹窗样式优化 ===== */
  1317. .dialog-footer {
  1318. text-align: right;
  1319. }
  1320. /* 表头样式 - 深灰色背景,白色文字 */
  1321. .approval-logs-table >>> .el-table__header-wrapper th,
  1322. .approval-logs-table >>> .el-table__header-wrapper .el-table__cell {
  1323. background-color: #F5F7FA !important;
  1324. color: #606266 !important;
  1325. font-size: 12px;
  1326. font-weight: 600;
  1327. }
  1328. /* 数据行样式 - 更高的行高防止遮挡 */
  1329. .approval-logs-table >>> .el-table__body-wrapper .el-table__row {
  1330. height: 40px !important;
  1331. }
  1332. .approval-logs-table >>> .el-table__body-wrapper td,
  1333. .approval-logs-table >>> .el-table__body-wrapper .el-table__cell {
  1334. height: 40px !important;
  1335. padding: 0 !important;
  1336. }
  1337. /* 关键修复:cell容器要有足够的padding和overflow可见 */
  1338. .approval-logs-table >>> .el-table__body-wrapper .cell {
  1339. padding: 1px 12px !important;
  1340. line-height: 24px !important;
  1341. overflow: visible !important;
  1342. height: auto !important;
  1343. }
  1344. /* 标签样式 - 确保不被遮挡 */
  1345. .approval-logs-table >>> .el-tag {
  1346. vertical-align: middle !important;
  1347. display: inline-block !important;
  1348. margin: 2px 0 !important;
  1349. }
  1350. /* 悬停效果 */
  1351. .approval-logs-table >>> .el-table__body tr:hover > td {
  1352. background-color: #f5f7fa !important;
  1353. }
  1354. /deep/ .no-arrow .el-collapse-item__header .el-collapse-item__arrow {
  1355. display: none !important;
  1356. }
  1357. </style>