更新:登录功能

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