feat: Upload上传组件新增支持上传视频和文件 (#412)

closes #186  and #336
This commit is contained in:
不如摸鱼去 2024-07-07 19:35:06 +08:00 committed by GitHub
parent d09bd037e7
commit e07dbdd530
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 846 additions and 162 deletions

View File

@ -40,5 +40,6 @@
"i18n-ally.localesPaths": [
"src/uni_modules/wot-design-uni/locale",
"src/uni_modules/wot-design-uni/locale/lang"
]
],
"common-intellisense.ui": []
}

View File

@ -79,7 +79,7 @@
通过设置 `clickable` 开启点击反馈,之后可以监听`click`事件。
```html
<wd-toast id="wd-toast" />
<wd-toast />
<wd-cell title="标题文字" value="内容" clickable @click="toast" />
```

View File

@ -2,7 +2,11 @@
# Upload 上传
图片上传组件
图片、视频和文件上传组件
::: tip 提示
目前组件库已兼容的平台,都支持上传视频,使用`video`组件实现的视频封面在`H5``微信小程序``支付宝小程序`平台得到支持,而在`钉钉小程序``App`平台则受限于`video`标签在这两个平台的能力无法用做视频封面。故推荐在`change`事件中获取视频封面并给`fileList`对应视频添加封面:`thumb`(上传至各种云服务器时,各厂商应该都提供了视频封面的功能)。
:::
## 基本用法
@ -10,7 +14,7 @@
数据更改后通过绑定 `change` 事件给 fileList 赋值。
`action` 设置图片上传的地址;
`action` 设置上传的地址;
```html
<wd-upload :file-list="fileList1" image-mode="aspectFill" :action="action" @change="handleChange1"></wd-upload>
@ -23,7 +27,7 @@ const fileList = ref<any[]>([
}
])
const action: string = 'https://ftf.jd.com/api/uploadImg'
const action: string = 'https://mockapi.eolink.com/zhTuw2P8c29bc981a741931bdd86eb04dc1e8fd64865cb5/upload'
function handleChange({ fileList: files }) {
fileList.value = files
@ -35,7 +39,12 @@ function handleChange({ fileList: files }) {
设置 `disabled` 开启禁用上传
```html
<wd-upload :file-list="fileList" action="https://ftf.jd.com/api/uploadImg" @change="handleChange" disabled></wd-upload>
<wd-upload
:file-list="fileList"
action="https://mockapi.eolink.com/zhTuw2P8c29bc981a741931bdd86eb04dc1e8fd64865cb5/upload"
@change="handleChange"
disabled
></wd-upload>
```
## 多选上传
@ -43,7 +52,12 @@ function handleChange({ fileList: files }) {
通过设置 `multiple` 开启文件多选上传。
```html
<wd-upload :file-list="fileList" multiple action="https://ftf.jd.com/api/uploadImg" @change="handleChange"></wd-upload>
<wd-upload
:file-list="fileList"
multiple
action="https://mockapi.eolink.com/zhTuw2P8c29bc981a741931bdd86eb04dc1e8fd64865cb5/upload"
@change="handleChange"
></wd-upload>
```
## 最大上传数限制
@ -51,7 +65,12 @@ function handleChange({ fileList: files }) {
上传组件可通过设置 `limit` 来限制上传文件的个数。
```html
<wd-upload :file-list="fileList" :limit="3" action="https://ftf.jd.com/api/uploadImg" @change="handleChange"></wd-upload>
<wd-upload
:file-list="fileList"
:limit="3"
action="https://mockapi.eolink.com/zhTuw2P8c29bc981a741931bdd86eb04dc1e8fd64865cb5/upload"
@change="handleChange"
></wd-upload>
```
## 拦截预览图片操作
@ -59,7 +78,12 @@ function handleChange({ fileList: files }) {
设置 `before-preview` 函数,在用户点击图片进行预览时,会执行 `before-preview` 函数,接收 { index: 当前预览的下标, imgList: 所有图片地址列表, resolve },通过 `resolve` 函数告知组件是否确定通过,`resolve` 接受 1 个 boolean 值,`resolve(true)` 表示选项通过,`resolve(false)` 表示选项不通过,不通过时不会执行预览图片操作。
```html
<wd-upload :file-list="fileList" action="https://ftf.jd.com/api/uploadImg" @change="handleChange" :before-preview="beforePreview"></wd-upload>
<wd-upload
:file-list="fileList"
action="https://mockapi.eolink.com/zhTuw2P8c29bc981a741931bdd86eb04dc1e8fd64865cb5/upload"
@change="handleChange"
:before-preview="beforePreview"
></wd-upload>
```
```typescript
@ -97,7 +121,12 @@ function handleChange({ files }) {
设置 `before-upload` 函数,弹出图片选择界面,在用户选择图片点击确认后,会执行 `before-upload` 函数,接收 { files: 当前上传的文件, fileList: 文件列表, resolve },可以对 `file` 进行处理,并通过 `resolve` 函数告知组件是否确定通过,`resolve` 接受 1 个 boolean 值,`resolve(true)` 表示选项通过,`resolve(false)` 表示选项不通过,不通过时不会执行上传操作。
```html
<wd-upload :file-list="fileList" action="https://ftf.jd.com/api/uploadImg" @change="handleChange" :before-upload="beforeUpload"></wd-upload>
<wd-upload
:file-list="fileList"
action="https://mockapi.eolink.com/zhTuw2P8c29bc981a741931bdd86eb04dc1e8fd64865cb5/upload"
@change="handleChange"
:before-upload="beforeUpload"
></wd-upload>
```
```typescript
@ -135,7 +164,12 @@ function handleChange({ files }) {
设置 `before-remove` 函数,在用户点击关闭按钮时,会执行 `before-remove` 函数,接收 { file: 移除的文件, index: 移除文件的下标, fileList: 文件列表, resolve },可以对 `file` 进行处理,并通过 `resolve` 函数告知组件是否确定通过,`resolve` 接受 1 个 boolean 值,`resolve(true)` 表示选项通过,`resolve(false)` 表示选项不通过,不通过时不会执行移除图片操作。
```html
<wd-upload :file-list="fileList" action="https://ftf.jd.com/api/uploadImg" @change="handleChange" :before-remove="beforeRemove"></wd-upload>
<wd-upload
:file-list="fileList"
action="https://mockapi.eolink.com/zhTuw2P8c29bc981a741931bdd86eb04dc1e8fd64865cb5/upload"
@change="handleChange"
:before-remove="beforeRemove"
></wd-upload>
```
```typescript
@ -174,7 +208,12 @@ function handleChange({ files }) {
设置 `before-choose` 函数,在用户点击唤起项时,会执行 `before-choose` 函数,接收 { fileList: 文件列表, resolve },通过 `resolve` 函数告知组件是否确定通过,`resolve` 接受 1 个 boolean 值,`resolve(true)` 表示选项通过,`resolve(false)` 表示选项不通过,不通过时不会执行选择文件操作。
```html
<wd-upload :file-list="fileList" action="https://ftf.jd.com/api/uploadImg" @change="handleChange" :before-choose="beforeChoose"></wd-upload>
<wd-upload
:file-list="fileList"
action="https://mockapi.eolink.com/zhTuw2P8c29bc981a741931bdd86eb04dc1e8fd64865cb5/upload"
@change="handleChange"
:before-choose="beforeChoose"
></wd-upload>
```
```typescript
@ -352,6 +391,11 @@ const buildFormData = ({ file, formData, resolve }) => {
使用默认插槽可以修改唤起上传的样式。
```html
<wd-upload
:file-list="fileList"
action="https://mockapi.eolink.com/zhTuw2P8c29bc981a741931bdd86eb04dc1e8fd64865cb5/upload"
@change="handleChange"
>
<wd-upload :file-list="fileList" :limit="5" action="https://ftf.jd.com/api/uploadImg" @change="handleChange">
<wd-button>上传</wd-button>
</wd-upload>
@ -365,35 +409,117 @@ const fileList = ref<any[]>([
])
```
## 上传视频
`accept`设置为`video`可以用于上传视频类型的文件。
```html
<wd-upload accept="video" multiple :file-list="fileList" :action="action" @change="handleChange"></wd-upload>
```
```typescript
const action = ref<string>('https://mockapi.eolink.com/zhTuw2P8c29bc981a741931bdd86eb04dc1e8fd64865cb5/upload')
const fileList = ref([])
function handleChange({ files }) {
fileList.value = files
}
```
## 同时上传视频和图片
`accept`设置为`media`可以用于同时上传视频和图片。仅微信小程序支持。
```html
<wd-upload accept="media" multiple :file-list="fileList" :action="action" @change="handleChange"></wd-upload>
```
```typescript
const action = ref<string>('https://mockapi.eolink.com/zhTuw2P8c29bc981a741931bdd86eb04dc1e8fd64865cb5/upload')
const fileList = ref([])
function handleChange({ files }) {
fileList.value = files
}
```
## 仅上传文件
`accept`设置为`file`可以用于上传除图片和视频以外类型的文件。仅微信小程序支持。
```html
<wd-upload accept="file" multiple :file-list="fileList" :action="action" @change="handleChange"></wd-upload>
```
```typescript
const action = ref<string>('https://mockapi.eolink.com/zhTuw2P8c29bc981a741931bdd86eb04dc1e8fd64865cb5/upload')
const fileList = ref([])
function handleChange({ files }) {
fileList.value = files
}
```
## 上传视频图片和文件
`accept`设置为`all`可以用于上传视频图片和文件。仅微信小程序和H5支持。微信小程序使用`chooseMessageFile`实现H5使用`chooseFile`实现。
```html
<wd-upload accept="all" multiple :file-list="fileList" :action="action" @change="handleChange"></wd-upload>
```
```typescript
const action = ref<string>('https://mockapi.eolink.com/zhTuw2P8c29bc981a741931bdd86eb04dc1e8fd64865cb5/upload')
const fileList = ref([])
function handleChange({ files }) {
fileList.value = files
}
```
## Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | 最低版本 |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------- | ------ | -------------------------- | -------- |
| file-list | 上传的文件列表, 例如: [{ name: 'food.jpg', url: 'https://xxx.cdn.com/xxx.jpg' }] | array | - | [] | - |
| action | 必选参数,上传的地址 | string | - | - | - |
| header | 设置上传的请求头部 | object | - | - | - |
| multiple | 是否支持多选文件 | boolean | - | - | - |
| disabled | 是否禁用 | boolean | - | false | - |
| limit | 最大允许上传个数 | number | - | - | - |
| show-limit-num | 限制上传个数的情况下,是否展示当前上传的个数 | boolean | - | false | - |
| max-size | 文件大小限制,单位为`byte` | number | - | - | - |
| source-type | 选择图片的来源chooseImage 接口详细参数,查看[官方手册](https://uniapp.dcloud.net.cn/api/media/image.html#chooseimage) | array / string | - | ['album', 'camera'] | - |
| size-type | 所选的图片的尺寸chooseImage 接口详细参数,查看[官方手册](https://uniapp.dcloud.net.cn/api/media/image.html#chooseimage) | array / string | - | ['original', 'compressed'] | - |
| name | 文件对应的 key开发者在服务端可以通过这个 key 获取文件的二进制内容uploadFile 接口详细参数,查看[官方手册](https://uniapp.dcloud.net.cn/api/request/network-file#uploadfile) | string | - | file | - |
| formData | HTTP 请求中其他额外的 form datauploadFile 接口详细参数,查看[官方手册](https://uniapp.dcloud.net.cn/api/request/network-file#uploadfile) | object | - | - | - |
| header | HTTP 请求 HeaderHeader 中不能设置 RefereruploadFile 接口详细参数,查看[官方手册](https://uniapp.dcloud.net.cn/api/request/network-file#uploadfile) | object | - | - | - |
| on-preview-fail | 预览失败执行操作 | function({ index, imgList }) | - | - | - |
| before-upload | 上传文件之前的钩子,参数为上传的文件和文件列表,若返回 false 或者返回 Promise 且被 reject则停止上传。 | function({ files, fileList, resolve }) | - | - | - |
| before-choose | 选择图片之前的钩子,参数为文件列表,若返回 false 或者返回 Promise 且被 reject则停止上传。 | function({ fileList, resolve }) | - | - | - |
| before-remove | 删除文件之前的钩子,参数为要删除的文件和文件列表,若返回 false 或者返回 Promise 且被 reject则停止上传。 | function({ file, fileList, resolve }) | - | - | - |
| before-preview | 图片预览前的钩子,参数为预览的图片下标和图片列表,若返回 false 或者返回 Promise 且被 reject则停止上传。 | function({ index, imgList, resolve }) | - | - | - |
| build-form-data | 构建上传`formData`的钩子,参数为上传的文件、待处理的`formData`,返回值为处理后的`formData`,若返回 false 或者返回 Promise 且被 reject则停止上传。 | function({ file, formData, resolve }) | - | - | 0.1.61 |
| loading-type | [加载中图标类型](/component/loading) | string | - | circular-ring | - |
| loading-color | [加载中图标颜色](/component/loading) | string | - | #ffffff | - |
| loading-size | [加载中图标尺寸](/component/loading) | string | - | 24px | - |
| <s>use-default-slot</s> | <s>开启默认唤起项插槽</s>移除该属性,直接使用`默认插槽`即可 | boolean | - | false | - |
| status-key | file 数据结构中status 对应的 key | string | - | status | - |
| 参数 | 说明 | 类型 | 可选值 | 默认值 | 最低版本 |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------- | ---------------------------------------------- | -------------------------- | -------- |
| file-list | 上传的文件列表, 例如: [{ name: 'food.jpg', url: 'https://xxx.cdn.com/xxx.jpg' }] | array | - | [] | - |
| action | 必选参数,上传的地址 | string | - | - | - |
| header | 设置上传的请求头部 | object | - | - | - |
| multiple | 是否支持多选文件 | boolean | - | - | - |
| disabled | 是否禁用 | boolean | - | false | - |
| limit | 最大允许上传个数 | number | - | - | - |
| show-limit-num | 限制上传个数的情况下,是否展示当前上传的个数 | boolean | - | false | - |
| max-size | 文件大小限制,单位为`byte` | number | - | - | - |
| source-type | 选择图片的来源chooseImage 接口详细参数,查看[官方手册](https://uniapp.dcloud.net.cn/api/media/image.html#chooseimage) | array / string | - | ['album', 'camera'] | - |
| size-type | 所选的图片的尺寸chooseImage 接口详细参数,查看[官方手册](https://uniapp.dcloud.net.cn/api/media/image.html#chooseimage) | array / string | - | ['original', 'compressed'] | - |
| name | 文件对应的 key开发者在服务端可以通过这个 key 获取文件的二进制内容uploadFile 接口详细参数,查看[官方手册](https://uniapp.dcloud.net.cn/api/request/network-file#uploadfile) | string | - | file | - |
| formData | HTTP 请求中其他额外的 form datauploadFile 接口详细参数,查看[官方手册](https://uniapp.dcloud.net.cn/api/request/network-file#uploadfile) | object | - | - | - |
| header | HTTP 请求 HeaderHeader 中不能设置 RefereruploadFile 接口详细参数,查看[官方手册](https://uniapp.dcloud.net.cn/api/request/network-file#uploadfile) | object | - | - | - |
| on-preview-fail | 预览失败执行操作 | function({ index, imgList }) | - | - | - |
| before-upload | 上传文件之前的钩子,参数为上传的文件和文件列表,若返回 false 或者返回 Promise 且被 reject则停止上传。 | function({ files, fileList, resolve }) | - | - | - |
| before-choose | 选择图片之前的钩子,参数为文件列表,若返回 false 或者返回 Promise 且被 reject则停止上传。 | function({ fileList, resolve }) | - | - | - |
| before-remove | 删除文件之前的钩子,参数为要删除的文件和文件列表,若返回 false 或者返回 Promise 且被 reject则停止上传。 | function({ file, fileList, resolve }) | - | - | - |
| before-preview | 图片预览前的钩子,参数为预览的图片下标和图片列表,若返回 false 或者返回 Promise 且被 reject则停止上传。 | function({ index, imgList, resolve }) | - | - | - |
| build-form-data | 构建上传`formData`的钩子,参数为上传的文件、待处理的`formData`,返回值为处理后的`formData`,若返回 false 或者返回 Promise 且被 reject则停止上传。 | function({ file, formData, resolve }) | - | - | 0.1.61 |
| loading-type | [加载中图标类型](/component/loading) | string | - | circular-ring | - |
| loading-color | [加载中图标颜色](/component/loading) | string | - | #ffffff | - |
| loading-size | [加载中图标尺寸](/component/loading) | string | - | 24px | - |
| status-key | file 数据结构中status 对应的 key | string | - | status | - |
| image-mode | 预览图片的 mode 属性 | ImageMode | - | aspectFit | - |
| accept | 接受的文件类型 | UploadFileType | **image** **video** **media** **file** **all** | **image** | 1.3.0 |
| compressed | 是否压缩视频,当 accept 为 video \| media 时生效 | boolean | - | true | 1.3.0 |
| maxDuration | 拍摄视频最长拍摄时间,当 accept 为 video \| media 时生效,单位秒 | Number | - | 60 | 1.3.0 |
| camera | 使用前置或者后置相机,当 accept 为 video \| media 时生效 | UploadCameraType | **front** | **back** | 1.3.0 |
## accept 的合法值
| name | 说明 | 最低版本 |
| ----- | -------------------------------------------------------------------------------------- | -------- |
| image | 图片,全平台支持 | - |
| video | 视频,全平台支持 | 1.3.0 |
| media | 图片和视频,仅微信支持,使用`chooseMedia`实现 | 1.3.0 |
| file | 从客户端会话选择图片和视频以外的文件,仅微信支持,使用`chooseMessageFile`实现 | 1.3.0 |
| all | 全部类型的文件,仅微信和 H5 支持,微信使用`chooseMessageFile`H5 使用`chooseFile`实现 | 1.3.0 |
## file 数据结构

View File

@ -4,12 +4,9 @@
<wd-privacy-popup></wd-privacy-popup>
<!-- #endif -->
<wd-message-box></wd-message-box>
<wd-toast id="wd-toast"></wd-toast>
<wd-toast></wd-toast>
<demo-block title="基本用法">
<wd-upload :file-list="fileList1" image-mode="aspectFill" :action="action" @change="handleChange1"></wd-upload>
</demo-block>
<demo-block title="多选上传">
<wd-upload :file-list="fileList2" multiple :action="action" @change="handleChange2"></wd-upload>
<wd-upload accept="image" :file-list="fileList" image-mode="aspectFill" :action="action" @change="handleChange"></wd-upload>
</demo-block>
<demo-block title="最大上传数限制">
<wd-upload :file-list="fileList3" :limit="3" :action="action" @change="handleChange3"></wd-upload>
@ -48,32 +45,58 @@
<!-- <demo-block title="上传至oss">
<wd-upload :file-list="fileList11" action="https://xxx.aliyuncs.com" :build-form-data="buildFormData" @change="handleChange11"></wd-upload>
</demo-block> -->
<demo-block title="上传视频">
<wd-upload accept="video" multiple :file-list="fileList1" :action="action" @change="handleChange1"></wd-upload>
</demo-block>
<!-- #ifdef MP-WEIXIN -->
<demo-block title="上传视频和图片">
<wd-upload accept="media" multiple :file-list="fileList11" :action="action" @change="handleChange11"></wd-upload>
</demo-block>
<demo-block title="仅上传文件">
<wd-upload accept="file" multiple :file-list="fileList12" :action="action" @change="handleChange12"></wd-upload>
</demo-block>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN || H5 -->
<demo-block title="上传视频图片和文件">
<wd-upload accept="all" multiple :file-list="fileList13" :action="action" @change="handleChange13"></wd-upload>
</demo-block>
<!-- #endif -->
</page-wraper>
</template>
<script lang="ts" setup>
import { useToast, useMessage } from '@/uni_modules/wot-design-uni'
import type { UploadFile } from '@/uni_modules/wot-design-uni/components/wd-upload/types'
import { ref } from 'vue'
const action: string = 'https://ftf.jd.com/api/uploadImg'
const fileList1 = ref<any[]>([
const action: string = 'https://mockapi.eolink.com/zhTuw2P8c29bc981a741931bdd86eb04dc1e8fd64865cb5/upload'
const fileList = ref<UploadFile[]>([
{
url: 'https://img.yzcdn.cn/vant/cat.jpeg'
}
])
const fileList2 = ref<any[]>([
const fileList1 = ref<UploadFile[]>([])
const fileList2 = ref<UploadFile[]>([
{
url: 'https://img12.360buyimg.com//n0/jfs/t1/29118/6/4823/55969/5c35c16bE7c262192/c9fdecec4b419355.jpg'
}
])
const fileList3 = ref([])
const fileList4 = ref([])
const fileList5 = ref([])
const fileList6 = ref([])
const fileList7 = ref([])
const fileList8 = ref([])
const fileList9 = ref([])
const fileList10 = ref([])
const fileList11 = ref([])
const fileList3 = ref<UploadFile[]>([])
const fileList4 = ref<UploadFile[]>([])
const fileList5 = ref<UploadFile[]>([])
const fileList6 = ref<UploadFile[]>([])
const fileList7 = ref<UploadFile[]>([])
const fileList8 = ref<UploadFile[]>([])
const fileList9 = ref<UploadFile[]>([])
const fileList10 = ref<UploadFile[]>([])
const fileList11 = ref<UploadFile[]>([])
const fileList12 = ref<UploadFile[]>([])
const fileList13 = ref<UploadFile[]>([])
const fileList14 = ref<UploadFile[]>([])
const fileList15 = ref<UploadFile[]>([])
const messageBox = useMessage()
const toast = useToast()
@ -176,7 +199,17 @@ function handleFail(event: any) {
function handleProgess(event: any) {
console.log('加载中', event)
}
function handleChange1({ fileList }: any) {
function handleChange({ fileList }: any) {
fileList.value = fileList
}
function handleChange1({ fileList }: { fileList: UploadFile[] }) {
// fileList.forEach((item) => {
// if (!item.thumb) {
// item.thumb = 'https://unpkg.com/wot-design-uni-assets/redpanda.jpg'
// }
// })
fileList1.value = fileList
}
function handleChange2({ fileList }: any) {
@ -209,5 +242,14 @@ function handleChange10({ fileList }: any) {
function handleChange11({ fileList }: any) {
fileList11.value = fileList
}
function handleChange12({ fileList }: any) {
fileList12.value = fileList
}
function handleChange13({ fileList }: any) {
fileList13.value = fileList
}
function handleChange14({ fileList }: any) {
fileList14.value = fileList
}
</script>
<style lang="scss" scoped></style>

View File

@ -700,6 +700,10 @@ $-upload-evoke-disabled-color: var(--wot-upload-evoke-disabled-color, rgba(0, 0,
$-upload-close-icon-size: var(--wot-upload-close-icon-size, 16px) !default; // 移除按钮尺寸
$-upload-close-icon-color: var(--wot-upload-close-icon-color, rgba(0, 0, 0, 0.65)) !default; // 移除按钮颜色
$-upload-progress-fs: var(--wot-upload-progress-fs, 14px) !default; // 进度文字字号
$-upload-file-fs: var(--wot-upload-file-fs, 12px) !default; // 文件名字号
$-upload-file-color: var(--wot-upload-file-color, $-color-secondary) !default; // 文件名字颜色
$-upload-preview-name-fs: var(--wot-upload-preview-name-fs, 12px) !default; // 预览图片名字号
$-upload-preview-icon-size: var(--wot-upload-preview-icon-size, 24px) !default; // 预览内部图标尺寸
$-upload-preview-name-bg: var(--wot-upload-preview-name-bg, rgba(0, 0, 0, 0.6)) !default; // 预览文件名背景色

View File

@ -656,6 +656,28 @@ export const getPropByPath = (obj: any, path: string): any => {
*/
export const isDate = (val: unknown): val is Date => Object.prototype.toString.call(val) === '[object Date]' && !Number.isNaN((val as Date).getTime())
/**
* URL是否为视频链接
* @param url URL字符串
* @returns URL是视频链接则为truefalse
*/
export function isVideoUrl(url: string): boolean {
// 使用正则表达式匹配视频文件类型的URL
const videoRegex = /\.(mp4|mpg|mpeg|dat|asf|avi|rm|rmvb|mov|wmv|flv|mkv|video)/i
return videoRegex.test(url)
}
/**
* URL是否为图片URL
* @param url URL字符串
* @returns URL是图片格式truefalse
*/
export function isImageUrl(url: string): boolean {
// 使用正则表达式匹配图片URL
const imageRegex = /\.(jpeg|jpg|gif|png|svg|webp|jfif|bmp|dpg|image)/i
return imageRegex.test(url)
}
/**
* H5
*/

View File

@ -11,6 +11,14 @@
color: $-dark-color-gray;
}
}
@include e(file) {
background-color: $-dark-background4;
}
@include e(file-name) {
color: $-dark-color3;
}
}
}
@ -70,12 +78,46 @@
display: flex;
}
@include e(picture) {
@include e(picture, file, video) {
position: relative;
display: block;
width: 100%;
height: 100%;
}
@include e(file, video) {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: $-upload-evoke-bg;
}
@include e(file-name, video-name) {
width: 100%;
font-size: $-upload-file-fs;
color: $-upload-file-color;
box-sizing: border-box;
padding: 0 4px;
text-align: center;
margin-top: 8px;
@include lineEllipsis()
}
@include edeep(video-paly) {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24px;
color: $-color-white;
&::before {
background-color: rgba(0, 0, 0, 0.5);
border-radius: 50%;
}
}
@include edeep(close) {
position: absolute;
right: calc($-upload-close-icon-size / 2 * -1);

View File

@ -4,34 +4,63 @@ import type { LoadingType } from '../wd-loading/types'
import type { ImageMode } from '../wd-img/types'
export interface ChooseFileOption {
// 是否支持多选文件
multiple: boolean
sizeType: string | string[]
sourceType: string[]
// 所选的图片的尺寸
sizeType?: UploadSizeType[]
// 选择文件的来源
sourceType: UploadSourceType[]
// 最大允许上传个数
maxCount: number
// 接受文件类型
accept: UploadFileType
/**
* accept video
*/
compressed: boolean
/**
* accept video | media
*/
maxDuration: number
/**
* 使 accept video | media backfront
*/
camera: UploadCameraType
}
export interface UploadFileItem {
export type UploadFileItem = {
[key: string]: any
// 当前上传文件在列表中的唯一标识
uid: number
// 缩略图地址
thumb?: string
// 当前文件名称仅h5支持
name?: string
// 上传状态
status: string
status?: string
// 文件大小
size: number
// 上传图片的本地地址
size?: number
// 上传图片/视频的本地地址
url: string
// 上传的地址
action: string
// 上传进度
percent: number
percent?: number
// 后端返回的内容,可能是对象,也可能是字符串
response?: string | Record<string, any>
}
export interface ChooseFile {
path: string // 上传临时地址
size?: number // 上传大小
name?: string // 当前文件名称仅h5支持
type: 'image' | 'video' | 'file' // 上传类型
duration?: number // 上传时间
thumb?: string // 缩略图地址
}
export type UploadSourceType = 'album' | 'camera'
export type UploadSizeType = 'original' | 'compressed'
export type UploadFileType = 'image' | 'video' | 'media' | 'all' | 'file'
export type UploadCameraType = 'front' | 'back'
export type UploadBeforePreviewOption = {
index: number
@ -74,15 +103,16 @@ export type UploadBuildFormDataOption = {
}
export type UploadBuildFormData = (options: UploadBuildFormDataOption) => void
export type UploadFile = Partial<UploadFileItem> & { url: string }
export const uploadProps = {
...baseProps,
/**
* ,:[{name:'food.jpg',url:'https://xxx.cdn.com/xxx.jpg'}]
* array
* []
*/
fileList: makeArrayProp<UploadFileItem>(),
fileList: makeArrayProp<UploadFile>(),
/**
*
* string
@ -110,9 +140,9 @@ export const uploadProps = {
/**
*
* number
* 0
*
*/
limit: makeNumberProp(0),
limit: Number,
/**
*
* boolean
@ -141,7 +171,9 @@ export const uploadProps = {
*/
sizeType: {
type: Array as PropType<UploadSizeType[]>,
// #ifndef MP-DINGTALK
default: () => ['original', 'compressed']
// #endif
},
/**
* keykey获取文件的二进制内容uploadFile接口详细参数
@ -204,25 +236,43 @@ export const uploadProps = {
* '#ffffff'
*/
loadingColor: makeStringProp('#ffffff'),
accept: makeStringProp('image'),
/**
* 'image' | 'video' | 'media' | 'all' | 'file'
* image
* 'media''image''video''file''image''video''all'
* 'media''file''all'H5支持
*/
accept: makeStringProp<UploadFileType>('image'),
/**
* file status key
* string
* 'status'
*/
statusKey: makeStringProp('status'),
/**
*
* boolean
* false
*/
useDefaultSlot: makeBooleanProp(false),
/**
*
* string
* '24px'
*/
loadingSize: makeStringProp('24px'),
/**
* accept video
* boolean
* true
*/
compressed: makeBooleanProp(true),
/**
* accept video | media
* number
* 60
*/
maxDuration: makeNumberProp(60),
/**
* 使 accept video | media backfront
* UploadCameraType
* 'back'
*/
camera: makeStringProp<UploadCameraType>('back'),
/**
*
* string

View File

@ -1,14 +1,152 @@
import type { ChooseFileOption } from './types'
/*
* @Author: weisheng
* @Date: 2024-03-18 22:36:44
* @LastEditTime: 2024-07-07 18:59:40
* @LastEditors: weisheng
* @Description:
* @FilePath: /wot-design-uni/src/uni_modules/wot-design-uni/components/wd-upload/utils.ts
*
*/
import { isArray, isDef } from '../common/util'
import type { ChooseFile, ChooseFileOption } from './types'
// 后续会对外暴露选中视频文件
export function chooseFile({ multiple, sizeType, sourceType, maxCount }: ChooseFileOption) {
return new Promise((resolve, reject) => {
uni.chooseImage({
count: multiple ? Math.min(maxCount, 9) : 1, // 最多可以选择的数量如果不支持多选则数量为1
sizeType,
sourceType,
success: resolve,
fail: reject
function formatImage(res: UniApp.ChooseImageSuccessCallbackResult): ChooseFile[] {
// #ifdef MP-DINGTALK
// 钉钉文件在files中
res.tempFiles = isDef((res as any).files) ? (res as any).files : res.tempFiles
// #endif
if (isArray(res.tempFiles)) {
const result: ChooseFile[] = []
res.tempFiles.forEach(async (item: any) => {
result.push({
path: item.path || '',
name: item.name || '',
size: item.size,
type: 'image',
thumb: item.path || ''
})
})
return result
} else {
return [
{
path: (res.tempFiles as any).path || '',
name: (res.tempFiles as any).name || '',
size: (res.tempFiles as any).size,
type: 'image',
thumb: (res.tempFiles as any).path || ''
}
]
}
}
function formatVideo(res: UniApp.ChooseVideoSuccess): ChooseFile[] {
return [
{
path: res.tempFilePath || (res as any).filePath || '',
name: res.name || '',
size: res.size,
type: 'video',
thumb: (res as any).thumbTempFilePath || '',
duration: res.duration
}
]
}
function formatMedia(res: UniApp.ChooseMediaSuccessCallbackResult): ChooseFile[] {
return res.tempFiles.map((item) => ({
type: item.fileType,
path: item.tempFilePath,
thumb: item.fileType === 'video' ? item.thumbTempFilePath : item.tempFilePath,
size: item.size,
duration: item.duration
}))
}
export function chooseFile({
multiple,
sizeType,
sourceType,
maxCount,
accept,
compressed,
maxDuration,
camera
}: ChooseFileOption): Promise<ChooseFile[]> {
return new Promise((resolve, reject) => {
switch (accept) {
case 'image':
uni.chooseImage({
count: multiple ? Math.min(maxCount, 9) : 1, // 最多可以选择的数量如果不支持多选则数量为1
sizeType,
sourceType,
success: (res) => resolve(formatImage(res)),
fail: (error) => {
reject(error)
}
})
break
case 'video':
uni.chooseVideo({
sourceType,
compressed,
maxDuration,
camera,
success: (res) => {
resolve(formatVideo(res))
},
fail: reject
})
break
// #ifdef MP-WEIXIN
case 'media':
uni.chooseMedia({
count: multiple ? Math.min(maxCount, 9) : 1,
sourceType,
sizeType,
camera,
maxDuration,
success: (res) => resolve(formatMedia(res)),
fail: reject
})
break
case 'file':
uni.chooseMessageFile({
count: multiple ? Math.min(maxCount, 100) : 1,
type: accept,
success: (res) => resolve(res.tempFiles),
fail: reject
})
break
// #endif
case 'all':
// #ifdef MP-WEIXIN
uni.chooseMessageFile({
count: multiple ? Math.min(maxCount, 100) : 1,
type: accept,
success: (res) => resolve(res.tempFiles),
fail: reject
})
// #endif
// #ifdef H5
uni.chooseFile({
count: multiple ? Math.min(maxCount, 100) : 1,
type: accept,
success: (res) => resolve(res.tempFiles as ChooseFile[]),
fail: reject
})
// #endif
break
default:
// 默认选择图片
uni.chooseImage({
count: multiple ? Math.min(maxCount, 9) : 1, // 最多可以选择的数量如果不支持多选则数量为1
sizeType,
sourceType,
success: (res) => resolve(formatImage(res)),
fail: reject
})
break
}
})
}

View File

@ -4,7 +4,45 @@
<view :class="['wd-upload__preview', customPreviewClass]" v-for="(file, index) in uploadFiles" :key="index">
<!-- 成功时展示图片 -->
<view class="wd-upload__status-content">
<image :src="file.url" :mode="imageMode" class="wd-upload__picture" @click="onPreviewImage(index)" />
<image v-if="isImage(file)" :src="file.url" :mode="imageMode" class="wd-upload__picture" @click="onPreviewImage(index)" />
<template v-else-if="isVideo(file)">
<view class="wd-upload__video" v-if="file.thumb" @click="onPreviewVideo(file)">
<image :src="file.thumb" :mode="imageMode" class="wd-upload__picture" />
<wd-icon name="play-circle-filled" custom-class="wd-upload__video-paly"></wd-icon>
</view>
<view v-else class="wd-upload__video" @click="onPreviewVideo(file)">
<!-- #ifdef APP-PLUS || MP-DINGTALK -->
<wd-icon name="video" size="22px"></wd-icon>
<!-- #endif -->
<!-- #ifndef APP-PLUS -->
<!-- #ifndef MP-DINGTALK -->
<video
:src="file.url"
:title="file.name || '视频' + index"
object-fit="contain"
:controls="false"
:poster="file.thumb"
:autoplay="false"
:show-center-play-btn="false"
:show-fullscreen-btn="false"
:show-play-btn="false"
:show-loading="false"
:show-progress="false"
:show-mute-btn="false"
:enable-progress-gesture="false"
:enableNative="true"
class="wd-upload__video"
></video>
<wd-icon name="play-circle-filled" custom-class="wd-upload__video-paly"></wd-icon>
<!-- #endif -->
<!-- #endif -->
</view>
</template>
<view v-else class="wd-upload__file" @click="onPreviewFile(file)">
<wd-icon name="file" size="22px"></wd-icon>
<view class="wd-upload__file-name">{{ file.name || file.url }}</view>
</view>
</view>
<view v-if="file.status !== 'success'" class="wd-upload__mask wd-upload__status-content">
@ -36,6 +74,7 @@
</view>
</block>
</view>
<wd-video-preview ref="videoPreview"></wd-video-preview>
</template>
<script lang="ts">
@ -51,10 +90,11 @@ export default {
<script lang="ts" setup>
import { computed, ref, watch } from 'vue'
import { context, getType, isDef, isEqual, isFunction } from '../common/util'
import { context, getType, isEqual, isImageUrl, isVideoUrl, isFunction } from '../common/util'
import { chooseFile } from './utils'
import { useTranslate } from '../composables/useTranslate'
import { uploadProps, type UploadFileItem } from './types'
import { uploadProps, type UploadFileItem, type ChooseFile } from './types'
import type { VideoPreviewInstance } from '../wd-video-preview/types'
const props = defineProps(uploadProps)
const emit = defineEmits(['fail', 'change', 'success', 'progress', 'oversize', 'chooseerror', 'remove'])
@ -65,17 +105,17 @@ const uploadFiles = ref<UploadFileItem[]>([])
const showUpload = computed(() => !props.limit || uploadFiles.value.length < props.limit)
const videoPreview = ref<VideoPreviewInstance>()
watch(
() => props.fileList,
(val) => {
const { statusKey } = props
if (isEqual(val, uploadFiles.value)) return
const uploadFileList = val.map((item) => {
item.uid = context.id++
const uploadFileList: UploadFileItem[] = val.map((item) => {
item[statusKey] = item[statusKey] || 'success'
item.action = props.action || ''
item.response = item.response || ''
return item
return { ...item, uid: context.id++ }
})
uploadFiles.value = uploadFileList
},
@ -176,20 +216,38 @@ watch(
}
)
/**
* 获取图片信息
* @param img
*/
function getImageInfo(img: string) {
return new Promise<UniApp.GetImageInfoSuccessData>((resolve, reject) => {
uni.getImageInfo({
src: img,
success: (res) => {
resolve(res)
},
fail: (error) => {
reject(error)
}
})
})
}
/**
* @description 初始化文件数据
* @param {Object} file 上传的文件
*/
function initFile(file: UploadFileItem) {
function initFile(file: ChooseFile) {
//
const initState: UploadFileItem = {
uid: context.id++,
// h5 name
name: file.name || '',
thumb: file.thumb || '',
status: 'loading',
size: file.size,
size: file.size || 0,
url: file.path,
action: props.action,
percent: 0
}
@ -261,7 +319,6 @@ function handleProgress(res: Record<string, any>, file: UploadFileItem) {
*/
function handleUpload(file: UploadFileItem, formData: Record<string, any>) {
const { action, name, header = {}, accept } = props
const uploadTask = uni.uploadFile({
url: action,
header,
@ -300,69 +357,53 @@ function handleUpload(file: UploadFileItem, formData: Record<string, any>) {
* @description 选择文件的实际操作将chooseFile自己用promise包了一层
*/
function onChooseFile() {
const { multiple, maxSize, accept, sizeType, limit, sourceType, beforeUpload } = props
// 使 chooseImage
if (accept === 'image') {
//
chooseFile({
multiple,
sizeType,
sourceType,
maxCount: limit ? limit - uploadFiles.value.length : 9
})
.then((res: any) => {
// file
let files: Array<any> = Array.prototype.slice.call(res.tempFiles)
//
if (!multiple) {
files = files.slice(0, 1)
const { multiple, maxSize, accept, sizeType, limit, sourceType, compressed, maxDuration, camera, beforeUpload } = props
//
chooseFile({
multiple,
sizeType,
sourceType,
maxCount: limit ? limit - uploadFiles.value.length : 9,
accept,
compressed,
maxDuration,
camera
})
.then((res) => {
// file
let files = res
//
if (!multiple) {
files = files.slice(0, 1)
}
//
const mapFiles = async (files: ChooseFile[]) => {
for (let index = 0; index < files.length; index++) {
const file = files[index]
if (file.type === 'image' && !file.size) {
const imageInfo = await getImageInfo(file.path)
file.size = imageInfo.width * imageInfo.height
}
Number(file.size) <= maxSize ? initFile(file) : emit('oversize', { file })
}
}
//
const mapFiles = (files: Array<any>) => {
files.forEach(async (file: any) => {
if (!isDef(file.size)) {
file.size = await getImageInfo(file.path)
}
file.size <= maxSize ? initFile(file) : emit('oversize', { file })
})
}
//
if (beforeUpload) {
beforeUpload({
files,
fileList: uploadFiles.value,
resolve: (isPass: boolean) => {
isPass && mapFiles(files)
}
})
} else {
mapFiles(files)
}
})
.catch((error) => {
emit('chooseerror', { error })
})
}
}
/**
* 获取图片信息
* @param src 图片地址
*/
function getImageInfo(src: string) {
return new Promise<number>((resolve, reject) => {
uni.getImageInfo({
src: src,
success: (res) => {
resolve(res.height * res.width)
},
fail: () => {
reject(0)
//
if (beforeUpload) {
beforeUpload({
files,
fileList: uploadFiles.value,
resolve: (isPass: boolean) => {
isPass && mapFiles(files)
}
})
} else {
mapFiles(files)
}
})
})
.catch((error) => {
emit('chooseerror', { error })
})
}
/**
@ -419,7 +460,23 @@ function removeFile(index: number) {
}
}
function onPreview(index: number, lists: string[]) {
/**
* 预览文件
* @param file
*/
function handlePreviewFile(file: UploadFileItem) {
uni.openDocument({
filePath: file.url,
showMenu: true
})
}
/**
* 预览图片
* @param index
* @param lists
*/
function handlePreviewImage(index: number, lists: string[]) {
const { onPreviewFail } = props
uni.previewImage({
urls: lists,
@ -437,21 +494,100 @@ function onPreview(index: number, lists: string[]) {
})
}
/**
* 预览视频
* @param index
* @param lists
*/
function handlePreviewVieo(index: number, lists: UploadFileItem[]) {
const { onPreviewFail } = props
// #ifdef MP-WEIXIN
uni.previewMedia({
current: index,
sources: lists.map((file) => {
return {
url: file.url,
type: 'video',
poster: file.thumb
}
}),
fail() {
if (onPreviewFail) {
onPreviewFail({
index,
imgList: []
})
} else {
uni.showToast({ title: '预览视频失败', icon: 'none' })
}
}
})
// #endif
// #ifndef MP-WEIXIN
videoPreview.value?.open({ url: lists[index].url, poster: lists[index].thumb, title: lists[index].name })
// #endif
}
function onPreviewImage(index: number) {
const { beforePreview } = props
const lists = uploadFiles.value.map((file) => file.url)
const lists = uploadFiles.value.filter((file) => isImage(file)).map((file) => file.url)
if (beforePreview) {
beforePreview({
index,
imgList: lists,
resolve: (isPass: boolean) => {
isPass && onPreview(index, lists)
isPass && handlePreviewImage(index, lists)
}
})
} else {
onPreview(index, lists)
handlePreviewImage(index, lists)
}
}
function onPreviewVideo(file: UploadFileItem) {
const { beforePreview } = props
const lists = uploadFiles.value.filter((file) => isVideo(file))
const index: number = lists.findIndex((item) => item.url === file.url)
if (beforePreview) {
beforePreview({
index,
imgList: [],
resolve: (isPass: boolean) => {
isPass && handlePreviewVieo(index, lists)
}
})
} else {
handlePreviewVieo(index, lists)
}
}
function onPreviewFile(file: UploadFileItem) {
const { beforePreview } = props
const lists = uploadFiles.value.filter((file) => {
return !isVideo(file) && !isImage(file)
})
const index: number = lists.findIndex((item) => item.url === file.url)
if (beforePreview) {
beforePreview({
index,
imgList: [],
resolve: (isPass: boolean) => {
isPass && handlePreviewFile(file)
}
})
} else {
handlePreviewFile(file)
}
}
function isVideo(file: UploadFileItem) {
return (file.name && isVideoUrl(file.name)) || isVideoUrl(file.url)
}
function isImage(file: UploadFileItem) {
return (file.name && isImageUrl(file.name)) || isImageUrl(file.url)
}
</script>
<style lang="scss" scoped>
@import './index.scss';

View File

@ -0,0 +1,28 @@
@import "../common/abstracts/variable.scss";
@import "../common/abstracts/_mixin.scss";
@include b(video-preview) {
position: fixed;
top: 0;
left: 0;
z-index: 101;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: #000;
@include e(video) {
width: 100%;
height: 242px;
transition: all 0.3s ease;
}
@include edeep(close){
margin-top: 64px;
padding: 6px;
color: $-curtain-content-close-color;
}
}

View File

@ -0,0 +1,32 @@
/*
* @Author: weisheng
* @Date: 2024-06-30 23:09:08
* @LastEditTime: 2024-07-01 21:47:34
* @LastEditors: weisheng
* @Description:
* @FilePath: /wot-design-uni/src/uni_modules/wot-design-uni/components/wd-video-preview/types.ts
*
*/
import type { ComponentPublicInstance, ExtractPropTypes, PropType } from 'vue'
import { baseProps } from '../common/props'
export const videoPreviewProps = {
...baseProps
}
export type PreviewVideo = {
url: string // 视频资源地址
poster?: string // 视频封面
title?: string // 视频标题
}
export type VideoPreviewProps = ExtractPropTypes<typeof videoPreviewProps>
export type VideoPreviewExpose = {
// 打开弹框
open: (video: PreviewVideo) => void
// 关闭弹框
close: () => void
}
export type VideoPreviewInstance = ComponentPublicInstance<VideoPreviewExpose, VideoPreviewProps>

View File

@ -0,0 +1,62 @@
<template>
<view :class="`wd-video-preview ${customClass}`" :style="customStyle" v-if="showPopup">
<video
class="wd-video-preview__video"
v-if="previdewVideo.url"
:controls="true"
:poster="previdewVideo.poster"
:title="previdewVideo.title"
play-btn-position="center"
:enableNative="true"
:src="previdewVideo.url"
:enable-progress-gesture="false"
></video>
<wd-icon name="close-circle" size="48px" :custom-class="`wd-video-preview__close`" @click="close" />
</view>
</template>
<script lang="ts">
export default {
name: 'wd-video-preview',
options: {
addGlobalClass: true,
virtualHost: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import { reactive, ref } from 'vue'
import { videoPreviewProps, type PreviewVideo, type VideoPreviewExpose } from './types'
defineProps(videoPreviewProps)
const showPopup = ref<boolean>(false)
const previdewVideo = reactive<PreviewVideo>({ url: '', poster: '', title: '' })
function open(video: PreviewVideo) {
showPopup.value = true
previdewVideo.url = video.url
previdewVideo.poster = video.poster
previdewVideo.title = video.title
}
function close() {
showPopup.value = false
}
function handleClosed() {
previdewVideo.url = ''
previdewVideo.poster = ''
previdewVideo.title = ''
}
defineExpose<VideoPreviewExpose>({
open,
close
})
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

View File

@ -1,7 +1,7 @@
/*
* @Author: weisheng
* @Date: 2023-09-25 17:28:12
* @LastEditTime: 2024-01-09 12:48:02
* @LastEditTime: 2024-07-05 14:37:28
* @LastEditors: weisheng
* @Description:
* @FilePath: \wot-design-uni\src\uni_modules\wot-design-uni\global.d.ts
@ -95,6 +95,7 @@ declare module '@vue/runtime-core' {
WdPasswordInput: typeof import('./components/wd-password-input/wd-password-input.vue')['default']
WdForm: typeof import('./components/wd-form/wd-form.vue')['default']
WdTextarea: typeof import('./components/wd-textarea/wd-textarea.vue')['default']
WdVideoPreview: typeof import('./components/wd-video-preview/wd-video-preview.vue')['default']
WdBacktop: typeof import('./components/wd-backtop/wd-backtop.vue')['default']
WdSkeleton: typeof import('./components/wd-skeleton/wd-skeleton.vue')['default']
WdIndexBar: typeof import('./components/wd-index-bar/wd-index-bar.vue')['default']