单文件上传使用说明
管理端普通文件 / 图片 / 小体积批量文件通过 admin/src/components/upload/ 下两个组件上传,都走后端 POST /api/admin/common/upload/file 落盘 + checkInstant 秒传预检。业务不要自己写 axios 上传。
| 场景 | 组件 |
|---|---|
| 用户点击按钮触发(表单里的"上传头像"等) | components/upload(默认插槽作为触发区) |
| 拖拽区 + 选完自动开始上传(素材库) | components/upload/dropzone |
大文件 / 需要分片 / 需要中断,请改用 分片上传。 后端存储/接口详见 后端-存储引擎。业务表只保存
file_hash_id(统一文件哈希主键),访问地址通过/api/uploads/<id>动态拼装。
一、共同流程
两个组件内部完全同链路,仅 UI 与触发时机不同:
选择文件 → SparkMD5(10MB 分块) → POST /common/upload/checkInstant 秒传预检
├─ 命中:直接返回 { file_hash_id, url, ... },不再上传
└─ 未命中:FormData → POST /common/upload/file,onUploadProgress 上报进度- 鉴权统一走请求头
token: <JWT>+version: <config.version>,不是Authorization。 - 回传结构与
el-upload一致:emit('change', [{ name, response }]),response.data即后端CommonUploadFileResp(file_hash_id / name / ext / size / url)。 - 任一文件失败:
feedback.msgError+emit('error')中断后续。
二、components/upload
默认插槽作为触发区,点击后弹出文件选择;可选 showProgress 打开上传进度对话框(每个文件独立进度条 + 秒传/失败标签)。
Props
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
url | string | '' | 相对路径会拼 config.baseUrl + config.urlPrefix;空则用默认 /common/upload/file;以 http 开头视为绝对 URL 直接使用 |
ext | string[] | [] | 允许扩展名(无点),如 ['png','jpg'],生成 <input accept> |
multiple | boolean | true | 是否允许多选 |
limit | number | 10 | 多选时上限,超出会直接拒收整批并提示 |
data | object | {} | 随文件一起提交的业务字段(追加到 FormData) |
showProgress | boolean | false | 是否弹出上传进度对话框 |
Emits / Expose
change(fileLists: { name, response }[])— 全部成功或秒传命中。error()— 任一文件失败即触发。defineExpose({ triggerSelect })— 外部可编程触发文件选择。
用法
vue
<template>
<upload :ext="['png','jpg']" :multiple="false" @change="onUploaded">
<el-button>选择头像</el-button>
</upload>
</template>
<script setup lang="ts">
import Upload from '@/components/upload/index.vue'
const onUploaded = (list: any[]) => {
const r = list[0]?.response?.data // 统一后端结构
form.avatar = r?.file_hash_id ?? '' // 业务表存哈希 ID
}
</script>三、components/upload/dropzone(拖拽版)
外层是虚线拖拽区,选完自动开始上传;无进度弹窗,改为内嵌进度条(多文件时累加计算总进度)。
Props / Emits
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
ext | string[] | [] | 允许扩展名,生成 <input accept> |
multiple | boolean | true | 是否允许多选 |
limit | number | 10 | 上限;0 视作无上限(内部用 999);超出会截断到上限继续上传(与 upload 拒收整批不同) |
change(fileLists)/error()语义与upload完全一致。
用法(素材库案例)
vue
<!-- components/material/picker.vue 内 -->
<UploadDropzone :ext="['png','jpg','gif','webp']" @change="handleUpload" />ts
import UploadDropzone from '@/components/upload/dropzone.vue'四、后端接口
| 用途 | 方法 | 路径 | 请求 |
|---|---|---|---|
| 秒传预检 | POST | /api/admin/common/upload/checkInstant | { file_md5, file_name } |
| 上传落盘 | POST | /api/admin/common/upload/file | multipart/form-data,字段 file + data 追加项 |
| 文件流访问 | GET | /api/uploads/<file_hash_id>[/<fileName>] | 自动 MIME + 长缓存 |
鉴权:请求头 token: <JWT>;管理端接口路径带 /api/admin 前缀,由 config.urlPrefix 决定。
五、常见坑
- 业务表存
file_hash_id,别存 URL:URL 由ossDomain + /api/uploads/<id>动态拼(见stores/modules/app.ts),换域名/协议时不用回补数据。 limit语义差异:upload超出直接拒收整批并msgError终止;dropzone是截断到上限继续上传。批量场景按 UI 需要选组件。- 重复选择同一文件:
upload在handleFileChange末尾会input.value = ''清空,允许再次选同名文件;自定义触发时若绕过它,可能选不中同一文件。 - MD5 计算占用主线程:
SparkMD5走FileReader.readAsArrayBuffer,几百 MB 图片会明显卡顿;极限优化把两个组件的calcMD5迁到 Web Worker 即可。 - "取消"不是真中断:
upload关进度弹窗只是uploadSeq++,让在途任务在下一个检查点静默退出(已 in-flight 的 axios 请求仍会跑完,结果不再emit);dropzone无取消入口。若需要真中断,改用 分片上传(Upload.abort())。 - 秒传前提是历史曾成功上传过同 MD5:命中后不写文件、只回哈希 ID;首次上传的 MD5 由后端落
x_common_file_hash表,不需要业务侧显式登记。
六、相关代码位置
- 前端组件:
admin/src/components/upload/index.vueadmin/src/components/upload/dropzone.vueadmin/src/utils/auth.ts(getToken)、admin/src/config/index.ts(baseUrl / urlPrefix / version)
- 后端接口:
server/app/controller/admin_ctl/common_controller/upload.go;驱动与存储:server/plugin/storage/。 - 现网调用点:
admin/src/components/material/picker.vue(UploadDropzone)。