extractMaterial.vue 20 KB

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