更新:登录功能

This commit is contained in:
2025-11-04 12:37:04 +08:00
commit a21fb92916
897 changed files with 51500 additions and 0 deletions

View 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%;
}
}

View 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 时生效可选值为backfront。
*/
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请求中其他额外的formdatauploadFile接口详细参数查看官方手册
* 类型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 时生效可选值为backfront。
* 类型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>

View 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>