UploadComponent.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. <template>
  2. <div class="upload-container">
  3. <!-- 上传按钮和清空按钮 -->
  4. <div class="upload-buttons">
  5. <!-- <el-button
  6. plain
  7. type="primary"
  8. size="small"
  9. :disabled="isUploadDisabled || isDisabled"
  10. @click="showConfirmDialog"
  11. >
  12. {{ buttonText }}
  13. </el-button> -->
  14. <el-upload
  15. :action="uploadUrl"
  16. :auto-upload="false"
  17. :on-change="handleChange"
  18. :file-list="uploadList"
  19. :accept="accept"
  20. :multiple="uploadMode == 'single' ? false : true"
  21. :data="{ businessFolder: businessFolder }"
  22. style="display: inline-block"
  23. >
  24. <el-tooltip effect="dark" placement="top">
  25. <el-button
  26. plain
  27. :type="props.btnType ? props.btnType : 'primary'"
  28. size="small"
  29. :disabled="isUploadDisabled || isDisabled"
  30. >
  31. {{ buttonText }}
  32. </el-button>
  33. <div slot="content">
  34. 请上传(大小不超过{{ formatFileSize(maxSize) }},格式:{{
  35. allowedTypes.join('/')
  36. }})
  37. </div>
  38. </el-tooltip>
  39. </el-upload>
  40. <!-- <el-dialog
  41. title="选择文件"
  42. :visible.sync="confirmDialogVisible"
  43. width="30%"
  44. :modal-append-to-body="true"
  45. :append-to-body="true"
  46. >
  47. <el-upload
  48. :action="uploadUrl"
  49. :auto-upload="false"
  50. :on-change="handleChange"
  51. :file-list="uploadList"
  52. accept=".xlsx,.xls,.doc,.docx,.pdf"
  53. :multiple="uploadMode == 'single' ? false : true"
  54. :data="{ businessFolder: businessFolder }"
  55. style="display: inline-block"
  56. >
  57. <el-button
  58. plain
  59. type="primary"
  60. size="small"
  61. :disabled="isUploadDisabled"
  62. >
  63. 选择文件
  64. </el-button>
  65. </el-upload>
  66. <div slot="footer" class="dialog-footer">
  67. <el-button type="primary" size="small" @click="confirmUpload">
  68. 确认
  69. </el-button>
  70. <el-button
  71. plain
  72. type="primary"
  73. size="small"
  74. @click="confirmDialogVisible = false"
  75. >
  76. 取消
  77. </el-button>
  78. </div>
  79. </el-dialog> -->
  80. <!-- <el-button plain type="primary" size="small" @click="clearFiles">
  81. 清空
  82. </el-button> -->
  83. </div>
  84. <!-- 附件列表(3个以内) v-if="files.length <= 3" -->
  85. <div class="file-list-simple">
  86. <div v-for="(file, index) in files" :key="index" class="file-item">
  87. <i
  88. class="fileIcon iconfont-5039297"
  89. :class="fileIcon(file.fileExtension)"
  90. ></i>
  91. <span class="file-fileName" @click="handlePreview(file)">
  92. {{ file.fileName }}
  93. </span>
  94. <!-- 状态标签 -->
  95. <span class="file-status">
  96. <el-tag size="mini" :type="getStatusType(file.status)">
  97. {{ getStatusText(file.status) }}
  98. </el-tag>
  99. </span>
  100. <span
  101. class="delete-btn"
  102. :disabled="isDisabled"
  103. :class="{ 'disabled-text': isDisabled }"
  104. @click="removeFile(index)"
  105. >
  106. <el-tooltip effect="dark" content="删除" placement="top">
  107. <i class="iconfont-5039297 icon-shanchu1"></i>
  108. </el-tooltip>
  109. </span>
  110. </div>
  111. </div>
  112. <!-- 表格形式(超过3个) -->
  113. <!-- <div v-else class="file-list-table">
  114. <el-table :data="files" style="width: 100%">
  115. <el-table-column type="index" label="序号" width="60" />
  116. <el-table-column prop="fileName" label="附件名称" />
  117. <el-table-column prop="type" label="附件类型" width="100">
  118. <template slot-scope="scope">
  119. {{ scope.row.fileExtension || '' }}
  120. </template>
  121. </el-table-column>
  122. <el-table-column prop="fileSize" label="附件大小" width="100" />
  123. <el-table-column label="状态" width="80">
  124. <template slot-scope="scope">
  125. <el-tag size="mini" :type="getStatusType(scope.row.status)">
  126. {{ getStatusText(scope.row.status) }}
  127. </el-tag>
  128. </template>
  129. </el-table-column>
  130. <el-table-column label="操作" width="200">
  131. <template slot-scope="scope">
  132. <el-button size="mini" @click="handleSort(scope.$index, 'up')">
  133. </el-button>
  134. <el-button size="mini" @click="handleSort(scope.$index, 'down')">
  135. </el-button>
  136. <el-button
  137. size="mini"
  138. type="danger"
  139. @click="removeFile(scope.$index)"
  140. >
  141. 删除
  142. </el-button>
  143. </template>
  144. </el-table-column>
  145. </el-table> -->
  146. <!-- </div> -->
  147. <!-- 底部按钮(仅当超过3个时显示) -->
  148. <!-- <div v-if="files.length > 3" class="table-footer">
  149. <el-button type="primary" size="small" @click="saveFiles">保存</el-button>
  150. <el-button size="small">关闭</el-button>
  151. </div> -->
  152. </div>
  153. </template>
  154. <script>
  155. import { uploadFile } from '@/api/file'
  156. export default {
  157. name: 'UploadComponent',
  158. props: {
  159. filesList: {
  160. type: Array,
  161. default: () => [],
  162. },
  163. uploadMode: {
  164. type: String,
  165. default: 'single', // single 或 multiple
  166. },
  167. actionUrl: {
  168. type: String,
  169. default: '', // 上传路径
  170. },
  171. businessFolder: {
  172. type: String,
  173. default: '',
  174. },
  175. buttonText: {
  176. type: String,
  177. default: '上传',
  178. },
  179. maxSize: {
  180. type: Number,
  181. default: 50 * 1024 * 1024, // 默认50MB
  182. validator(value) {
  183. return value >= 0
  184. },
  185. },
  186. allowedTypes: {
  187. type: Array,
  188. default: () => ['xlsx', 'xls', 'doc', 'docx', 'pdf'],
  189. },
  190. isDisabled: {
  191. type: Boolean,
  192. default: false,
  193. },
  194. props: {
  195. type: Object,
  196. default: () => ({}),
  197. },
  198. },
  199. data() {
  200. return {
  201. files: [],
  202. uploadList: [],
  203. uploadUrl: '/api/file/v1/upload', // 单文件上传地址
  204. uploading: false,
  205. confirmDialogVisible: false,
  206. }
  207. },
  208. computed: {
  209. // 计算属性:判断上传按钮是否应该禁用
  210. isUploadDisabled() {
  211. // 只在单文件上传模式下检查
  212. if (this.uploadMode === 'single') {
  213. // 如果已经有文件(无论是上传的还是回显的),则禁用上传按钮
  214. return this.files.length > 0 || this.uploadList.length > 0
  215. }
  216. // 多文件上传模式下不禁用
  217. return false
  218. },
  219. accept() {
  220. let allowedTypes = this.allowedTypes.map((type) => {
  221. return '.' + type
  222. })
  223. return allowedTypes.join(',')
  224. },
  225. },
  226. watch: {
  227. filesList: {
  228. handler(newVal) {
  229. this.files = newVal.map((item) => {
  230. // 如果 item 是字符串(文件路径)
  231. if (typeof item === 'string') {
  232. return {
  233. fileName: item.substring(item.lastIndexOf('/') + 1),
  234. filePath: item,
  235. status: 'success',
  236. }
  237. }
  238. // 如果 item 已经是对象
  239. return {
  240. ...item,
  241. status: item.status || 'success',
  242. }
  243. })
  244. },
  245. immediate: true, // 立即执行一次,用于初始化回显
  246. },
  247. actionUrl: {
  248. handler(newVal) {
  249. this.uploadUrl = newVal || '/api/file/v1/upload'
  250. },
  251. immediate: true,
  252. },
  253. },
  254. methods: {
  255. // 在 methods 中添加格式化方法
  256. formatFileSize(bytes) {
  257. if (bytes === 0) return '0 Bytes'
  258. const k = 1024
  259. const sizes = ['Bytes', 'KB', 'MB', 'GB']
  260. const i = Math.floor(Math.log(bytes) / Math.log(k))
  261. return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
  262. },
  263. showConfirmDialog() {
  264. this.confirmDialogVisible = true
  265. },
  266. clearFiles() {
  267. this.files = []
  268. this.uploadList = []
  269. },
  270. handleChange(file, fileList) {
  271. // 校验文件大小
  272. if (this.maxSize && file.size > this.maxSize) {
  273. this.$message.error(
  274. `文件大小不能超过 ${this.formatFileSize(this.maxSize)}`
  275. )
  276. this.uploadList = this.uploadList.filter((f) => f !== file)
  277. return
  278. }
  279. // 校验文件类型
  280. const fileName = file.name.toLowerCase()
  281. const fileExtension = fileName.split('.').pop()
  282. if (!this.allowedTypes.includes(fileExtension)) {
  283. this.$message.error(
  284. `不支持的文件格式,请上传 ${this.allowedTypes.join(
  285. ', '
  286. )} 格式的文件`
  287. )
  288. this.uploadList = this.uploadList.filter((f) => f !== file)
  289. return
  290. }
  291. this.uploadList = fileList
  292. this.confirmUpload()
  293. // 根据上传模式决定是否立即上传
  294. // if (this.uploadMode === 'single') {
  295. // this.confirmUpload()
  296. // }
  297. },
  298. confirmUpload() {
  299. if (this.uploading) return
  300. this.uploading = true
  301. const formData = new FormData()
  302. formData.append('file', this.uploadList[0].raw)
  303. // if (this.uploadMode === 'single' && this.uploadList.length > 0) {
  304. // formData.append('file', this.uploadList[0].raw)
  305. // } else if (this.uploadMode === 'multiple') {
  306. // // 多文件上传模式
  307. // // 正确的多文件上传方式:为每个文件单独添加到formData中,使用相同的键名
  308. // this.uploadList.forEach((file) => {
  309. // formData.append('files', file.raw)
  310. // })
  311. // }
  312. // 发送上传请求
  313. this.uploadFiles(formData)
  314. },
  315. async uploadFiles(formData) {
  316. try {
  317. this.uploading = true
  318. // 注意:根据实际情况选择合适的API调用方式
  319. const res = await uploadFile(this.uploadUrl, formData, {
  320. onUploadProgress: (progressEvent) => {
  321. // 可添加上传进度显示
  322. if (progressEvent.total) {
  323. const percentComplete = Math.round(
  324. (progressEvent.loaded * 100) / progressEvent.total
  325. )
  326. console.log('上传进度:', percentComplete, '%')
  327. // 如需显示进度条可在此添加逻辑
  328. }
  329. },
  330. timeout: 60000, // 添加60秒超时设置
  331. })
  332. if (res && res.value) {
  333. if (this.uploadUrl == '/api/file/v1/tempUpload') {
  334. this.files.push({
  335. ...res.value,
  336. ...res.value.fileUploadResult,
  337. status: 'success',
  338. })
  339. } else {
  340. this.files.push({
  341. ...res.value,
  342. status: 'success',
  343. })
  344. }
  345. // if (this.uploadMode === 'single') {
  346. // this.files.push({
  347. // ...res.value,
  348. // status: 'success',
  349. // })
  350. // } else {
  351. // // 多文件上传模式
  352. // res.value.forEach((item) => {
  353. // this.files.push({
  354. // ...item,
  355. // status: 'success',
  356. // })
  357. // })
  358. // }
  359. this.uploadList = []
  360. this.$message.success('上传成功')
  361. this.saveFiles() // 自动保存上传结果
  362. } else {
  363. throw new Error('上传返回数据格式不正确')
  364. }
  365. } catch (error) {
  366. console.error('文件上传失败:', error)
  367. // 重置上传状态,确保用户可以再次尝试
  368. this.uploadList = []
  369. } finally {
  370. this.uploading = false
  371. }
  372. },
  373. removeFile(index) {
  374. if (this.isDisabled) return
  375. // 删除文件
  376. const removedFile = this.files.splice(index, 1)[0]
  377. // 通知父组件文件已被删除,并传递当前文件列表
  378. this.$emit('removeFile', index, removedFile, this.files, this.props)
  379. },
  380. handleSort(index, direction) {
  381. if (direction === 'up' && index > 0) {
  382. this.files.splice(index - 1, 0, this.files.splice(index, 1)[0])
  383. } else if (direction === 'down' && index < this.files.length - 1) {
  384. this.files.splice(index + 1, 0, this.files.splice(index, 1)[0])
  385. }
  386. },
  387. getStatusText(status) {
  388. const map = {
  389. pending: '待上传',
  390. success: '已上传',
  391. uploading: '上传中',
  392. failed: '上传失败',
  393. }
  394. return map[status] || status
  395. },
  396. // 添加文件图标类名方法
  397. fileIcon(extension) {
  398. const map = {
  399. xlsx: 'icon-exel',
  400. xls: 'icon-exel',
  401. doc: 'icon-word',
  402. docx: 'icon-word',
  403. pdf: 'icon-word',
  404. }
  405. return map[extension] || 'icon-word'
  406. },
  407. getStatusType(status) {
  408. const map = {
  409. pending: 'warning',
  410. success: 'success',
  411. uploading: 'info',
  412. failed: 'danger',
  413. }
  414. return map[status] || 'default'
  415. },
  416. // 添加获取文件扩展名的方法
  417. getFileExtension(fileName) {
  418. // 从文件名中提取扩展名并转为小写
  419. const lastDotIndex = fileName.lastIndexOf('.')
  420. if (lastDotIndex > -1 && lastDotIndex < fileName.length - 1) {
  421. return fileName.substring(lastDotIndex + 1).toLowerCase()
  422. }
  423. return 'unknown' // 如果没有扩展名,返回unknown
  424. },
  425. // 保存文件列表
  426. saveFiles() {
  427. this.$emit('saveFiles', this.files, this.props)
  428. this.confirmDialogVisible = false
  429. },
  430. handlePreview(file) {
  431. // 对文件URL进行Base64编码
  432. const encodedUrl = encodeURIComponent(
  433. Base64.encode(window.context.form + file.filePath)
  434. )
  435. // 构建 kkFileView 预览URL
  436. // onlinePreview - 在线预览
  437. // onlinePreview?type=pdf - 强制使用PDF模式预览
  438. window.open(`${host}:8012/onlinePreview?url=${encodedUrl}`)
  439. },
  440. },
  441. }
  442. </script>
  443. <style scoped lang="scss">
  444. @import '@/styles/costAudit.scss';
  445. // 上传按钮区域
  446. .upload-buttons {
  447. display: flex;
  448. flex-wrap: wrap;
  449. gap: 10px;
  450. margin-bottom: 20px;
  451. }
  452. // 文件列表
  453. .file-list-simple {
  454. margin-bottom: 0;
  455. }
  456. // 文件项样式
  457. .file-item {
  458. display: flex;
  459. align-items: center;
  460. padding: 12px 15px;
  461. margin-bottom: 8px;
  462. background-color: #fff;
  463. border: 1px solid #ebeef5;
  464. border-radius: 6px;
  465. font-size: 14px;
  466. transition: all 0.3s ease;
  467. &:hover {
  468. border-color: $base-color-default;
  469. box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.06);
  470. transform: translateY(-1px);
  471. }
  472. &:last-child {
  473. margin-bottom: 0;
  474. }
  475. }
  476. // 文件图标
  477. .fileIcon {
  478. font-size: 24px;
  479. color: $base-color-default;
  480. margin-right: 12px;
  481. width: 40px;
  482. height: 40px;
  483. display: flex;
  484. align-items: center;
  485. justify-content: center;
  486. background-color: rgba($base-color-default, 0.1);
  487. border-radius: 4px;
  488. transition: all 0.3s ease;
  489. .file-item:hover & {
  490. background-color: rgba($base-color-default, 0.2);
  491. }
  492. }
  493. // 文件名
  494. .file-fileName {
  495. flex: 1;
  496. color: #303133;
  497. overflow: hidden;
  498. text-overflow: ellipsis;
  499. white-space: nowrap;
  500. transition: all 0.3s ease;
  501. &:hover {
  502. color: $base-color-default;
  503. cursor: pointer;
  504. text-decoration: underline;
  505. }
  506. }
  507. // 状态标签
  508. .file-status {
  509. margin: 0 15px;
  510. }
  511. // 删除按钮
  512. .file-item .delete-btn {
  513. margin-left: 10px;
  514. color: #909399;
  515. cursor: pointer;
  516. font-size: 16px;
  517. padding: 4px;
  518. border-radius: 4px;
  519. transition: all 0.3s ease;
  520. &:not(.disabled-text):hover {
  521. color: #f56c6c;
  522. background-color: rgba(245, 108, 108, 0.1);
  523. }
  524. &.disabled-text {
  525. cursor: not-allowed;
  526. opacity: 0.6;
  527. }
  528. }
  529. // 表格形式文件列表
  530. .file-list-table {
  531. margin-top: 10px;
  532. background-color: #fff;
  533. border-radius: 6px;
  534. overflow: hidden;
  535. }
  536. // 表格底部按钮
  537. .table-footer {
  538. margin-top: 20px;
  539. text-align: right;
  540. }
  541. </style>