index.vue 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. <template>
  2. <view class="upload-container">
  3. <u-upload
  4. :fileList="fileList"
  5. @afterRead="handleAfterRead"
  6. @delete="handleDelete"
  7. :name="name"
  8. :multiple="multiple"
  9. :maxCount="maxCount"
  10. :auto-upload="false"
  11. :show-progress="true"
  12. :sizeType="sizeType"
  13. :sourceType="sourceType"
  14. :accept="accept"
  15. previewFullImage
  16. >
  17. <view v-if="accept !== 'image'">
  18. <text class="file-icon">📎</text>
  19. <text>点击上传文件</text>
  20. </view>
  21. </u-upload>
  22. <!-- 文件列表区域,针对文件类型优化显示 -->
  23. <view class="file-list" v-if="fileList && fileList.length > 0 && accept !== 'image'">
  24. <view class="file-item" v-for="(file, index) in fileList" :key="index">
  25. <view class="file-info">
  26. <view class="file-icon">
  27. <text>{{ getFileIcon(file.fileName) }}</text>
  28. </view>
  29. <view class="file-details">
  30. <text class="file-name">{{ file.fileName || '未命名文件' }}</text>
  31. </view>
  32. </view>
  33. </view>
  34. </view>
  35. </view>
  36. </template>
  37. <script>
  38. import config from '@/config.js'; // 根据实际路径调整
  39. export default {
  40. name: 'UploadComponent',
  41. data() {
  42. return {
  43. // 上传URL
  44. uploadUrl: `${config.baseUrl}/common/upload`,
  45. fileList: []
  46. }
  47. },
  48. props: {
  49. // 组件标识名称
  50. name: {
  51. type: String,
  52. default: 'upload'
  53. },
  54. // 是否允许多文件
  55. multiple: {
  56. type: Boolean,
  57. default: false
  58. },
  59. // 最大文件数量
  60. maxCount: {
  61. type: Number,
  62. default: 10
  63. },
  64. // 文件类型限制
  65. accept: {
  66. type: String,
  67. default: 'image'
  68. },
  69. // 图片尺寸类型
  70. sizeType: {
  71. type: Array,
  72. default: () => ['original', 'compressed']
  73. },
  74. // 图片来源
  75. sourceType: {
  76. type: Array,
  77. default: () => ['album', 'camera']
  78. },
  79. // 额外的表单数据
  80. formData: {
  81. type: Object,
  82. default: () => ({})
  83. }
  84. },
  85. methods: {
  86. // 删除图片/文件
  87. handleDelete(event) {
  88. // 通知父组件删除文件
  89. this.$emit('delete', {
  90. name: this.name,
  91. index: event.index,
  92. file: this.fileList[event.index]
  93. });
  94. // 从本地列表中移除
  95. this.fileList.splice(event.index, 1);
  96. },
  97. // 新增图片/文件
  98. async handleAfterRead(event) {
  99. // 触发开始上传事件
  100. this.$emit('upload-start', {
  101. name: this.name,
  102. files: event.file
  103. });
  104. // 处理文件列表
  105. let lists = [].concat(event.file);
  106. // 更新UI显示上传中状态
  107. lists.forEach((item) => {
  108. // 添加上传中的文件到列表
  109. this.fileList.push({
  110. ...item,
  111. status: "uploading",
  112. message: "上传中"
  113. });
  114. this.$emit('file-added', {
  115. name: this.name,
  116. file: this.fileList[this.fileList.length - 1]
  117. });
  118. });
  119. try {
  120. // 逐个上传文件
  121. for (let i = 0; i < lists.length; i++) {
  122. const fileIndex = this.fileList.length - lists.length + i;
  123. const result = await this.uploadFilePromise(lists[i].url);
  124. this.fileList= [result]
  125. // 更新文件状态为成功
  126. this.fileList[fileIndex] = {
  127. ...this.fileList[fileIndex],
  128. ...result,
  129. status: "success",
  130. message: "上传成功"
  131. };
  132. this.$emit('file-updated', {
  133. name: this.name,
  134. index: fileIndex,
  135. file: this.fileList[fileIndex]
  136. });
  137. }
  138. // 触发上传完成事件
  139. this.$emit('upload-success', {
  140. name: this.name,
  141. files: lists
  142. });
  143. } catch (error) {
  144. // 标记上传失败
  145. lists.forEach((_, i) => {
  146. const fileIndex = this.fileList.length - lists.length + i;
  147. this.fileList[fileIndex].status = "error";
  148. this.fileList[fileIndex].message = "上传失败";
  149. });
  150. // 触发上传失败事件
  151. this.$emit('upload-error', {
  152. name: this.name,
  153. error: error
  154. });
  155. }
  156. },
  157. // 上传文件的Promise封装
  158. uploadFilePromise(url) {
  159. return new Promise((resolve, reject) => {
  160. uni.uploadFile({
  161. url: this.uploadUrl,
  162. filePath: url,
  163. name: "file",
  164. formData: {
  165. ...this.formData,
  166. },
  167. success: (res) => {
  168. try {
  169. // 尝试解析JSON响应
  170. const data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data;
  171. resolve(data.data || data);
  172. } catch (e) {
  173. // 如果解析失败,直接返回原始数据
  174. resolve(res.data);
  175. }
  176. },
  177. fail: (err) => {
  178. reject(err);
  179. }
  180. });
  181. });
  182. },
  183. // 根据文件扩展名获取对应的图标
  184. getFileIcon(filename) {
  185. if (!filename) return '📄';
  186. const ext = filename.split('.').pop().toLowerCase();
  187. // 根据文件类型返回不同的图标emoji
  188. if (['jpg', 'jpeg', 'png', 'gif', 'bmp'].includes(ext)) return '🖼️';
  189. if (['pdf'].includes(ext)) return '📑';
  190. if (['doc', 'docx'].includes(ext)) return '📝';
  191. if (['xls', 'xlsx'].includes(ext)) return '📊';
  192. if (['ppt', 'pptx'].includes(ext)) return '📈';
  193. if (['mp3', 'wav', 'flac'].includes(ext)) return '🎵';
  194. if (['mp4', 'mov', 'avi'].includes(ext)) return '🎬';
  195. if (['zip', 'rar', '7z'].includes(ext)) return '🗜️';
  196. return '📄';
  197. },
  198. // 格式化文件大小
  199. formatFileSize(bytes) {
  200. if (bytes === 0) return '0 B';
  201. const k = 1024;
  202. const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
  203. const i = Math.floor(Math.log(bytes) / Math.log(k));
  204. return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  205. }
  206. }
  207. }
  208. </script>
  209. <style lang="scss" scoped>
  210. .upload-container {
  211. .upload-hint {
  212. display: flex;
  213. flex-direction: column;
  214. align-items: center;
  215. justify-content: center;
  216. width: 80px;
  217. height: 80px;
  218. border: 1px dashed #d9d9d9;
  219. border-radius: 4px;
  220. background-color: #fafafa;
  221. color: #999;
  222. font-size: 12px;
  223. .file-icon {
  224. font-size: 24px;
  225. margin-bottom: 4px;
  226. }
  227. }
  228. .file-list {
  229. margin-top: 10px;
  230. .file-item {
  231. display: flex;
  232. align-items: center;
  233. padding: 8px 0;
  234. border-bottom: 1px solid #eee;
  235. .file-info {
  236. display: flex;
  237. align-items: center;
  238. flex: 1;
  239. .file-thumb {
  240. width: 32px;
  241. height: 32px;
  242. margin-right: 10px;
  243. border-radius: 4px;
  244. background-color: #f5f5f5;
  245. display: flex;
  246. align-items: center;
  247. justify-content: center;
  248. image {
  249. width: 100%;
  250. height: 100%;
  251. object-fit: cover;
  252. border-radius: 4px;
  253. }
  254. .file-icon {
  255. font-size: 20px;
  256. }
  257. }
  258. .file-details {
  259. .file-name {
  260. font-size: 14px;
  261. color: #333;
  262. max-width: 200px;
  263. overflow: hidden;
  264. text-overflow: ellipsis;
  265. white-space: nowrap;
  266. }
  267. .file-size {
  268. font-size: 12px;
  269. color: #999;
  270. }
  271. }
  272. }
  273. }
  274. }
  275. }
  276. </style>