更新记录
1.2.0(2026-07-16) 下载此版本
- 支持鸿蒙:
uni.chooseFile+uni.uploadFile(非 renderjs);package.json标记 harmony √ - 新增:
debug/distinct、uploadOptions.method/toBase、JSON body、withCredentials - 新增:
chooseFile、命令式upload({url,file,...})、openDocument、getTempFilePath/getBase/getArrayBuffer - 文档:多端表拆分 Android/iOS vs 鸿蒙
平台兼容性
uni-app(3.8.0)
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| √ | √ | √ | √ | √ | - | √ | √ | √ |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| √ | - | - | - | - | - | √ | - | - | - | - | - |
kex-file-uploader
附件上传组件(uni-app Vue3):可选图片、PDF、Office 等任意文件,支持列表展示、进度上传、自定义 UI。
支持:H5 / App(vue 页)/ 微信小程序 / 支付宝小程序 / 鸿蒙 等主流端。
目录
功能一览
| 能力 | 说明 |
|---|---|
| 任意文件 | 图片 / 文档 / 压缩包等;formats 为空则不限制(后端不拦即可) |
| 单次多选 | multiple 默认 true,受 maxCount 剩余名额限制 |
| 内置列表 | 文件名、大小、进度、删除、重试、取消;图片缩略图 |
| 自定义按钮 | #trigger 完全自定义,或 show-trigger=false + 外部按钮 |
| 自定义列表 | #item 改每一行,或 mode="headless" 完全自绘 |
| 自动 / 手动上传 | autoUpload + uploadUrl;也可 ref.uploadAll() |
| 上传配置 | uploadOptions:name / header / formData(有默认值,只填 url 也能传) |
| 并发 / 取消 | concurrent(默认 3)、abort / abortAll |
| 响应解析 | responseUrlKey / parseResponse |
| 只读 / 确认删 | readonly、beforeRemove |
安装
将插件拷贝到业务项目:
你的项目/uni_modules/kex-file-uploader/
uni-app easycom 会自动注册,页面里直接写标签,无需 import。
要求:
- uni-app Vue3
- App 请使用 vue 页面(不要用 nvue;App 选任意文件依赖 renderjs)
演示页(本示例工程):首页 → kex-file-uploader,或路径 /pages/demos/file-uploader/index。
三种用法怎么选
| 场景 | 写法 | 内置按钮 | 内置列表 |
|---|---|---|---|
| 开箱即用 | 默认 mode="list" |
✅ | ✅ |
| 按钮自己画,列表用默认 | :show-trigger="false" + ref.choose() |
❌ | ✅ |
| 按钮和列表都自己画 | mode="headless" + ref.choose() + 自己 v-for |
❌ | ❌ |
show-trigger="false" / headless 时,组件可放在页面任意位置,只要 ref 能调到即可。
其它 Props(uploadUrl、multiple、maxCount、formats 等)在三种模式下都生效。
快速上手
1. 只选文件(不上传)
<template>
<kex-file-uploader v-model="files" :max-count="5" />
</template>
<script setup>
import { ref } from 'vue'
const files = ref([])
</script>
2. 自动上传(最少只填接口)
name / header / formData 有默认值,只配 upload-url 即可发起 uni.uploadFile。
<template>
<kex-file-uploader
v-model="files"
:auto-upload="true"
upload-url="https://example.com/api/upload"
response-url-key="data.url"
@uploadSuccess="onOk"
@uploadFail="onFail"
/>
</template>
<script setup>
import { ref } from 'vue'
const files = ref([])
const onOk = (e) => console.log('成功', e.url, e.data)
const onFail = (e) => console.log('失败', e.err)
</script>
3. 带 Token / 业务字段
<script setup>
import { ref, computed } from 'vue'
const files = ref([])
const token = ref('Bearer xxx')
const uploadOptions = computed(() => ({
// name 默认 'file',可不写
header: { Authorization: token.value },
// 值必须是字符串
formData: { folderId: '123', module: 'attach' }
}))
</script>
<template>
<kex-file-uploader
v-model="files"
:auto-upload="true"
upload-url="https://example.com/api/upload"
:upload-options="uploadOptions"
/>
</template>
4. 限制类型、大小、多选
<!-- 单次可多选,列表最多 9 个;仅 pdf/word;单文件 ≤ 10MB -->
<kex-file-uploader
v-model="files"
:max-count="9"
:multiple="true"
:max-size="10"
formats="pdf,doc,docx"
/>
<!-- 单次只能选 1 个,仍可多次点选直到 maxCount -->
<kex-file-uploader v-model="files" :max-count="9" :multiple="false" />
5. 删除前确认
<script setup>
const beforeRemove = ({ file }) =>
new Promise((resolve) => {
uni.showModal({
title: '确认删除',
content: `删除「${file.name}」?`,
success: (r) => resolve(!!r.confirm),
fail: () => resolve(false)
})
})
</script>
<template>
<kex-file-uploader v-model="files" :before-remove="beforeRemove" />
</template>
6. 只读回显
<script setup>
const files = ref([
{ name: '合同.pdf', url: 'https://cdn.example.com/a.pdf', size: 102400, status: 'success' }
])
</script>
<template>
<kex-file-uploader v-model="files" :readonly="true" />
</template>
7. 自定义选择按钮(保留默认列表)
<!-- 方式 A:插槽完全替换按钮 UI -->
<kex-file-uploader v-model="files" :max-count="5">
<template #trigger="{ choose, count, maxCount }">
<view class="my-btn" @click="choose">上传材料 {{ count }}/{{ maxCount }}</view>
</template>
</kex-file-uploader>
<!-- 方式 B:隐藏内置按钮,外部任意位置调用 -->
<kex-file-uploader ref="fu" v-model="files" :show-trigger="false" />
<button @click="fu.choose()">选择附件</button>
8. 自定义列表行(保留组件结构)
<kex-file-uploader v-model="files">
<template #item="{ file, remove, retry, abort, preview }">
<view @click="preview">
<text>{{ file.name }} · {{ file.status }}</text>
<text v-if="file.status === 'uploading'" @click.stop="abort">取消</text>
<text v-else-if="file.status === 'fail'" @click.stop="retry">重试</text>
<text v-else @click.stop="remove">删除</text>
</view>
</template>
</kex-file-uploader>
9. 外部打开 + 完全自定义列表(headless)
无内置按钮、无内置列表;组件可藏在页面任意处。
<template>
<kex-file-uploader
ref="fu"
v-model="files"
mode="headless"
:max-count="9"
:multiple="true"
:auto-upload="true"
upload-url="https://example.com/api/upload"
:upload-options="uploadOptions"
@select=""
@uploadSuccess="onOk"
@uploadFail="onFail"
/>
<button @click="fu.choose()">选择附件</button>
<button @click="fu.uploadAll()">上传全部</button>
<button @click="fu.clear()">清空</button>
<view v-for="(f, i) in files" :key="f.uid || i">
<text>{{ f.name }} · {{ f.status }} {{ f.progress }}%</text>
<text v-if="f.status === 'uploading'" @click="fu.abort(i)">取消</text>
<text v-else-if="f.status === 'fail'" @click="fu.upload(i)">重试</text>
<text @click="files.splice(i, 1)">删除</text>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
const fu = ref(null)
const files = ref([])
const uploadOptions = computed(() => ({
header: { Authorization: 'Bearer xxx' }
}))
const = (e) => console.log('选中', e.count, e.fileList)
const onOk = (e) => console.log('成功', e.url, e.data)
const onFail = (e) => console.log('失败', e.err)
</script>
10. 手动上传(先选后传)
<template>
<kex-file-uploader
ref="fu"
v-model="files"
:auto-upload="false"
upload-url="https://example.com/api/upload"
/>
<button @click="fu.uploadAll()">开始上传</button>
</template>
完整示例
业务页常见写法(自动上传 + Token + 确认删除 + 事件):
<template>
<kex-file-uploader
v-model="files"
:max-count="9"
:max-size="20"
:multiple="true"
formats=""
:auto-upload="true"
upload-url="https://example.com/api/upload"
:upload-options="uploadOptions"
response-url-key="data.url"
:concurrent="3"
:before-remove="beforeRemove"
@select=""
@change="onChange"
@uploadSuccess="onOk"
@uploadFail="onFail"
@uploadAbort=""
@remove="onRemove"
/>
</template>
<script setup>
import { ref, computed } from 'vue'
const files = ref([])
const token = ref('') // 从登录态取值
const uploadOptions = computed(() => ({
name: 'file',
header: token.value ? { Authorization: token.value } : {},
formData: { module: 'attachment' }
}))
const beforeRemove = ({ file }) =>
new Promise((resolve) => {
uni.showModal({
title: '确认删除',
content: `删除「${file.name}」?`,
success: (r) => resolve(!!r.confirm)
})
})
const = (e) => console.log('选图', e.count)
const onChange = (e) => console.log('列表', e.fileList)
const onOk = (e) => console.log('成功', e.url, e.data)
const onFail = (e) => console.log('失败', e.err)
const = (e) => console.log('取消', e.file)
const onRemove = (e) => console.log('删除', e.index, e.file)
</script>
提交表单时,一般只取已成功项的远程地址:
const urls = files.value
.filter((f) => f.status === 'success' && f.url)
.map((f) => f.url)
Props
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
| modelValue / v-model | Array |
[] |
文件列表,见下方结构 |
| mode | String |
'list' |
list 带列表 UI;headless 无 UI 纯能力 |
| maxCount | Number |
9 |
列表最多文件数 |
| multiple | Boolean |
true |
单次能否多选;false 则每次只能选 1 个 |
| maxSize | Number |
20 |
单文件上限 MB;0 表示不限制 |
| formats | String \| Array |
'' |
允许扩展名,空=不限制。如 'pdf,png' 或 ['pdf','png'] |
| fileType | String |
'all' |
选文件类型提示:all / image / video / file |
| accept | String |
'' |
H5 / App 原生 input 的 accept,如 image/*、.pdf,.doc |
| disabled | Boolean |
false |
禁用选择 / 删除 / 上传 |
| readonly | Boolean |
false |
只读展示,不可选、不可删、不可传 |
| showRemove | Boolean |
true |
是否显示删除 |
| showCount | Boolean |
true |
默认按钮上是否显示数量 |
| showThumb | Boolean |
true |
图片是否显示缩略图 |
| addText | String |
'添加附件' |
默认选择按钮文案 |
| showTrigger | Boolean |
true |
是否显示内置选择按钮;false 时用外部 ref.choose() |
| triggerPosition | String |
'bottom' |
内置按钮位置:bottom / top |
| addClass | String \| Array \| Object |
'' |
默认按钮额外 class |
| addStyle | Object \| String |
{} |
默认按钮内联样式 |
| toast | Boolean |
true |
超限等是否 toast |
| autoUpload | Boolean |
false |
选完是否自动上传 |
| uploadUrl | String |
'' |
上传接口(对应 uni.uploadFile 的 url) |
| uploadOptions | Object |
见下表 | 上传参数 |
| concurrent | Number |
3 |
并发上传数;≤0 按 1 处理 |
| responseUrlKey | String |
'' |
从响应取远程 url 的路径,如 data.url、data.fileUrl |
| parseResponse | Function |
null |
(raw, res) => url \| { url, data },优先级最高 |
| beforeRemove | Function |
null |
({ index, file }) => boolean \| Promise,返回 false 拦截删除 |
uploadOptions
对应 uni.uploadFile,未传字段自动补默认,只填 uploadUrl 也能上传:
| 字段 | 默认 | 说明 |
|---|---|---|
| name | 'file' |
文件字段名 |
| header | {} |
请求头(可用 computed 动态带 Token) |
| formData | {} |
额外表单字段;值必须是字符串 |
| method | 'POST' |
App(Android/iOS)XHR 方法;其它端仍走 uni.uploadFile(POST) |
| toBase | false |
App:以 Base64 字符串作为字段值上传 |
| withCredentials | false |
App XHR 是否跨域携带 cookie(需服务端 CORS 允许 credentials) |
{
name: 'file',
header: { Authorization: 'Bearer xxx' },
formData: { bizId: '1', type: 'attach' },
method: 'POST',
// toBase: true, // App 需要 Base64 字段时
withCredentials: true // iOS 本地测远程且要带 cookie
}
另有 props:debug(日志)、distinct(同名覆盖,默认重命名)。
关于跨域(CORS)
跨域需服务端配置 CORS,组件侧可配合 withCredentials:
- Android/iOS App:webview XHR → 需服务端 CORS。
- 鸿蒙 / H5 / 小程序:
uni.uploadFile,同样要服务端允许跨域。 - 带 cookie:
uploadOptions.withCredentials: true+ 服务端Access-Control-Allow-Credentials: true(Origin 不能是*)。
Events
| 事件 | 触发时机 | 回调参数 |
|---|---|---|
| update:modelValue | 列表变化 | fileList(同 v-model) |
| change | 列表任意变化 | { fileList, ... } |
| select | 每次选文件完成 | 见下方 |
| uploadSuccess | 单文件上传成功 | 见下方 |
| uploadFail | 单文件上传失败 | { index, file, err, statusCode?, data?, res? } |
| uploadAbort | 取消上传 | { index, file } |
| remove | 删除(内置删除按钮) | { index, file } |
| oversize | 单文件超体积 | { name, path, size, maxSize } |
| exceed | 超过 maxCount | { maxCount } |
| preview | 点击预览 | { file } |
@select
{
files, // 本次新增:与 v-model 单项同结构
count, // 本次新增数量
fileList // 当前全部
}
@uploadSuccess
{
index,
file, // 当前文件
url, // 优先为解析出的远程地址
statusCode,
data, // 解析后的服务端 JSON(失败则为原始字符串)
res, // 原始回调
fileList // 当前全部
}
@change
{
fileList // 当前全部文件
}
方法(ref)
<kex-file-uploader ref="fu" v-model="files" upload-url="..." />
| 方法 | 说明 |
|---|---|
choose() |
打开系统选文件 |
chooseFile({ success, fail, count, ... }) |
回调式选文件(可带 count/formats/size 等) |
upload(index) |
上传指定下标 |
upload({ url, file, success, fail, , ... }) |
命令式上传单文件 |
upload() / uploadAll() |
上传所有 ready / fail 项(受 concurrent 控制) |
abort(index) |
取消指定上传中的项 |
abortAll() |
取消全部上传中 |
clear() |
清空列表(会先 abort) |
getFileList() |
返回当前列表副本 |
openDocument({ file, success, fail }) |
打开文档预览 |
getTempFilePath / getBase / getArrayBuffer |
读临时路径 / Base64 / ArrayBuffer |
fu.value.choose()
fu.value.uploadAll()
fu.value.abort(0)
fu.value.clear()
console.log(fu.value.getFileList())
// 回调式选文件 / 命令式上传
fu.value.chooseFile({
count: 3,
success: (files) => console.log(files)
})
fu.value.upload({
url: 'https://xxx/upload',
file: files[0],
header: { Authorization: 'Bearer xxx' },
success: (e) => console.log(e.result),
fail: console.error,
: (e) => console.log(e.progress)
})
插槽
| 插槽 | 说明 | 作用域参数 |
|---|---|---|
trigger |
完全自定义选择按钮(推荐);达上限或 disabled/readonly 时不渲染 | choose、count、maxCount、disabled、readonly |
add |
只替换默认虚线框里的内容 | count、maxCount |
item |
完全自定义每一行列表 | file、index、remove、retry、abort、preview |
点击 trigger 里请调用插槽传入的 choose()(或外部 ref.choose())。
v-model 数据结构
每一项大致为:
{
uid: 'kex-fu-...', // 内部唯一 id
name: '报告.pdf',
url: 'https://...', // 成功后优先为远程地址;App 选中未上传时可能为空
path: '', // 本地路径(上传用;App 选任意文件时可能为空)
size: 102400, // 字节
ext: 'pdf',
status: 'success', // ready | uploading | success | fail
progress: 100, // 0~100
message: '' // 失败等原因文案
}
| status | 含义 |
|---|---|
| ready | 已选中,待上传 |
| uploading | 上传中 |
| success | 成功(回显远程文件也是此状态) |
| fail | 失败或已取消,可重试 |
说明:
- App 选图会额外在内部保留
thumb(缩略图 dataURL)用于展示,不会把大段 base64 写进v-model的url - 提交后端时建议只提交
status === 'success'的url/name等业务字段
上传与响应解析
底层:非 App 用 uni.uploadFile;App 用 renderjs + XMLHttpRequest。
成功条件:HTTP 状态码 2xx。
远程地址解析优先级:
parseResponse(最高)responseUrlKey(如data.url、data.fileUrl)- 内置字段:
url/path/data.url/data.path/data.fileUrl/result.url
<!-- 方式 A:路径取值 -->
<kex-file-uploader response-url-key="data.url" ... />
<!-- 方式 B:自定义函数 -->
<kex-file-uploader
:parse-response="(raw) => {
const d = typeof raw === 'string' ? JSON.parse(raw) : raw
return (d && d.data && d.data.fileUrl) || ''
}"
...
/>
也可在 @uploadSuccess 里用 e.data 自行处理,再改 v-model。
多端说明
| 端 | 选文件方式 | 上传 | 多选 |
|---|---|---|---|
| H5 | uni.chooseFile |
uni.uploadFile |
✅ |
| 微信 / QQ | chooseMessageFile(聊天会话) |
uni.uploadFile |
✅ |
| 支付宝 | 图片相册 / 其它 chooseFileFromDisk |
uni.uploadFile |
本地多为单选 |
| Android / iOS App | renderjs <input type="file"> |
renderjs XHR | ✅(个别安卓管理器可能单选) |
| 鸿蒙 App | uni.chooseFile(不走 renderjs) |
uni.uploadFile |
以系统能力为准 |
说明:uni-app 里 APP-PLUS 不含鸿蒙;鸿蒙走 #ifndef APP-PLUS 的选传链路。不要把「App = 必须 renderjs」理解成鸿蒙也要 renderjs。
微信选文件来自聊天记录、支付宝本地单选等,属于平台能力限制。
常见问题
Q:外部打开时,其它 props 还能用吗?
能。show-trigger="false" / headless 只影响 UI,上传、限制、多选、事件等全部照常。
Q:组件必须和按钮放在一起吗?
不必。组件放页面任意位置(甚至视觉上藏起来),按钮在别处调 ref.choose() 即可。注意不要把组件 v-if 掉,否则 ref 为空。
Q:为什么没有多选?
确认 multiple 为 true(默认)且 maxCount > 1。支付宝本地文件、部分安卓系统选择器可能仍只能单选,可多次点选凑满。
Q:App 真机图片只有「图」字、不能预览?
App 无本地 path 时用缩略图 + 内置全屏预览。需重新编译运行。
Q:如何判断都传完了?
const allDone = files.value.every(
(f) => f.status === 'success' || f.status === 'fail'
)
const allOk = files.value.length && files.value.every((f) => f.status === 'success')
Q:控制台一直刷「uni统计 上报超时」?
与本组件无关。在项目 manifest.json 中设置:
"uniStatistics": { "enable": false }
然后重新编译。
注意事项
- 仅 Vue3 +
uni_moduleseasycom。 formData的值必须是字符串(uni 规范),如folderId: '123'。- 自动上传需同时:
autoUpload === true且uploadUrl非空。 mode="headless"不渲染列表/按钮,请自行v-model+ref方法。- Android/iOS App 用 vue 页(renderjs),不要用 nvue;鸿蒙用
uni.chooseFile,无需 renderjs。 - 删除:内置按钮走
beforeRemove+@remove;headless 下自己改数组时不会走beforeRemove,需业务自行确认。 - 各端对「任意文件」支持程度不同,以真机为准;
formats为空时组件不做类型拦截。
版本
当前能力见同目录 changelog.md。

收藏人数:
下载插件并导入HBuilderX
下载示例项目ZIP
赞赏(0)
下载 8
赞赏 0
下载 12431794
赞赏 1934
赞赏
京公网安备:11010802035340号