# kex-file-uploader
附件上传组件(uni-app **Vue3**):可选图片、PDF、Office 等任意文件,支持列表展示、进度上传、自定义 UI。
支持:**H5 / App(vue 页)/ 微信小程序 / 支付宝小程序 / 鸿蒙** 等主流端。
---
## 目录
1. [功能一览](#功能一览)
2. [安装](#安装)
3. [三种用法怎么选](#三种用法怎么选)
4. [快速上手](#快速上手)
5. [完整示例](#完整示例)
6. [Props](#props)
7. [Events](#events)
8. [方法 ref](#方法-ref)
9. [插槽](#插槽)
10. [v-model 数据结构](#v-model-数据结构)
11. [上传与响应解析](#上传与响应解析)
12. [多端说明](#多端说明)
13. [常见问题](#常见问题)
14. [注意事项](#注意事项)
15. [版本](#版本)
---
## 功能一览
| 能力 | 说明 |
| --- | --- |
| 任意文件 | 图片 / 文档 / 压缩包等;`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` |
---
## 安装
将插件拷贝到业务项目:
```text
你的项目/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. 只选文件(不上传)
```vue
```
### 2. 自动上传(最少只填接口)
`name` / `header` / `formData` 有默认值,只配 `upload-url` 即可发起 `uni.uploadFile`。
```vue
```
### 3. 带 Token / 业务字段
```vue
```
### 4. 限制类型、大小、多选
```vue
```
### 5. 删除前确认
```vue
```
### 6. 只读回显
```vue
```
### 7. 自定义选择按钮(保留默认列表)
```vue
上传材料 {{ count }}/{{ maxCount }}
```
### 8. 自定义列表行(保留组件结构)
```vue
{{ file.name }} · {{ file.status }}
取消
重试
删除
```
### 9. 外部打开 + 完全自定义列表(headless)
无内置按钮、无内置列表;组件可藏在页面任意处。
```vue
{{ f.name }} · {{ f.status }} {{ f.progress }}%
取消
重试
删除
```
### 10. 手动上传(先选后传)
```vue
```
---
## 完整示例
业务页常见写法(自动上传 + Token + 确认删除 + 事件):
```vue
```
提交表单时,一般只取已成功项的远程地址:
```js
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) |
```js
{
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`
```js
{
files, // 本次新增:与 v-model 单项同结构
count, // 本次新增数量
fileList // 当前全部
}
```
### `@uploadSuccess`
```js
{
index,
file, // 当前文件
url, // 优先为解析出的远程地址
statusCode,
data, // 解析后的服务端 JSON(失败则为原始字符串)
res, // 原始回调
fileList // 当前全部
}
```
### `@change`
```js
{
fileList // 当前全部文件
}
```
---
## 方法(ref)
```vue
```
| 方法 | 说明 |
| --- | --- |
| `choose()` | 打开系统选文件 |
| `chooseFile({ success, fail, count, ... })` | 回调式选文件(可带 count/formats/size 等) |
| `upload(index)` | 上传指定下标 |
| `upload({ url, file, success, fail, onprogress, ... })` | 命令式上传单文件 |
| `upload()` / `uploadAll()` | 上传所有 `ready` / `fail` 项(受 `concurrent` 控制) |
| `abort(index)` | 取消指定上传中的项 |
| `abortAll()` | 取消全部上传中 |
| `clear()` | 清空列表(会先 abort) |
| `getFileList()` | 返回当前列表副本 |
| `openDocument({ file, success, fail })` | 打开文档预览 |
| `getTempFilePath` / `getBase` / `getArrayBuffer` | 读临时路径 / Base64 / ArrayBuffer |
```js
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,
onprogress: (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 数据结构
每一项大致为:
```js
{
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`。
远程地址解析优先级:
1. **`parseResponse`**(最高)
2. **`responseUrlKey`**(如 `data.url`、`data.fileUrl`)
3. 内置字段:`url` / `path` / `data.url` / `data.path` / `data.fileUrl` / `result.url`
```vue
```
也可在 `@uploadSuccess` 里用 `e.data` 自行处理,再改 `v-model`。
---
## 多端说明
| 端 | 选文件方式 | 上传 | 多选 |
| --- | --- | --- | --- |
| H5 | `uni.chooseFile` | `uni.uploadFile` | ✅ |
| 微信 / QQ | `chooseMessageFile`(聊天会话) | `uni.uploadFile` | ✅ |
| 支付宝 | 图片相册 / 其它 `chooseFileFromDisk` | `uni.uploadFile` | 本地多为单选 |
| **Android / iOS App** | renderjs `` | 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:如何判断都传完了?**
```js
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` 中设置:
```json
"uniStatistics": { "enable": false }
```
然后重新编译。
---
## 注意事项
1. **仅 Vue3** + `uni_modules` easycom。
2. **`formData` 的值必须是字符串**(uni 规范),如 `folderId: '123'`。
3. **自动上传**需同时:`autoUpload === true` 且 `uploadUrl` 非空。
4. **`mode="headless"`** 不渲染列表/按钮,请自行 `v-model` + `ref` 方法。
5. **Android/iOS App 用 vue 页**(renderjs),不要用 nvue;**鸿蒙**用 `uni.chooseFile`,无需 renderjs。
6. 删除:内置按钮走 `beforeRemove` + `@remove`;headless 下自己改数组时不会走 `beforeRemove`,需业务自行确认。
7. 各端对「任意文件」支持程度不同,以真机为准;`formats` 为空时组件不做类型拦截。
---
## 版本
当前能力见同目录 [changelog.md](./changelog.md)。