更新:登录功能
This commit is contained in:
175
uni_modules/wot-design-uni/components/wd-upload/index.scss
Normal file
175
uni_modules/wot-design-uni/components/wd-upload/index.scss
Normal file
@@ -0,0 +1,175 @@
|
||||
@import "../common/abstracts/variable.scss";
|
||||
@import "../common/abstracts/_mixin.scss";
|
||||
|
||||
.wot-theme-dark {
|
||||
@include b(upload) {
|
||||
@include e(evoke) {
|
||||
background-color: $-dark-background4;
|
||||
color: $-dark-color3;
|
||||
|
||||
@include when(disabled) {
|
||||
color: $-dark-color-gray;
|
||||
}
|
||||
}
|
||||
|
||||
@include e(file) {
|
||||
background-color: $-dark-background4;
|
||||
}
|
||||
|
||||
@include e(file-name) {
|
||||
color: $-dark-color3;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@include b(upload) {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@include e(evoke) {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: $-upload-size;
|
||||
height: $-upload-size;
|
||||
font-size: $-upload-evoke-icon-size;
|
||||
background-color: $-upload-evoke-bg;
|
||||
color: $-upload-evoke-color;
|
||||
margin-bottom: 12px;
|
||||
|
||||
@include when(disabled) {
|
||||
color: $-upload-evoke-disabled-color;
|
||||
}
|
||||
}
|
||||
|
||||
@include e(evoke-num) {
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
@include edeep(evoke-icon) {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
@include e(input) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@include e(preview) {
|
||||
position: relative;
|
||||
width: $-upload-size;
|
||||
height: $-upload-size;
|
||||
margin: 0 12px 12px 0;
|
||||
}
|
||||
|
||||
@include e(preview-list) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@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(video-icon, file-icon) {
|
||||
font-size: $-upload-cover-icon-size;
|
||||
}
|
||||
|
||||
@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);
|
||||
top: calc($-upload-close-icon-size / 2 * -1);
|
||||
font-size: $-upload-close-icon-size;
|
||||
z-index: 1;
|
||||
color: $-upload-close-icon-color;
|
||||
width: $-upload-close-icon-size;
|
||||
height: $-upload-close-icon-size;
|
||||
line-height: $-upload-close-icon-size;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background-color: $-color-white;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
@include e(mask) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: $-upload-preview-name-bg;
|
||||
}
|
||||
|
||||
@include e(progress-txt) {
|
||||
font-size: $-upload-progress-fs;
|
||||
line-height: $-upload-progress-fs;
|
||||
margin-top: 9px;
|
||||
color: $-color-white;
|
||||
}
|
||||
|
||||
@include edeep(icon) {
|
||||
font-size: $-upload-preview-icon-size;
|
||||
color: $-color-white;
|
||||
}
|
||||
|
||||
@include e(status-content) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
391
uni_modules/wot-design-uni/components/wd-upload/types.ts
Normal file
391
uni_modules/wot-design-uni/components/wd-upload/types.ts
Normal file
@@ -0,0 +1,391 @@
|
||||
import type { ComponentPublicInstance, ExtractPropTypes, PropType } from 'vue'
|
||||
import { baseProps, makeArrayProp, makeBooleanProp, makeNumberProp, makeStringProp } from '../common/props'
|
||||
import type { LoadingType } from '../wd-loading/types'
|
||||
import type { ImageMode } from '../wd-img/types'
|
||||
|
||||
export interface ChooseFileOption {
|
||||
// 是否支持多选文件
|
||||
multiple?: boolean
|
||||
// 所选的图片的尺寸
|
||||
sizeType?: UploadSizeType[]
|
||||
// 选择文件的来源
|
||||
sourceType?: UploadSourceType[]
|
||||
// 最大允许上传个数
|
||||
maxCount?: number
|
||||
// 接受文件类型
|
||||
accept?: UploadFileType
|
||||
/**
|
||||
* 是否压缩视频,当 accept 为 video 时生效。
|
||||
*/
|
||||
compressed?: boolean
|
||||
/**
|
||||
* 拍摄视频最长拍摄时间,当 accept 为 video | media 时生效,单位秒。
|
||||
*/
|
||||
maxDuration?: number
|
||||
/**
|
||||
* 使用前置或者后置相机,当 accept 为 video | media 时生效,可选值为:back|front。
|
||||
*/
|
||||
camera?: UploadCameraType
|
||||
/**
|
||||
* 根据文件拓展名过滤,H5、微信小程序支持
|
||||
* 每一项都不能是空字符串, 默认不过滤
|
||||
* 例如: ['.jpg'] 表示只选择.jpg文件
|
||||
*/
|
||||
extension?: string[]
|
||||
}
|
||||
|
||||
export type UploadFileItem = {
|
||||
[key: string]: any
|
||||
// 当前上传文件在列表中的唯一标识
|
||||
uid: number
|
||||
// 缩略图地址
|
||||
thumb?: string
|
||||
// 当前文件名称,仅h5支持
|
||||
name?: string
|
||||
// 上传状态。若自定义了status-key,应取对应字段
|
||||
status?: UploadStatusType
|
||||
// 文件大小
|
||||
size?: number
|
||||
// 上传图片/视频的本地地址
|
||||
url: string
|
||||
// 上传进度
|
||||
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 UploadStatusType = 'pending' | 'loading' | 'success' | 'fail'
|
||||
|
||||
export type UploadBeforePreviewOption = {
|
||||
file: UploadFileItem
|
||||
index: number
|
||||
imgList: string[]
|
||||
fileList: UploadFileItem[]
|
||||
resolve: (isPass: boolean) => void
|
||||
}
|
||||
export type UploadBeforePreview = (option: UploadBeforePreviewOption) => void
|
||||
|
||||
export type UploadOnPreviewFailOption = {
|
||||
index: number
|
||||
imgList: string[]
|
||||
}
|
||||
export type UploadOnPreviewFail = (option: UploadOnPreviewFailOption) => void
|
||||
|
||||
export type UploadBeforeRemoveOption = {
|
||||
file: UploadFileItem
|
||||
index: number
|
||||
fileList: UploadFileItem[]
|
||||
resolve: (isPass: boolean) => void
|
||||
}
|
||||
export type UploadBeforeRemove = (option: UploadBeforeRemoveOption) => void
|
||||
|
||||
export type UploadBeforeChooseOption = {
|
||||
fileList: UploadFileItem[]
|
||||
resolve: (isPass: boolean) => void
|
||||
}
|
||||
export type UploadBeforeChoose = (option: UploadBeforeChooseOption) => void
|
||||
|
||||
export type UploadBeforeUploadOption = {
|
||||
files: Record<string, any>[]
|
||||
fileList: UploadFileItem[]
|
||||
resolve: (isPass: boolean) => void
|
||||
}
|
||||
export type UploadBeforeUpload = (options: UploadBeforeUploadOption) => void
|
||||
|
||||
export type UploadFormData = Record<string, any>
|
||||
|
||||
export type UploadBuildFormDataOption = {
|
||||
file: UploadFileItem
|
||||
formData: UploadFormData
|
||||
resolve: (formData: Record<string, any>) => void
|
||||
}
|
||||
export type UploadBuildFormData = (options: UploadBuildFormDataOption) => void
|
||||
|
||||
export type UploadFile = Partial<UploadFileItem> & { url: string }
|
||||
|
||||
export type UploadMethod = (
|
||||
uploadFile: UploadFileItem,
|
||||
formData: UploadFormData,
|
||||
options: {
|
||||
action: string
|
||||
header: Record<string, any>
|
||||
name: string
|
||||
fileName: string
|
||||
fileType: 'image' | 'video' | 'audio'
|
||||
statusCode: number
|
||||
// 添加是否自动中断之前上传的选项
|
||||
abortPrevious?: boolean
|
||||
onSuccess: (res: UniApp.UploadFileSuccessCallbackResult, file: UploadFileItem, formData: UploadFormData) => void
|
||||
onError: (res: UniApp.GeneralCallbackResult, file: UploadFileItem, formData: UploadFormData) => void
|
||||
onProgress: (res: UniApp.OnProgressUpdateResult, file: UploadFileItem) => void
|
||||
}
|
||||
) => UniApp.UploadTask | void | Promise<void> // 修改这里,支持返回 UploadTask 类型
|
||||
|
||||
export const uploadProps = {
|
||||
...baseProps,
|
||||
/**
|
||||
* 上传的文件列表,例如:[{name:'food.jpg',url:'https://xxx.cdn.com/xxx.jpg'}]
|
||||
* 类型:array
|
||||
* 默认值:[]
|
||||
*/
|
||||
fileList: makeArrayProp<UploadFile>(),
|
||||
/**
|
||||
* 必选参数,上传的地址
|
||||
* 类型:string
|
||||
* 默认值:''
|
||||
*/
|
||||
action: makeStringProp(''),
|
||||
/**
|
||||
* 设置上传的请求头部
|
||||
* 类型:object
|
||||
* 默认值:{}
|
||||
*/
|
||||
header: { type: Object as PropType<Record<string, any>>, default: () => ({}) },
|
||||
/**
|
||||
* 是否支持多选文件
|
||||
* 类型:boolean
|
||||
* 默认值:false
|
||||
*/
|
||||
multiple: makeBooleanProp(false),
|
||||
/**
|
||||
* 是否禁用
|
||||
* 类型:boolean
|
||||
* 默认值:false
|
||||
*/
|
||||
disabled: makeBooleanProp(false),
|
||||
/**
|
||||
* 最大允许上传个数
|
||||
* 类型:number
|
||||
* 默认值:无
|
||||
*/
|
||||
limit: Number,
|
||||
/**
|
||||
* 限制上传个数的情况下,是否展示当前上传的个数
|
||||
* 类型:boolean
|
||||
* 默认值:true
|
||||
*/
|
||||
showLimitNum: makeBooleanProp(true),
|
||||
/**
|
||||
* 文件大小限制,单位为byte
|
||||
* 类型:number
|
||||
* 默认值:Number.MAX_VALUE
|
||||
*/
|
||||
maxSize: makeNumberProp(Number.MAX_VALUE),
|
||||
/**
|
||||
* 选择图片的来源,chooseImage接口详细参数,查看官方手册
|
||||
* 类型:array
|
||||
* 默认值:['album','camera']
|
||||
*/
|
||||
sourceType: {
|
||||
type: Array as PropType<UploadSourceType[]>,
|
||||
default: () => ['album', 'camera']
|
||||
},
|
||||
/**
|
||||
* 所选的图片的尺寸,chooseImage接口详细参数,查看官方手册
|
||||
* 类型:array
|
||||
* 默认值:['original','compressed']
|
||||
*/
|
||||
sizeType: {
|
||||
type: Array as PropType<UploadSizeType[]>,
|
||||
// #ifndef MP-DINGTALK
|
||||
default: () => ['original', 'compressed']
|
||||
// #endif
|
||||
},
|
||||
/**
|
||||
* 文件对应的key,开发者在服务端可以通过这个key获取文件的二进制内容,uploadFile接口详细参数,查看官方手册
|
||||
* 类型:string
|
||||
* 默认值:'file'
|
||||
*/
|
||||
name: makeStringProp('file'),
|
||||
/**
|
||||
* HTTP请求中其他额外的formdata,uploadFile接口详细参数,查看官方手册
|
||||
* 类型:object
|
||||
* 默认值:{}
|
||||
*/
|
||||
formData: { type: Object as PropType<UploadFormData>, default: () => ({}) },
|
||||
/**
|
||||
* 预览失败执行操作
|
||||
* 类型:function({index,imgList})
|
||||
* 默认值:-
|
||||
*/
|
||||
onPreviewFail: Function as PropType<UploadOnPreviewFail>,
|
||||
/**
|
||||
* 上传文件之前的钩子,参数为上传的文件和文件列表,若返回false或者返回Promise且被reject,则停止上传。
|
||||
* 类型:function({files,fileList,resolve})
|
||||
* 默认值:-
|
||||
*/
|
||||
beforeUpload: Function as PropType<UploadBeforeUpload>,
|
||||
/**
|
||||
* 选择图片之前的钩子,参数为文件列表,若返回false或者返回Promise且被reject,则停止上传。
|
||||
* 类型:function({fileList,resolve})
|
||||
* 默认值:-
|
||||
*/
|
||||
beforeChoose: Function as PropType<UploadBeforeChoose>,
|
||||
/**
|
||||
* 删除文件之前的钩子,参数为要删除的文件和文件列表,若返回false或者返回Promise且被reject,则停止上传。
|
||||
* 类型:function({file,fileList,resolve})
|
||||
* 默认值:-
|
||||
*/
|
||||
beforeRemove: Function as PropType<UploadBeforeRemove>,
|
||||
/**
|
||||
* 图片预览前的钩子,参数为预览的图片下标和图片列表,若返回false或者返回Promise且被reject,则停止上传。
|
||||
* 类型:function({index,imgList,resolve})
|
||||
* 默认值:-
|
||||
*/
|
||||
beforePreview: Function as PropType<UploadBeforePreview>,
|
||||
/**
|
||||
* 构建上传formData的钩子,参数为上传的文件、待处理的formData,返回值为处理后的formData,若返回false或者返回Promise且被reject,则停止上传。
|
||||
* 类型:function({file,formData,resolve})
|
||||
* 默认值:-
|
||||
* 最低版本:0.1.61
|
||||
*/
|
||||
buildFormData: Function as PropType<UploadBuildFormData>,
|
||||
/**
|
||||
* 加载中图标类型
|
||||
* 类型:string
|
||||
* 默认值:'ring'
|
||||
*/
|
||||
loadingType: makeStringProp<LoadingType>('ring'),
|
||||
/**
|
||||
* 加载中图标颜色
|
||||
* 类型:string
|
||||
* 默认值:'#ffffff'
|
||||
*/
|
||||
loadingColor: makeStringProp('#ffffff'),
|
||||
/**
|
||||
* 文件类型,可选值:'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'),
|
||||
/**
|
||||
* 加载中图标尺寸
|
||||
* 类型:string
|
||||
* 默认值:'24px'
|
||||
*/
|
||||
loadingSize: makeStringProp('24px'),
|
||||
/**
|
||||
* 是否压缩视频,当 accept 为 video 时生效。
|
||||
* 类型:boolean
|
||||
* 默认值:true
|
||||
*/
|
||||
compressed: makeBooleanProp(true),
|
||||
/**
|
||||
* 拍摄视频最长拍摄时间,当 accept 为 video | media 时生效,单位秒。
|
||||
* 类型:number
|
||||
* 默认值:60
|
||||
*/
|
||||
maxDuration: makeNumberProp(60),
|
||||
/**
|
||||
* 使用前置或者后置相机,当 accept 为 video | media 时生效,可选值为:back|front。
|
||||
* 类型:UploadCameraType
|
||||
* 默认值:'back'
|
||||
*/
|
||||
camera: makeStringProp<UploadCameraType>('back'),
|
||||
/**
|
||||
* 预览图片的mode属性
|
||||
*/
|
||||
imageMode: makeStringProp<ImageMode>('aspectFit'),
|
||||
/**
|
||||
* 接口响应的成功状态(statusCode)值
|
||||
*/
|
||||
successStatus: makeNumberProp(200),
|
||||
/**
|
||||
* 自定义上传按钮样式
|
||||
* 类型:string
|
||||
*/
|
||||
customEvokeClass: makeStringProp(''),
|
||||
/**
|
||||
* 自定义预览图片列表样式
|
||||
* 类型:string
|
||||
*/
|
||||
customPreviewClass: makeStringProp(''),
|
||||
/**
|
||||
* 是否选择文件后自动上传
|
||||
* 类型:boolean
|
||||
*/
|
||||
autoUpload: makeBooleanProp(true),
|
||||
/**
|
||||
* 点击已上传时是否可以重新上传
|
||||
* 类型:boolean
|
||||
* 默认值:false
|
||||
*/
|
||||
reupload: makeBooleanProp(false),
|
||||
/**
|
||||
* 自定义上传文件的请求方法
|
||||
* 类型:UploadMethod
|
||||
* 默认值:-
|
||||
*/
|
||||
uploadMethod: Function as PropType<UploadMethod>,
|
||||
/**
|
||||
* 根据文件拓展名过滤,每一项都不能是空字符串。默认不过滤。
|
||||
* H5支持全部类型过滤。
|
||||
* 微信小程序支持all和file时过滤,其余平台不支持。
|
||||
*/
|
||||
extension: Array as PropType<string[]>
|
||||
}
|
||||
|
||||
export type UploadProps = ExtractPropTypes<typeof uploadProps>
|
||||
|
||||
export type UploadExpose = {
|
||||
/**
|
||||
* 手动触发上传
|
||||
*/
|
||||
submit: () => void
|
||||
/**
|
||||
* 取消上传
|
||||
* @param task 上传任务
|
||||
*/
|
||||
abort: (task?: UniApp.UploadTask) => void
|
||||
}
|
||||
|
||||
export type UploadErrorEvent = {
|
||||
error: any
|
||||
file: UploadFileItem
|
||||
formData: UploadFormData
|
||||
}
|
||||
|
||||
export type UploadChangeEvent = {
|
||||
fileList: UploadFileItem[]
|
||||
}
|
||||
|
||||
export type UploadSuccessEvent = {
|
||||
file: UploadFileItem
|
||||
fileList: UploadFileItem[]
|
||||
formData: UploadFormData
|
||||
}
|
||||
|
||||
export type UploadProgressEvent = {
|
||||
response: UniApp.OnProgressUpdateResult
|
||||
file: UploadFileItem
|
||||
}
|
||||
|
||||
export type UploadOversizeEvent = {
|
||||
file: ChooseFile
|
||||
}
|
||||
|
||||
export type UploadRemoveEvent = {
|
||||
file: UploadFileItem
|
||||
}
|
||||
|
||||
export type UploadInstance = ComponentPublicInstance<UploadProps, UploadExpose>
|
||||
673
uni_modules/wot-design-uni/components/wd-upload/wd-upload.vue
Normal file
673
uni_modules/wot-design-uni/components/wd-upload/wd-upload.vue
Normal file
@@ -0,0 +1,673 @@
|
||||
<template>
|
||||
<view :class="['wd-upload', customClass]" :style="customStyle">
|
||||
<!-- 预览列表 -->
|
||||
<view :class="['wd-upload__preview', customPreviewClass]" v-for="(file, index) in uploadFiles" :key="index">
|
||||
<!-- 成功时展示图片 -->
|
||||
<view class="wd-upload__status-content">
|
||||
<image v-if="isImage(file)" :src="file.url" :mode="imageMode" class="wd-upload__picture" @click="onPreviewImage(file)" />
|
||||
<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 custom-class="wd-upload__video-icon" name="video"></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" custom-class="wd-upload__file-icon"></wd-icon>
|
||||
<view class="wd-upload__file-name">{{ file.name || file.url }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="file[props.statusKey] !== 'success'" class="wd-upload__mask wd-upload__status-content">
|
||||
<!-- loading时展示loading图标和进度 -->
|
||||
<view v-if="file[props.statusKey] === 'loading'" class="wd-upload__status-content">
|
||||
<wd-loading :type="loadingType" :size="loadingSize" :color="loadingColor" />
|
||||
<text class="wd-upload__progress-txt">{{ file.percent }}%</text>
|
||||
</view>
|
||||
<!-- 失败时展示失败图标以及失败信息 -->
|
||||
<view v-if="file[props.statusKey] === 'fail'" class="wd-upload__status-content">
|
||||
<wd-icon name="close-outline" custom-class="wd-upload__icon"></wd-icon>
|
||||
<text class="wd-upload__progress-txt">{{ file.error || translate('error') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 上传状态为上传中时不展示移除按钮 -->
|
||||
<wd-icon
|
||||
v-if="file[props.statusKey] !== 'loading' && !disabled"
|
||||
name="error-fill"
|
||||
custom-class="wd-upload__close"
|
||||
@click="removeFile(index)"
|
||||
></wd-icon>
|
||||
<!-- 自定义预览样式 -->
|
||||
<slot name="preview-cover" v-if="$slots['preview-cover']" :file="file" :index="index"></slot>
|
||||
</view>
|
||||
|
||||
<block v-if="showUpload">
|
||||
<view :class="['wd-upload__evoke-slot', customEvokeClass]" v-if="$slots.default" @click="onEvokeClick">
|
||||
<slot></slot>
|
||||
</view>
|
||||
<!-- 唤起项 -->
|
||||
<view v-else @click="onEvokeClick" :class="['wd-upload__evoke', disabled ? 'is-disabled' : '', customEvokeClass]">
|
||||
<!-- 唤起项图标 -->
|
||||
<wd-icon class="wd-upload__evoke-icon" name="fill-camera"></wd-icon>
|
||||
<!-- 有限制个数时确认是否展示限制个数 -->
|
||||
<view v-if="limit && showLimitNum" class="wd-upload__evoke-num">({{ uploadFiles.length }}/{{ limit }})</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
<wd-video-preview ref="videoPreview"></wd-video-preview>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'wd-upload',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
virtualHost: true,
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import wdIcon from '../wd-icon/wd-icon.vue'
|
||||
import wdVideoPreview from '../wd-video-preview/wd-video-preview.vue'
|
||||
import wdLoading from '../wd-loading/wd-loading.vue'
|
||||
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { context, isEqual, isImageUrl, isVideoUrl, isFunction, isDef, deepClone } from '../common/util'
|
||||
import { useTranslate } from '../composables/useTranslate'
|
||||
import { useUpload } from '../composables/useUpload'
|
||||
import {
|
||||
uploadProps,
|
||||
type UploadFileItem,
|
||||
type ChooseFile,
|
||||
type UploadExpose,
|
||||
type UploadErrorEvent,
|
||||
type UploadChangeEvent,
|
||||
type UploadSuccessEvent,
|
||||
type UploadProgressEvent,
|
||||
type UploadOversizeEvent,
|
||||
type UploadRemoveEvent,
|
||||
type UploadMethod
|
||||
} from './types'
|
||||
import type { VideoPreviewInstance } from '../wd-video-preview/types'
|
||||
|
||||
const props = defineProps(uploadProps)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'fail', value: UploadErrorEvent): void
|
||||
(e: 'change', value: UploadChangeEvent): void
|
||||
(e: 'success', value: UploadSuccessEvent): void
|
||||
(e: 'progress', value: UploadProgressEvent): void
|
||||
(e: 'oversize', value: UploadOversizeEvent): void
|
||||
(e: 'chooseerror', value: any): void
|
||||
(e: 'remove', value: UploadRemoveEvent): void
|
||||
(e: 'update:fileList', value: UploadFileItem[]): void
|
||||
}>()
|
||||
|
||||
defineExpose<UploadExpose>({
|
||||
submit: () => startUploadFiles(),
|
||||
abort: () => abort()
|
||||
})
|
||||
|
||||
const { translate } = useTranslate('upload')
|
||||
|
||||
const uploadFiles = ref<UploadFileItem[]>([])
|
||||
|
||||
const showUpload = computed(() => !props.limit || uploadFiles.value.length < props.limit)
|
||||
|
||||
const videoPreview = ref<VideoPreviewInstance>()
|
||||
|
||||
const { startUpload, abort, chooseFile, UPLOAD_STATUS } = useUpload()
|
||||
|
||||
watch(
|
||||
() => props.fileList,
|
||||
(val) => {
|
||||
const { statusKey } = props
|
||||
if (isEqual(val, uploadFiles.value)) return
|
||||
const uploadFileList: UploadFileItem[] = val.map((item) => {
|
||||
item[statusKey] = item[statusKey] || 'success'
|
||||
item.response = item.response || ''
|
||||
return { ...item, uid: context.id++ }
|
||||
})
|
||||
uploadFiles.value = uploadFileList
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.limit,
|
||||
(val) => {
|
||||
if (val && val < uploadFiles.value.length) {
|
||||
console.error('[wot-design]Error: props limit must less than fileList.length')
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.beforePreview,
|
||||
(fn) => {
|
||||
if (fn && !isFunction(fn)) {
|
||||
console.error('The type of beforePreview must be Function')
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.onPreviewFail,
|
||||
(fn) => {
|
||||
if (fn && !isFunction(fn)) {
|
||||
console.error('The type of onPreviewFail must be Function')
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.beforeRemove,
|
||||
(fn) => {
|
||||
if (fn && !isFunction(fn)) {
|
||||
console.error('The type of beforeRemove must be Function')
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.beforeUpload,
|
||||
(fn) => {
|
||||
if (fn && !isFunction(fn)) {
|
||||
console.error('The type of beforeUpload must be Function')
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.beforeChoose,
|
||||
(fn) => {
|
||||
if (fn && !isFunction(fn)) {
|
||||
console.error('The type of beforeChoose must be Function')
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.buildFormData,
|
||||
(fn) => {
|
||||
if (fn && !isFunction(fn)) {
|
||||
console.error('The type of buildFormData must be Function')
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
function emitFileList() {
|
||||
emit('update:fileList', uploadFiles.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始上传文件
|
||||
*/
|
||||
function startUploadFiles() {
|
||||
const { buildFormData, formData = {}, statusKey } = props
|
||||
const { action, name, header = {}, accept, successStatus, uploadMethod } = props
|
||||
const statusCode = isDef(successStatus) ? successStatus : 200
|
||||
|
||||
for (const uploadFile of uploadFiles.value) {
|
||||
// 仅开始未上传的文件
|
||||
if (uploadFile[statusKey] === UPLOAD_STATUS.PENDING) {
|
||||
if (buildFormData) {
|
||||
buildFormData({
|
||||
file: uploadFile,
|
||||
formData,
|
||||
resolve: (formData: Record<string, any>) => {
|
||||
formData &&
|
||||
startUpload(uploadFile, {
|
||||
action,
|
||||
header,
|
||||
name,
|
||||
formData,
|
||||
fileType: accept as 'image' | 'video' | 'audio',
|
||||
statusCode,
|
||||
statusKey,
|
||||
uploadMethod,
|
||||
onSuccess: handleSuccess,
|
||||
onError: handleError,
|
||||
onProgress: handleProgress
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
startUpload(uploadFile, {
|
||||
action,
|
||||
header,
|
||||
name,
|
||||
formData,
|
||||
fileType: accept as 'image' | 'video' | 'audio',
|
||||
statusCode,
|
||||
statusKey,
|
||||
uploadMethod,
|
||||
onSuccess: handleSuccess,
|
||||
onError: handleError,
|
||||
onProgress: handleProgress
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图片信息
|
||||
* @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: ChooseFile, currentIndex?: number) {
|
||||
const { statusKey } = props
|
||||
// 状态初始化
|
||||
const initState: UploadFileItem = {
|
||||
uid: context.id++,
|
||||
// 仅h5支持 name
|
||||
name: file.name || '',
|
||||
thumb: file.thumb || '',
|
||||
[statusKey]: 'pending',
|
||||
size: file.size || 0,
|
||||
url: file.path,
|
||||
percent: 0
|
||||
}
|
||||
if (typeof currentIndex === 'number') {
|
||||
uploadFiles.value.splice(currentIndex, 1, initState)
|
||||
} else {
|
||||
uploadFiles.value.push(initState)
|
||||
}
|
||||
if (props.autoUpload) {
|
||||
startUploadFiles()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 上传失败捕获
|
||||
* @param {Object} err 错误返回信息
|
||||
* @param {Object} file 上传的文件
|
||||
*/
|
||||
function handleError(err: Record<string, any>, file: UploadFileItem, formData: Record<string, any>) {
|
||||
const { statusKey } = props
|
||||
const index = uploadFiles.value.findIndex((item) => item.uid === file.uid)
|
||||
if (index > -1) {
|
||||
uploadFiles.value[index][statusKey] = 'fail'
|
||||
uploadFiles.value[index].error = err.message
|
||||
uploadFiles.value[index].response = err
|
||||
emit('fail', { error: err, file, formData })
|
||||
emitFileList()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 上传成功捕获
|
||||
* @param {Object} res 接口返回信息
|
||||
* @param {Object} file 上传的文件
|
||||
*/
|
||||
function handleSuccess(res: Record<string, any>, file: UploadFileItem, formData: Record<string, any>) {
|
||||
const { statusKey } = props
|
||||
const index = uploadFiles.value.findIndex((item) => item.uid === file.uid)
|
||||
if (index > -1) {
|
||||
uploadFiles.value[index][statusKey] = 'success'
|
||||
uploadFiles.value[index].response = res.data
|
||||
emit('change', { fileList: uploadFiles.value })
|
||||
emit('success', { file, fileList: uploadFiles.value, formData })
|
||||
emitFileList()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 上传中捕获
|
||||
* @param {Object} res 接口返回信息
|
||||
* @param {Object} file 上传的文件
|
||||
*/
|
||||
function handleProgress(res: UniApp.OnProgressUpdateResult, file: UploadFileItem) {
|
||||
const index = uploadFiles.value.findIndex((item) => item.uid === file.uid)
|
||||
if (index > -1) {
|
||||
uploadFiles.value[index].percent = res.progress
|
||||
emit('progress', { response: res, file })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 选择文件的实际操作,将chooseFile自己用promise包了一层
|
||||
*/
|
||||
function onChooseFile(currentIndex?: number) {
|
||||
const { multiple, maxSize, accept, sizeType, limit, sourceType, compressed, maxDuration, camera, beforeUpload, extension } = props
|
||||
chooseFile({
|
||||
multiple: isDef(currentIndex) ? false : multiple,
|
||||
sizeType,
|
||||
sourceType,
|
||||
maxCount: limit ? limit - uploadFiles.value.length : limit,
|
||||
accept,
|
||||
compressed,
|
||||
maxDuration,
|
||||
camera,
|
||||
extension
|
||||
})
|
||||
.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, currentIndex) : emit('oversize', { file })
|
||||
}
|
||||
}
|
||||
|
||||
// 上传前的钩子
|
||||
if (beforeUpload) {
|
||||
beforeUpload({
|
||||
files,
|
||||
fileList: uploadFiles.value,
|
||||
resolve: (isPass: boolean) => {
|
||||
isPass && mapFiles(files)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
mapFiles(files)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
emit('chooseerror', { error })
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 处理唤起选择文件的点击事件
|
||||
*/
|
||||
function onEvokeClick() {
|
||||
handleChoose()
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 选择文件,内置拦截选择操作
|
||||
*/
|
||||
function handleChoose(index?: number) {
|
||||
if (props.disabled) return
|
||||
const { beforeChoose } = props
|
||||
|
||||
// 选择图片前的钩子
|
||||
if (beforeChoose) {
|
||||
beforeChoose({
|
||||
fileList: uploadFiles.value,
|
||||
resolve: (isPass: boolean) => {
|
||||
isPass && onChooseFile(index)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
onChooseFile(index)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 移除文件
|
||||
* @param {Object} file 上传的文件
|
||||
* @param {Number} index 删除
|
||||
*/
|
||||
function handleRemove(file: UploadFileItem) {
|
||||
uploadFiles.value.splice(
|
||||
uploadFiles.value.findIndex((item) => item.uid === file.uid),
|
||||
1
|
||||
)
|
||||
emit('change', {
|
||||
fileList: uploadFiles.value
|
||||
})
|
||||
emit('remove', { file })
|
||||
emitFileList()
|
||||
}
|
||||
|
||||
function removeFile(index: number) {
|
||||
const { beforeRemove } = props
|
||||
const intIndex: number = index
|
||||
const file = uploadFiles.value[intIndex]
|
||||
if (beforeRemove) {
|
||||
beforeRemove({
|
||||
file,
|
||||
index: intIndex,
|
||||
fileList: uploadFiles.value,
|
||||
resolve: (isPass: boolean) => {
|
||||
isPass && handleRemove(file)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
handleRemove(file)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览文件
|
||||
* @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,
|
||||
current: lists[index],
|
||||
fail() {
|
||||
if (onPreviewFail) {
|
||||
onPreviewFail({
|
||||
index,
|
||||
imgList: lists
|
||||
})
|
||||
} else {
|
||||
uni.showToast({ title: '预览图片失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览视频
|
||||
* @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(file: UploadFileItem) {
|
||||
const { beforePreview, reupload } = props
|
||||
const fileList = deepClone(uploadFiles.value)
|
||||
const index: number = fileList.findIndex((item) => item.url === file.url)
|
||||
const imgList = fileList.filter((file) => isImage(file)).map((file) => file.url)
|
||||
const imgIndex: number = imgList.findIndex((item) => item === file.url)
|
||||
if (reupload) {
|
||||
handleChoose(index)
|
||||
} else {
|
||||
if (beforePreview) {
|
||||
beforePreview({
|
||||
file,
|
||||
index,
|
||||
fileList: fileList,
|
||||
imgList: imgList,
|
||||
resolve: (isPass: boolean) => {
|
||||
isPass && handlePreviewImage(imgIndex, imgList)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
handlePreviewImage(imgIndex, imgList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onPreviewVideo(file: UploadFileItem) {
|
||||
const { beforePreview, reupload } = props
|
||||
const fileList = deepClone(uploadFiles.value)
|
||||
const index: number = fileList.findIndex((item) => item.url === file.url)
|
||||
const videoList = fileList.filter((file) => isVideo(file))
|
||||
const videoIndex: number = videoList.findIndex((item) => item.url === file.url)
|
||||
if (reupload) {
|
||||
handleChoose(index)
|
||||
} else {
|
||||
if (beforePreview) {
|
||||
beforePreview({
|
||||
file,
|
||||
index,
|
||||
imgList: [],
|
||||
fileList,
|
||||
resolve: (isPass: boolean) => {
|
||||
isPass && handlePreviewVieo(videoIndex, videoList)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
handlePreviewVieo(videoIndex, videoList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onPreviewFile(file: UploadFileItem) {
|
||||
const { beforePreview, reupload } = props
|
||||
const fileList = deepClone(uploadFiles.value)
|
||||
const index: number = fileList.findIndex((item) => item.url === file.url)
|
||||
if (reupload) {
|
||||
handleChoose(index)
|
||||
} else {
|
||||
if (beforePreview) {
|
||||
beforePreview({
|
||||
file,
|
||||
index,
|
||||
imgList: [],
|
||||
fileList,
|
||||
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';
|
||||
</style>
|
||||
Reference in New Issue
Block a user