extractMaterial.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758
  1. <template>
  2. <div class="extract-material-container">
  3. <div class="extract-controls">
  4. <el-button type="primary" @click="handleAddExtract">添加资料</el-button>
  5. <!-- <el-button
  6. type="danger"
  7. :disabled="selectedRows.length === 0"
  8. @click="handleBatchDelete"
  9. >
  10. 批量删除
  11. </el-button> -->
  12. </div>
  13. <el-table
  14. v-loading="loading"
  15. :data="extractMaterials"
  16. style="width: 100%"
  17. stripe
  18. border
  19. size="small"
  20. >
  21. <!-- <el-table-column
  22. type="selection"
  23. width="55"
  24. align="center"
  25. ></el-table-column> -->
  26. <el-table-column prop="id" label="编号" width="80">
  27. <template slot-scope="scope">
  28. {{ scope.$index + 1 }}
  29. </template>
  30. </el-table-column>
  31. <el-table-column
  32. prop="materialName"
  33. label="材料名称"
  34. width="200"
  35. show-overflow-tooltip
  36. ></el-table-column>
  37. <el-table-column
  38. prop="pages"
  39. label="页数"
  40. width="100"
  41. align="center"
  42. ></el-table-column>
  43. <el-table-column
  44. prop="remark"
  45. label="备注"
  46. min-width="200"
  47. show-overflow-tooltip
  48. ></el-table-column>
  49. <el-table-column
  50. prop="createTime"
  51. label="提取时间"
  52. width="200"
  53. :formatter="formatDate"
  54. align="center"
  55. show-overflow-tooltip
  56. ></el-table-column>
  57. <el-table-column label="操作" width="180" fixed="right">
  58. <template slot-scope="scope">
  59. <el-button
  60. type="text"
  61. size="small"
  62. @click="handleEditExtract(scope.row)"
  63. >
  64. 修改
  65. </el-button>
  66. <el-button
  67. type="text"
  68. size="small"
  69. @click="handleDeleteExtract(scope.row)"
  70. >
  71. 删除
  72. </el-button>
  73. </template>
  74. </el-table-column>
  75. </el-table>
  76. <el-dialog
  77. :visible.sync="dialogVisible"
  78. :title="dialogTitle"
  79. width="600px"
  80. :close-on-click-modal="false"
  81. :modal="false"
  82. append-to-body
  83. custom-class="extract-material-dialog"
  84. >
  85. <div class="dialog-content">
  86. <el-form
  87. ref="extractForm"
  88. :model="extractForm"
  89. :rules="rules"
  90. class="extract-form"
  91. label-width="0"
  92. >
  93. <el-form-item
  94. prop="materialName"
  95. label="资料名称:"
  96. class="custom-form-item"
  97. >
  98. <el-input
  99. v-model="extractForm.materialName"
  100. placeholder="请输入材料名称"
  101. ></el-input>
  102. </el-form-item>
  103. <el-form-item prop="pageCount" label="页数:" class="custom-form-item">
  104. <el-input-number
  105. v-model.number="extractForm.pageCount"
  106. placeholder="请输入页数"
  107. :min="1"
  108. :step="1"
  109. ></el-input-number>
  110. </el-form-item>
  111. <el-form-item prop="orderNum" label="序号:" class="custom-form-item">
  112. <el-input-number
  113. v-model.number="extractForm.orderNum"
  114. placeholder="请输入序号"
  115. :min="1"
  116. :step="1"
  117. ></el-input-number>
  118. </el-form-item>
  119. <el-form-item prop="remark" label="备注:" class="custom-form-item">
  120. <el-input
  121. v-model="extractForm.remark"
  122. type="textarea"
  123. placeholder="请输入备注"
  124. :rows="4"
  125. ></el-input>
  126. </el-form-item>
  127. <el-form-item
  128. prop="fileList"
  129. label="上传扫描件:"
  130. class="custom-form-item"
  131. >
  132. <div class="upload-wrapper">
  133. <el-upload
  134. class="upload-demo"
  135. :action="''"
  136. :http-request="handleFileUpload"
  137. :on-remove="handleFileRemove"
  138. :before-upload="beforeFileUpload"
  139. :on-success="handleFileUploadSuccess"
  140. :on-error="handleFileUploadError"
  141. :on-progress="handleFileUploadProgress"
  142. :file-list="extractForm.fileList"
  143. :limit="1"
  144. :on-exceed="handleFileExceed"
  145. >
  146. <el-button
  147. v-show="
  148. !extractForm.fileList || extractForm.fileList.length === 0
  149. "
  150. type="primary"
  151. size="small"
  152. class="upload-btn"
  153. >
  154. 选择文件
  155. </el-button>
  156. <div class="upload-tip">
  157. 多张扫描图片请插入一个word文档中上传
  158. </div>
  159. </el-upload>
  160. </div>
  161. </el-form-item>
  162. </el-form>
  163. </div>
  164. <div slot="footer" class="dialog-footer">
  165. <el-button @click="dialogVisible = false">取消</el-button>
  166. <el-button type="primary" @click="handleSubmit">保存</el-button>
  167. </div>
  168. </el-dialog>
  169. </div>
  170. </template>
  171. <script>
  172. // 暂时使用mock数据,后续根据实际API调整
  173. import {
  174. getExtractMaterialList,
  175. addTaskEvidence,
  176. updateTaskEvidence,
  177. deleteTaskEvidence,
  178. } from '@/api/audit/taskEvidence'
  179. import { uploadFile } from '@/api/file'
  180. export default {
  181. name: 'ExtractMaterial',
  182. props: {
  183. id: {
  184. type: [String, Number],
  185. default: null,
  186. },
  187. },
  188. data() {
  189. return {
  190. loading: false,
  191. extractMaterials: [],
  192. selectedRows: [],
  193. dialogVisible: false,
  194. dialogTitle: '添加提取材料',
  195. isEdit: false,
  196. extractForm: {
  197. id: '',
  198. materialName: '',
  199. pageCount: null,
  200. orderNum: null,
  201. remark: '',
  202. fileList: [],
  203. attachmentUrl: '',
  204. },
  205. rules: {
  206. materialName: [
  207. { required: true, message: '请输入材料名称', trigger: 'blur' },
  208. {
  209. min: 1,
  210. max: 100,
  211. message: '材料名称长度应在1-100个字符之间',
  212. trigger: 'blur',
  213. },
  214. ],
  215. pageCount: [
  216. {
  217. required: true,
  218. message: '请输入页数',
  219. trigger: ['blur', 'change'],
  220. },
  221. {
  222. type: 'number',
  223. message: '页数必须为数字',
  224. trigger: ['blur', 'change'],
  225. },
  226. {
  227. validator: (rule, value, callback) => {
  228. if (value === null || value === undefined || value === '') {
  229. callback()
  230. return
  231. }
  232. if (typeof value !== 'number' || isNaN(value)) {
  233. callback(new Error('页数必须为数字'))
  234. return
  235. }
  236. if (value < 1) {
  237. callback(new Error('页数必须大于0'))
  238. return
  239. }
  240. callback()
  241. },
  242. trigger: ['blur', 'change'],
  243. },
  244. ],
  245. orderNum: [
  246. {
  247. required: true,
  248. message: '请输入序号',
  249. trigger: ['blur', 'change'],
  250. },
  251. {
  252. type: 'number',
  253. message: '序号必须为数字',
  254. trigger: ['blur', 'change'],
  255. },
  256. {
  257. validator: (rule, value, callback) => {
  258. if (value === null || value === undefined || value === '') {
  259. callback()
  260. return
  261. }
  262. if (typeof value !== 'number' || isNaN(value)) {
  263. callback(new Error('序号必须为数字'))
  264. return
  265. }
  266. if (value < 1) {
  267. callback(new Error('序号必须大于0'))
  268. return
  269. }
  270. callback()
  271. },
  272. trigger: ['blur', 'change'],
  273. },
  274. ],
  275. },
  276. }
  277. },
  278. mounted() {
  279. this.getExtractMaterials()
  280. },
  281. beforeDestroy() {
  282. // 清理定时器等资源
  283. this.loading = false
  284. },
  285. methods: {
  286. // 处理选中行变化
  287. handleSelectionChange(selection) {
  288. this.selectedRows = selection
  289. },
  290. // 获取提取材料列表
  291. async getExtractMaterials() {
  292. if (!this.id) {
  293. return
  294. }
  295. try {
  296. this.loading = true
  297. const res = await getExtractMaterialList({
  298. taskId: this.id,
  299. })
  300. if (res && res.value) {
  301. // 统一字段名:如果后端返回的是 pageCount,映射为 pages
  302. this.extractMaterials = (res.value || []).map((item) => ({
  303. ...item,
  304. pages: item.pages !== undefined ? item.pages : item.pageCount,
  305. }))
  306. } else {
  307. this.extractMaterials = []
  308. }
  309. } catch (error) {
  310. // this.$message.error('获取提取材料列表失败')
  311. console.error('获取提取材料列表失败:', error)
  312. this.extractMaterials = []
  313. } finally {
  314. this.loading = false
  315. }
  316. },
  317. // 添加资料
  318. handleAddExtract() {
  319. this.isEdit = false
  320. this.dialogTitle = '添加提取材料'
  321. this.extractForm = {
  322. id: '',
  323. materialName: '',
  324. pageCount: null,
  325. orderNum: null,
  326. remark: '',
  327. fileList: [],
  328. attachmentUrl: '',
  329. }
  330. // 重置表单验证状态
  331. if (this.$refs.extractForm) {
  332. this.$refs.extractForm.resetFields()
  333. }
  334. this.dialogVisible = true
  335. },
  336. // 编辑资料
  337. handleEditExtract(row) {
  338. this.isEdit = true
  339. this.dialogTitle = '编辑提取材料'
  340. // 确保字段名统一,如果后端返回的是 pages,映射为 pageCount
  341. // 处理文件列表
  342. let fileList = []
  343. if (row.attachmentUrl) {
  344. fileList = [
  345. {
  346. name: row.fileName || '附件',
  347. url: row.attachmentUrl,
  348. response: { savePath: row.attachmentUrl },
  349. },
  350. ]
  351. }
  352. this.extractForm = {
  353. ...row,
  354. pageCount: row.pageCount !== undefined ? row.pageCount : row.pages,
  355. orderNum: row.orderNum !== undefined ? row.orderNum : row.orderNumber,
  356. fileList: fileList,
  357. attachmentUrl: row.attachmentUrl || '',
  358. }
  359. this.dialogVisible = true
  360. },
  361. // 删除资料
  362. async handleDeleteExtract(row) {
  363. try {
  364. await this.$confirm('确定要删除该提取材料吗?', '提示', {
  365. confirmButtonText: '确定',
  366. cancelButtonText: '取消',
  367. type: 'warning',
  368. })
  369. const res = await deleteTaskEvidence({ id: row.id })
  370. if (res.code === 200) {
  371. this.$message.success('删除成功')
  372. // 重新获取列表
  373. this.getExtractMaterials()
  374. } else {
  375. this.$message.error(res.message || '删除失败')
  376. }
  377. } catch (error) {
  378. if (error !== 'cancel') {
  379. // this.$message.error('删除失败')
  380. console.error('删除提取材料失败:', error)
  381. }
  382. }
  383. },
  384. // 批量删除
  385. async handleBatchDelete() {
  386. if (this.selectedRows.length === 0) {
  387. this.$message.warning('请选择要删除的提取材料')
  388. return
  389. }
  390. try {
  391. await this.$confirm(
  392. `确定要删除选中的 ${this.selectedRows.length} 条提取材料吗?`,
  393. '提示',
  394. {
  395. confirmButtonText: '确定',
  396. cancelButtonText: '取消',
  397. type: 'warning',
  398. }
  399. )
  400. // 批量删除,逐条调用删除接口
  401. const deletePromises = this.selectedRows.map((row) =>
  402. deleteTaskEvidence({ id: row.id })
  403. )
  404. const results = await Promise.all(deletePromises)
  405. // 检查是否有失败的
  406. const failedCount = results.filter((res) => res.code !== 200).length
  407. if (failedCount === 0) {
  408. this.$message.success('批量删除成功')
  409. this.selectedRows = [] // 清空选择
  410. // 重新获取列表
  411. this.getExtractMaterials()
  412. } else {
  413. this.$message.warning(
  414. `批量删除完成,${
  415. results.length - failedCount
  416. } 条成功,${failedCount} 条失败`
  417. )
  418. // 即使有失败的,也刷新列表以获取最新数据
  419. this.selectedRows = []
  420. this.getExtractMaterials()
  421. }
  422. } catch (error) {
  423. if (error !== 'cancel') {
  424. // this.$message.error('批量删除失败')
  425. console.error('批量删除提取材料失败:', error)
  426. }
  427. }
  428. },
  429. // 提交表单
  430. async handleSubmit() {
  431. try {
  432. await this.$refs.extractForm.validate()
  433. const formData = {
  434. id: this.extractForm.id,
  435. materialName: this.extractForm.materialName,
  436. pageCount: this.extractForm.pageCount,
  437. orderNum: this.extractForm.orderNum,
  438. remark: this.extractForm.remark,
  439. attachmentUrl: this.extractForm.attachmentUrl,
  440. taskId: this.id,
  441. }
  442. let res
  443. if (this.isEdit) {
  444. // 编辑模式,调用更新接口
  445. res = await updateTaskEvidence(formData)
  446. } else {
  447. // 添加模式,调用新增接口
  448. res = await addTaskEvidence(formData)
  449. }
  450. if (res.code === 200) {
  451. this.$message.success(this.isEdit ? '更新成功' : '添加成功')
  452. this.dialogVisible = false
  453. this.getExtractMaterials()
  454. } else {
  455. this.$message.error(
  456. res.message || (this.isEdit ? '更新失败' : '添加失败')
  457. )
  458. }
  459. } catch (error) {
  460. if (error !== 'cancel') {
  461. // this.$message.error(this.isEdit ? '更新失败' : '添加失败')
  462. console.error(
  463. this.isEdit ? '更新提取材料失败:' : '添加提取材料失败:',
  464. error
  465. )
  466. }
  467. }
  468. },
  469. // 文件上传前验证
  470. beforeFileUpload(file) {
  471. const allowedTypes = [
  472. 'application/pdf',
  473. 'application/msword',
  474. 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  475. 'application/vnd.ms-excel',
  476. 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  477. 'text/csv',
  478. ]
  479. const allowedExtensions = [
  480. '.pdf',
  481. '.doc',
  482. '.docx',
  483. '.xls',
  484. '.xlsx',
  485. '.csv',
  486. ]
  487. const fileExtension = '.' + file.name.split('.').pop().toLowerCase()
  488. // 检查文件格式
  489. const isCorrectType = allowedTypes.includes(file.type)
  490. const isCorrectExtension = allowedExtensions.includes(fileExtension)
  491. if (!isCorrectType && !isCorrectExtension) {
  492. this.$message.error(
  493. '只允许上传 pdf, doc, docx, xls, xlsx, csv 格式的文件!'
  494. )
  495. return false
  496. }
  497. // 检查文件大小 (50MB)
  498. const isLt50M = file.size / 1024 / 1024 < 50
  499. if (!isLt50M) {
  500. this.$message.error('文件大小不能超过 50MB!')
  501. return false
  502. }
  503. return true
  504. },
  505. // 自定义上传方法
  506. async handleFileUpload(options) {
  507. const { file, onProgress, onSuccess, onError } = options
  508. // 检查是否已经上传了文件
  509. if (
  510. this.extractForm.fileList &&
  511. this.extractForm.fileList.length >= 1
  512. ) {
  513. this.$message.warning('只能上传一个文件,请先删除已上传的文件')
  514. if (onError) {
  515. onError(new Error('只能上传一个文件'))
  516. }
  517. return
  518. }
  519. const formData = new FormData()
  520. formData.append('file', file)
  521. try {
  522. // 显示上传进度
  523. if (onProgress) {
  524. onProgress({ percent: 0 })
  525. }
  526. // 调用上传API
  527. const uploadRes = await uploadFile('/api/file/v1/upload', formData, {
  528. onUploadProgress: (progressEvent) => {
  529. // 计算上传进度
  530. if (progressEvent.total) {
  531. const percent = Math.round(
  532. (progressEvent.loaded * 100) / progressEvent.total
  533. )
  534. if (onProgress) {
  535. onProgress({ percent })
  536. }
  537. }
  538. },
  539. })
  540. // 检查上传结果
  541. if (uploadRes && uploadRes.code === 200 && uploadRes.value) {
  542. const fileInfo = uploadRes.value
  543. // 构造文件信息对象,符合element-ui upload组件的格式
  544. const fileObj = {
  545. uid: file.uid,
  546. name: file.name,
  547. status: 'success',
  548. size: file.size,
  549. response: fileInfo,
  550. url: fileInfo.savePath || fileInfo.url,
  551. }
  552. if (onSuccess) {
  553. onSuccess(fileInfo, fileObj)
  554. this.extractForm.attachmentUrl = fileInfo.savePath || fileInfo.url
  555. }
  556. this.$message.success(`${file.name} 上传成功`)
  557. } else {
  558. throw new Error(uploadRes?.message || '上传失败,请稍后重试')
  559. }
  560. } catch (error) {
  561. console.error('文件上传失败:', error)
  562. // this.$message.error(`文件上传失败:${error.message || '未知错误'}`)
  563. if (onError) {
  564. onError(error)
  565. }
  566. }
  567. },
  568. // 上传成功回调
  569. handleFileUploadSuccess(response, file, fileList) {
  570. // 更新文件列表,添加文件URL信息
  571. this.extractForm.fileList = fileList.map((item) => {
  572. if (item.uid === file.uid && response) {
  573. return {
  574. ...item,
  575. url: response.savePath || response.url || item.url,
  576. response: response,
  577. }
  578. }
  579. return item
  580. })
  581. },
  582. // 上传进度回调
  583. handleFileUploadProgress(event, file, fileList) {
  584. // element-ui 会自动处理上传进度显示
  585. },
  586. // 上传失败回调
  587. handleFileUploadError(err, file, fileList) {
  588. console.error('文件上传错误:', err)
  589. this.$message.error(`${file.name} 上传失败`)
  590. // 从文件列表中移除失败的文件
  591. this.extractForm.fileList = fileList.filter(
  592. (item) => item.uid !== file.uid
  593. )
  594. },
  595. // 超出文件数量限制
  596. handleFileExceed(files, fileList) {
  597. this.$message.warning(
  598. `当前限制选择 1 个文件,本次选择了 ${files.length} 个文件,共选择了 ${fileList.length} 个文件`
  599. )
  600. },
  601. // 移除文件
  602. handleFileRemove(file, fileList) {
  603. this.extractForm.fileList = fileList
  604. this.extractForm.attachmentUrl = ''
  605. this.$message.info(`${file.name} 已移除`)
  606. },
  607. // 格式化日期
  608. formatDate(row, column, cellValue) {
  609. if (!cellValue) return ''
  610. try {
  611. const date = new Date(cellValue)
  612. if (isNaN(date.getTime())) return ''
  613. return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(
  614. 2,
  615. '0'
  616. )}-${String(date.getDate()).padStart(2, '0')} ${String(
  617. date.getHours()
  618. ).padStart(2, '0')}:${String(date.getMinutes()).padStart(
  619. 2,
  620. '0'
  621. )}:${String(date.getSeconds()).padStart(2, '0')}`
  622. } catch (error) {
  623. return cellValue
  624. }
  625. },
  626. },
  627. }
  628. </script>
  629. <style scoped>
  630. /* .extract-material-container {
  631. padding: 20px;
  632. } */
  633. .extract-controls {
  634. margin-bottom: 20px;
  635. text-align: left;
  636. }
  637. /* 操作按钮样式优化 */
  638. .el-button--small {
  639. margin-right: 8px;
  640. }
  641. /* 表格行悬停效果 */
  642. .el-table--enable-row-hover .el-table__body tr:hover > td {
  643. background-color: #f5f7fa;
  644. }
  645. </style>
  646. <style>
  647. /* 弹窗样式 */
  648. .extract-material-dialog .dialog-content {
  649. padding: 20px;
  650. }
  651. .extract-material-dialog .extract-form {
  652. margin: 0;
  653. }
  654. .extract-material-dialog .custom-form-item {
  655. display: flex;
  656. align-items: flex-start;
  657. margin-bottom: 20px;
  658. }
  659. .extract-material-dialog .custom-form-item .el-form-item__label {
  660. width: 100px !important;
  661. text-align: center !important;
  662. /* color: #409eff !important; */
  663. font-size: 14px;
  664. flex-shrink: 0;
  665. padding: 0 !important;
  666. line-height: 32px;
  667. }
  668. .extract-material-dialog .custom-form-item .el-form-item__content {
  669. flex: 1;
  670. margin-left: 0 !important;
  671. }
  672. .extract-material-dialog .custom-form-item .el-input,
  673. .extract-material-dialog .custom-form-item .el-textarea,
  674. .extract-material-dialog .custom-form-item .el-input-number {
  675. width: 100%;
  676. }
  677. .extract-material-dialog .upload-wrapper {
  678. flex: 1;
  679. display: flex;
  680. flex-direction: column;
  681. gap: 8px;
  682. align-items: flex-start;
  683. }
  684. .extract-material-dialog .upload-tip {
  685. font-size: 12px;
  686. color: #909399;
  687. line-height: 1.5;
  688. margin-top: 10px;
  689. }
  690. .extract-material-dialog .upload-btn {
  691. align-self: flex-start;
  692. }
  693. .extract-material-dialog .el-upload {
  694. text-align: left;
  695. }
  696. .extract-material-dialog .dialog-footer {
  697. text-align: center;
  698. padding: 10px 0;
  699. }
  700. .extract-material-dialog .dialog-footer .el-button {
  701. margin: 0 10px;
  702. }
  703. </style>