UploadComponent.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  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 class="el-icon-document"></i>
  88. <span class="file-fileName" @click="handlePreview(file)">
  89. {{ file.fileName }}
  90. </span>
  91. <span
  92. class="delete-btn"
  93. :disabled="isDisabled"
  94. :class="{ 'disabled-text': isDisabled }"
  95. @click="removeFile(index)"
  96. >
  97. <i class="el-icon-close"></i>
  98. </span>
  99. <!-- 状态标签 -->
  100. <span style="margin-left: 10px">
  101. <el-tag size="mini" :type="getStatusType(file.status)">
  102. {{ getStatusText(file.status) }}
  103. </el-tag>
  104. </span>
  105. </div>
  106. </div>
  107. <!-- 表格形式(超过3个) -->
  108. <!-- <div v-else class="file-list-table">
  109. <el-table :data="files" style="width: 100%">
  110. <el-table-column type="index" label="序号" width="60" />
  111. <el-table-column prop="fileName" label="附件名称" />
  112. <el-table-column prop="type" label="附件类型" width="100">
  113. <template slot-scope="scope">
  114. {{ scope.row.fileExtension || '' }}
  115. </template>
  116. </el-table-column>
  117. <el-table-column prop="fileSize" label="附件大小" width="100" />
  118. <el-table-column label="状态" width="80">
  119. <template slot-scope="scope">
  120. <el-tag size="mini" :type="getStatusType(scope.row.status)">
  121. {{ getStatusText(scope.row.status) }}
  122. </el-tag>
  123. </template>
  124. </el-table-column>
  125. <el-table-column label="操作" width="200">
  126. <template slot-scope="scope">
  127. <el-button size="mini" @click="handleSort(scope.$index, 'up')">
  128. </el-button>
  129. <el-button size="mini" @click="handleSort(scope.$index, 'down')">
  130. </el-button>
  131. <el-button
  132. size="mini"
  133. type="danger"
  134. @click="removeFile(scope.$index)"
  135. >
  136. 删除
  137. </el-button>
  138. </template>
  139. </el-table-column>
  140. </el-table> -->
  141. <!-- </div> -->
  142. <!-- 底部按钮(仅当超过3个时显示) -->
  143. <!-- <div v-if="files.length > 3" class="table-footer">
  144. <el-button type="primary" size="small" @click="saveFiles">保存</el-button>
  145. <el-button size="small">关闭</el-button>
  146. </div> -->
  147. </div>
  148. </template>
  149. <script>
  150. import { uploadFile } from '@/api/file'
  151. export default {
  152. name: 'UploadComponent',
  153. props: {
  154. filesList: {
  155. type: Array,
  156. default: () => [],
  157. },
  158. uploadMode: {
  159. type: String,
  160. default: 'single', // single 或 multiple
  161. },
  162. actionUrl: {
  163. type: String,
  164. default: '', // 上传路径
  165. },
  166. businessFolder: {
  167. type: String,
  168. default: '',
  169. },
  170. buttonText: {
  171. type: String,
  172. default: '上传',
  173. },
  174. maxSize: {
  175. type: Number,
  176. default: 50 * 1024 * 1024, // 默认50MB
  177. validator(value) {
  178. return value >= 0
  179. },
  180. },
  181. allowedTypes: {
  182. type: Array,
  183. default: () => ['xlsx', 'xls', 'doc', 'docx', 'pdf'],
  184. },
  185. isDisabled: {
  186. type: Boolean,
  187. default: false,
  188. },
  189. props: {
  190. type: Object,
  191. default: () => ({}),
  192. },
  193. },
  194. data() {
  195. return {
  196. files: [],
  197. uploadList: [],
  198. uploadUrl: '/api/file/v1/upload', // 单文件上传地址
  199. uploading: false,
  200. confirmDialogVisible: false,
  201. }
  202. },
  203. computed: {
  204. // 计算属性:判断上传按钮是否应该禁用
  205. isUploadDisabled() {
  206. // 只在单文件上传模式下检查
  207. if (this.uploadMode === 'single') {
  208. // 如果已经有文件(无论是上传的还是回显的),则禁用上传按钮
  209. return this.files.length > 0 || this.uploadList.length > 0
  210. }
  211. // 多文件上传模式下不禁用
  212. return false
  213. },
  214. accept() {
  215. let allowedTypes = this.allowedTypes.map((type) => {
  216. return '.' + type
  217. })
  218. return allowedTypes.join(',')
  219. },
  220. },
  221. watch: {
  222. filesList: {
  223. handler(newVal) {
  224. this.files = newVal.map((item) => {
  225. // 如果 item 是字符串(文件路径)
  226. if (typeof item === 'string') {
  227. return {
  228. fileName: item.substring(item.lastIndexOf('/') + 1),
  229. filePath: item,
  230. status: 'success',
  231. }
  232. }
  233. // 如果 item 已经是对象
  234. return {
  235. ...item,
  236. status: item.status || 'success',
  237. }
  238. })
  239. },
  240. immediate: true, // 立即执行一次,用于初始化回显
  241. },
  242. actionUrl: {
  243. handler(newVal) {
  244. this.uploadUrl = newVal || '/api/file/v1/upload'
  245. },
  246. immediate: true,
  247. },
  248. },
  249. methods: {
  250. // 在 methods 中添加格式化方法
  251. formatFileSize(bytes) {
  252. if (bytes === 0) return '0 Bytes'
  253. const k = 1024
  254. const sizes = ['Bytes', 'KB', 'MB', 'GB']
  255. const i = Math.floor(Math.log(bytes) / Math.log(k))
  256. return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
  257. },
  258. showConfirmDialog() {
  259. this.confirmDialogVisible = true
  260. },
  261. clearFiles() {
  262. this.files = []
  263. this.uploadList = []
  264. },
  265. handleChange(file, fileList) {
  266. // 校验文件大小
  267. if (this.maxSize && file.size > this.maxSize) {
  268. this.$message.error(
  269. `文件大小不能超过 ${this.formatFileSize(this.maxSize)}`
  270. )
  271. this.uploadList = this.uploadList.filter((f) => f !== file)
  272. return
  273. }
  274. // 校验文件类型
  275. const fileName = file.name.toLowerCase()
  276. const fileExtension = fileName.split('.').pop()
  277. if (!this.allowedTypes.includes(fileExtension)) {
  278. this.$message.error(
  279. `不支持的文件格式,请上传 ${this.allowedTypes.join(
  280. ', '
  281. )} 格式的文件`
  282. )
  283. this.uploadList = this.uploadList.filter((f) => f !== file)
  284. return
  285. }
  286. this.uploadList = fileList
  287. this.confirmUpload()
  288. // 根据上传模式决定是否立即上传
  289. // if (this.uploadMode === 'single') {
  290. // this.confirmUpload()
  291. // }
  292. },
  293. confirmUpload() {
  294. if (this.uploading) return
  295. this.uploading = true
  296. const formData = new FormData()
  297. formData.append('file', this.uploadList[0].raw)
  298. // if (this.uploadMode === 'single' && this.uploadList.length > 0) {
  299. // formData.append('file', this.uploadList[0].raw)
  300. // } else if (this.uploadMode === 'multiple') {
  301. // // 多文件上传模式
  302. // // 正确的多文件上传方式:为每个文件单独添加到formData中,使用相同的键名
  303. // this.uploadList.forEach((file) => {
  304. // formData.append('files', file.raw)
  305. // })
  306. // }
  307. // 发送上传请求
  308. this.uploadFiles(formData)
  309. },
  310. async uploadFiles(formData) {
  311. try {
  312. this.uploading = true
  313. // 注意:根据实际情况选择合适的API调用方式
  314. const res = await uploadFile(this.uploadUrl, formData, {
  315. onUploadProgress: (progressEvent) => {
  316. // 可添加上传进度显示
  317. if (progressEvent.total) {
  318. const percentComplete = Math.round(
  319. (progressEvent.loaded * 100) / progressEvent.total
  320. )
  321. console.log('上传进度:', percentComplete, '%')
  322. // 如需显示进度条可在此添加逻辑
  323. }
  324. },
  325. timeout: 60000, // 添加60秒超时设置
  326. })
  327. if (res && res.value) {
  328. if (this.uploadUrl == '/api/file/v1/tempUpload') {
  329. this.files.push({
  330. ...res.value,
  331. ...res.value.fileUploadResult,
  332. status: 'success',
  333. })
  334. } else {
  335. this.files.push({
  336. ...res.value,
  337. status: 'success',
  338. })
  339. }
  340. // if (this.uploadMode === 'single') {
  341. // this.files.push({
  342. // ...res.value,
  343. // status: 'success',
  344. // })
  345. // } else {
  346. // // 多文件上传模式
  347. // res.value.forEach((item) => {
  348. // this.files.push({
  349. // ...item,
  350. // status: 'success',
  351. // })
  352. // })
  353. // }
  354. this.uploadList = []
  355. this.$message.success('上传成功')
  356. this.saveFiles() // 自动保存上传结果
  357. } else {
  358. throw new Error('上传返回数据格式不正确')
  359. }
  360. } catch (error) {
  361. console.error('文件上传失败:', error)
  362. // 重置上传状态,确保用户可以再次尝试
  363. this.uploadList = []
  364. } finally {
  365. this.uploading = false
  366. }
  367. },
  368. removeFile(index) {
  369. if (this.isDisabled) return
  370. // 删除文件
  371. const removedFile = this.files.splice(index, 1)[0]
  372. // 通知父组件文件已被删除,并传递当前文件列表
  373. this.$emit('removeFile', index, removedFile, this.files, this.props)
  374. },
  375. handleSort(index, direction) {
  376. if (direction === 'up' && index > 0) {
  377. this.files.splice(index - 1, 0, this.files.splice(index, 1)[0])
  378. } else if (direction === 'down' && index < this.files.length - 1) {
  379. this.files.splice(index + 1, 0, this.files.splice(index, 1)[0])
  380. }
  381. },
  382. getStatusText(status) {
  383. const map = {
  384. pending: '待上传',
  385. success: '已上传',
  386. uploading: '上传中',
  387. failed: '上传失败',
  388. }
  389. return map[status] || status
  390. },
  391. getStatusType(status) {
  392. const map = {
  393. pending: 'warning',
  394. success: 'success',
  395. uploading: 'info',
  396. failed: 'danger',
  397. }
  398. return map[status] || 'default'
  399. },
  400. // 添加获取文件扩展名的方法
  401. getFileExtension(fileName) {
  402. // 从文件名中提取扩展名并转为小写
  403. const lastDotIndex = fileName.lastIndexOf('.')
  404. if (lastDotIndex > -1 && lastDotIndex < fileName.length - 1) {
  405. return fileName.substring(lastDotIndex + 1).toLowerCase()
  406. }
  407. return 'unknown' // 如果没有扩展名,返回unknown
  408. },
  409. // 保存文件列表
  410. saveFiles() {
  411. this.$emit('saveFiles', this.files, this.props)
  412. this.confirmDialogVisible = false
  413. },
  414. handlePreview(file) {
  415. // 对文件URL进行Base64编码
  416. const encodedUrl = encodeURIComponent(
  417. Base64.encode(window.context.form + file.filePath)
  418. )
  419. // 构建 kkFileView 预览URL
  420. // onlinePreview - 在线预览
  421. // onlinePreview?type=pdf - 强制使用PDF模式预览
  422. window.open(`${host}:8012/onlinePreview?url=${encodedUrl}`)
  423. },
  424. },
  425. }
  426. </script>
  427. <style scoped lang="scss">
  428. @import '@/styles/costAudit.scss';
  429. .upload-buttons {
  430. display: flex;
  431. gap: 10px;
  432. margin-bottom: 20px;
  433. }
  434. .file-list-simple {
  435. margin-bottom: 0px;
  436. }
  437. .file-item {
  438. display: flex;
  439. align-items: center;
  440. padding: 8px 0;
  441. border-bottom: 1px dashed #ebeef5;
  442. font-size: 14px;
  443. }
  444. .file-item .delete-btn {
  445. margin-left: auto;
  446. color: #f56c6c;
  447. cursor: pointer;
  448. }
  449. .file-list-table {
  450. margin-top: 10px;
  451. }
  452. .table-footer {
  453. margin-top: 20px;
  454. text-align: right;
  455. }
  456. .upload-container {
  457. margin-top: 0px !important;
  458. }
  459. .file-fileName {
  460. &:hover {
  461. color: $base-color-default;
  462. cursor: pointer;
  463. }
  464. }
  465. </style>