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.

1529 lines
39 KiB

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