UNPKG

@cataract6545/tmui

Version:

tm-vuetify是一个新势力由主题驱动的UI组件库,相比其它优势大,组件全,设计趋势紧跟未来。具有主题生成,主题实时切换,暗黑实时切换,lottie动画,图表等新颖功能,tmui TMUI

655 lines (595 loc) 23.4 kB
import { reactive, ref, watchEffect } from "vue" /** * 文件上传HOOKS * 只限h5,微信,qq使用。其它平台请自己修改实现。 * @description 鉴于大家可能需要只是上传功能,可能界面需要自己定制,那么这个hooks函数将提升你的效率。你仅需要专注于界面设计,其它的交给本函数。 * @author tmui|tmzdy|https://tmui.design * @version 1.0.0 * 2023-10-7 22:51:00 by tmui * @copyright 本代码只允许跟随tmui组件库发布。如果需要其它地方合并使用,请保留此注释版权信息。 */ //文件对象结构 export interface FILE_TYPE { url: string,//当前显示的图片地址,这是个本地临时地址。待上传前,成功后会替换服务器地址。 status?: string,//上传状态文本 progress?: number,//当前文件上传的进度 uid?: string | number,//文件唯一标识id statusCode?: STATUS_CODE,//文件状态 response?: any,//上传成功后的回调数据。 name?: string,//文件名称 size?: number,//文件大小字节单位 FILE?: any,//文件对象。 [key: string]: any } //文件上传的状态值 export enum STATUS_CODE { //待上传 upload = 0, //上传中 uploading = 1, //上传失败 fail = 2, //上传成功 success = 3, //超过大小限制 max = 4, } export interface USE_UPLOAD_FILE_CONFIG_TYPE { /**media表示只允许图片或者视频选择,file表示允许任意文件,但只支持h5,微信平台 */ uploadType:"media"|"file", maxCount: number,//一次选择文件最大数量。 extension: string[],//文件选择的类型。 type: "all" | "image" | "video" | "file" | undefined, /**只对h5 */ sourceType: Array<'album' | 'camera'>, maxSize: number,//每一个文件上传的最大尺寸,默认为10mb hostUrl: string,//上传文件的服务器地址 autoUpload: boolean, header: { [key: string]: any },//头部参数。 formData: { [key: string]: any },//额外的表单数据。 formName: string, code: number,//服务器响应码,如果不为此码,表示上传失败。 maxDuration:number,//如果选择的类型是视频,可以定制此拍摄的最大时长。仅uploadType为media时有效 sizeType:Array<'original'|'compressed'>,//仅对 mediaType 为 image 时有效,是否压缩所选文件,仅uploadType为media时有效 camera:'back'|'front',//仅在 sourceType 为 camera 时生效,使用前置或后置摄像头,仅uploadType为media时有效 mediaType:Array<'image'|'video'>,//注意当uploadType=media时,如果是微信,抖音,飞书这里可以为当前正常类型值;如果是其它平台:只取数组中第一个值,比如要选择图片设置为["image"],视频:["video"],仅uploadType为media时有效 } export function getUid(length = 3) { return Number(Number(Math.random().toString().substr(3, length) + Date.now()).toString(8)); } export const useUploadFile = (cfg:USE_UPLOAD_FILE_CONFIG_TYPE|null) => { const config = ref<USE_UPLOAD_FILE_CONFIG_TYPE>({ uploadType:'file', extension: [], type: "all", sourceType: ['album', 'camera'], maxSize: 10 * 1024 * 1024,//每一个文件上传的最大尺寸,默认为10mb maxCount: 9,//一次选择文件最大数量。 // 测试地址:https://mockapi.eolink.com/tNYKNA7ac71aa90bcbe83c5815871a5b419601e96a5524d/upload hostUrl: "",//上传文件的服务器地址 autoUpload: true, header: Object,//头部参数。 formData: {},//额外的表单数据。 formName: 'file', code: 200, maxDuration:20, sizeType:['original', 'compressed'], camera:'back', mediaType:['image','video'] }) config.value = uni.$tm.u.deepObjectMerge(config.value, cfg || {}); //文件列表。 const list = ref<FILE_TYPE[]>([]) //当前是否正在上传中,true表示上传中,false表示中断/或者未开始上传中。 const uploading = ref(false); //当前上传的索引位置。 const activeIndex = ref(0) //是否中断上传。 let isStopUpload = false; //是否已经超过了限制数量 let isLimitCount = ref(false) let uploadTask: UniApp.UploadTask|null = null; /** * 选择文件前执行 * @param cfg 全局参数,可以修改 * @return {USE_UPLOAD_FILE_CONFIG_TYPE|null} 需要返回配置,如果不想修改返回null即可。 */ let beforeChooseFile = (cfg: USE_UPLOAD_FILE_CONFIG_TYPE | null = null) => { return cfg } /** * 选择文件后执行 * @param files 选择后的文件列表对象, * @return {FILE_TYPE[]} 必须要返回文件列表,可以在这里修改待上传的文件列表地址。 */ let chooseFileAfter = (files: FILE_TYPE[]) => { return files } /** * 所有文件上传结束时触发。 * @param files 当前上传的文件列表 */ let uploadComplete = (files: FILE_TYPE[]) => { } /** * 某一个文件上传结束时触发,不管失败与否都会触发。 * @param file 当前上传的文件 * @param files 当前上传的文件列表 */ let complete = (file: FILE_TYPE, files: FILE_TYPE[]) => { } /** * 某一个文件上传失败时触发 * @param file 当前上传的文件 * @param files 当前上传的文件列表 */ let fail = (file: FILE_TYPE, files: FILE_TYPE[]) => { } /** * 某一个文件上传成功时触发 * @param file 当前上传的文件 * @param files 当前上传的文件列表 * @returns {FILE_TYPE} 需要返回本文件 */ let success = (file: FILE_TYPE, files: FILE_TYPE[]) => { } /** * 某一个文件上传成功后触发 * @param file 当前上传的文件 * @param files 当前上传的文件列表 * @returns {FILE_TYPE} 需要返回本文件,你可以在此修改文件的statusCode,status状态等,将会最终影响文件是否真的上传成功或者修改返回值等等。 */ let successAfter = (file: FILE_TYPE, files: FILE_TYPE[]) => { return file; } /** * 任意一个文件上传前触发 * @param file 当前上传的文件 * @param files 当前上传的文件列表 * @returns {boolean} 如果返回true将继续上传,如果返回false将阻止本次上传并设置为该文件为上传失败,然后接着上传下一个文件。 */ let beforeFileStart = (file: FILE_TYPE, files: FILE_TYPE[]):Promise<boolean>|boolean => { return true; } /** * 开始上传前触发 * @param cfg 当前上传文件的配置表 * @returns {boolean} 如果返回false将中止本次整体的上传。 */ let beforeStart = (cfg:USE_UPLOAD_FILE_CONFIG_TYPE):Promise<boolean>|boolean => { return true; } /** * 删除前触发 * @param file 当前上传文件 * @returns {boolean} 如果返回false将不允许删除 */ let beforeRemove = (file: FILE_TYPE):Promise<boolean>|boolean => { return true; } /** * 任意事件变动触发,管是中断,上传结束,失败等都会触发。 * @param file 变动的文件,可能为null */ let change = (file: FILE_TYPE|null) => { } /** * 选择文件 */ function choose(type:"file"|"media" = 'file'): Promise<FILE_TYPE[]> { if (list.value.length >= config.value.maxCount) { uni.showToast({ title: "超过上传数量", icon: "none", mask: true }); return Promise.reject("超过上传数量"); } //执行上传的勾子。 let temconfig = beforeChooseFile(uni.$tm.u.deepClone(config.value)); config.value = uni.$tm.u.deepObjectMerge(config.value, temconfig || {}); let cfg = { count: config.value.maxCount, extension: config.value.extension, type: config.value.type, mediaType:config.value.mediaType, sourceType:config.value.sourceType, maxDuration:config.value.maxDuration, sizeType:config.value.sizeType, camera:config.value.camera, } if (typeof cfg.extension === 'undefined' || !cfg.extension || cfg.extension?.length === 0) { delete cfg.extension; } return new Promise((resolve, rejects) => { let apiName = uni?.chooseFile; let ischooseVideo = false; // #ifdef MP-WEIXIN || MP-QQ apiName = uni?.chooseMessageFile // #endif // #ifdef H5 apiName = uni?.chooseFile; // #endif if(config.value.uploadType == 'media'){ // #ifdef MP-WEIXIN || MP-JD || MP-TOUTIAO || MP-LARK apiName = uni?.chooseMedia; // #endif // #ifndef MP-WEIXIN || MP-JD || MP-TOUTIAO || MP-LARK if(!config.value.mediaType[0]||config.value.mediaType[0]==='image'){ apiName = uni?.chooseImage; }else if(config.value.mediaType[0]==='video'){ apiName = uni?.chooseVideo; ischooseVideo=true; } // #endif } console.log(apiName) if(!apiName){ uni.showModal({ title:"警告", content:"当前只支持微信,QQ,webPC或者H5平台,其它平台不支持文件上传", showCancel:false, confirm:"懂了", }) return; } // @ts-ignore apiName({ ...cfg, fail(result) { uni.showToast({ title: typeof result == 'object' ? JSON.stringify(result) : result, icon: "none", mask: true }); rejects(typeof result == 'object' ? JSON.stringify(result) : result) }, success(res) { let temFiles = res?.tempFiles; console.log(res) let temlist: any[] = []; if (Array.isArray(temFiles)) { temFiles.forEach(ele => { temlist.push({ uid: getUid(4), url: ele.path, progress: 0, statusCode: ele.size > config.value.maxSize ? STATUS_CODE.max : STATUS_CODE.upload, status: ele.size > config.value.maxSize ? '超过大小' : '待上传', response: null, name: ele.name, FILE: ele }) }); } else { if(ischooseVideo){ temFiles = { file:res.tempFile, name:res.name, size:res.size, path:res.tempFilePath } } temlist.push({ uid: getUid(4), url: temFiles.path, progress: 0, statusCode: temFiles.size > config.value.maxSize ? STATUS_CODE.max : STATUS_CODE.upload, status: temFiles.size > config.value.maxSize ? '超过大小' : '待上传', response: null, name: temFiles.name, FILE: ischooseVideo?temFiles.file:temFiles }) } let syuCount = Math.max(config.value.maxCount - list.value.length, 0) temlist = [...temlist.slice(0, syuCount)] temlist = chooseFileAfter(temlist); list.value.push(...temlist) if(config.value.autoUpload){ start(); } resolve([...temlist]) } }) }) } /** * 删除指定文件 * @param file 可以是索引或者文件FILE_TYPE对象 */ async function remove(file: number | FILE_TYPE) { let removeIndex = null; if (typeof file === 'number') { removeIndex = file; } else { let uid = file.uid; let index = list.value.findIndex(ele => ele.uid === uid); if (index > -1) { removeIndex = index; } } if (typeof removeIndex === null) { uni.showToast({ title: "删除失败,无对应文件", icon: "none", mask: true }); return; } if (typeof removeIndex === 'number') { if (!list.value[removeIndex]) { uni.showToast({ title: "删除失败,无对应文件", icon: "none", mask: true }); return; } //是否允许删除 let isDel = true; if(beforeRemove instanceof Promise){ isDel = await beforeRemove(uni.$tm.u.deepClone(list.value[removeIndex])); }else{ isDel = beforeRemove(uni.$tm.u.deepClone(list.value[removeIndex])); } if(isDel){ if(uploading.value){ uni.showToast({ title: "上传中,暂无法操作", icon: "none", mask: true }); return; } list.value.splice(removeIndex, 1); change(uni.$tm.u.deepClone(list.value[removeIndex])) }else{ uni.showToast({ title: "删除失败,不允许", icon: "none", mask: true }); return; } } } /** * 添加已有(上传文件) * 通常是服务器返回时的数据,用来返选用。 * @param files 需要添加的静态文件 */ function addFile(files: string | FILE_TYPE | FILE_TYPE[] | string[] | Array<string | FILE_TYPE>) { let temList: any[] = []; if (typeof files === 'string') { temList.push({ uid: getUid(4), url: files, progress: 100, statusCode: STATUS_CODE.success, status: '已上传', response: JSON.stringify({ code: 0, msg: "上传成功", data: files }), name: files, FILE: null }) } else if (typeof files === 'object') { if (Array.isArray(files)) { files.forEach(el => { if (typeof el === 'string') { temList.push({ uid: getUid(4), url: el, progress: 100, statusCode: STATUS_CODE.success, status: '已上传', response: JSON.stringify({ code: 0, msg: "上传成功", data: el }), name: el, FILE: null }) } else if (typeof el === 'object') { const indexNow = list.value.findIndex(el=>el.uid===files?.uid) if(indexNow==-1){ temList.push({ uid: getUid(4), progress: 100, statusCode: STATUS_CODE.success, status: '已上传', response: JSON.stringify(el?.response ?? { code: 0, msg: "上传成功", data: el }), name: files, FILE: null, ...files }) } } }) } else { const indexNow = list.value.findIndex(el=>el.uid===files?.uid) if(indexNow==-1){ temList.push({ uid: getUid(4), progress: 100, statusCode: STATUS_CODE.success, status: '已上传', response: JSON.stringify(files?.response ?? { code: 0, msg: "上传成功", data: files }), name: files, FILE: null, ...files }) } } } list.value.push(...temList); } /** * 停止当前的上传 */ function stop() { isStopUpload = true; uploading.value = false; uploadTask?.abort(); change(null) } /** * 清空文件 * @param type {"fail"|"success"|"all" } 清除文件类型 */ function clearFile(type:"fail"|"success"|"all" = "all"){ if(uploading.value){ uni.showToast({ title: "任务进行中", icon: "none", mask: true }); return; } list.value = []; change(null) } /** * 获取文件 * @param type {"fail"|"success"|"all" } 获取当前所有文件时的筛选条件 */ function getFile(type:"fail"|"success"|"all" = "success"){ let temp = uni.$tm.u.deepClone(list.value); let temlist:FILE_TYPE[] = []; temp.forEach(el=>{ if(type == 'all'){ temlist.push(el) }else if(type == 'fail'){ if(el.statusCode == STATUS_CODE.fail || el.statusCode == STATUS_CODE.max){ temlist.push(el) } }else if(type == 'success'){ if(el.statusCode == STATUS_CODE.success){ temlist.push(el) } } }) return temlist; } async function start() { if (list.value.length <= 0) { uni.showToast({ title: "没有文件可上传", icon: "none", mask: true }); return; } // await beforeStart(uni.$tm.u.deepClone(config.value)); let isupload = true if(beforeStart instanceof Promise ){ isupload = await beforeStart(uni.$tm.u.deepClone(config.value)); }else{ // @ts-ignore isupload = beforeStart(uni.$tm.u.deepClone(config.value)); } if (uploading.value || !isupload) return; uploading.value = true; activeIndex.value = 0; isStopUpload = false; change(null) _upload(); } async function _upload() { if (isStopUpload) return; let nowItem = reactive(list.value[activeIndex.value]) //上结束 if (!nowItem || typeof nowItem === 'undefined') { uploading.value = false; uploadComplete(uni.$tm.u.deepClone(list.value)) change(null) return; } //成功,超过大小,上传中需要跳过上传直接下一个文件。 if(nowItem.statusCode == STATUS_CODE.success || nowItem.statusCode == STATUS_CODE.max || nowItem.statusCode == STATUS_CODE.uploading){ activeIndex.value+=1; change(uni.$tm.u.deepClone(nowItem)) _upload(); return; } //是否允许本次上传。 let isupload = true // 执行上传前项目的勾子。 if(beforeFileStart instanceof Promise ){ isupload = await beforeFileStart(uni.$tm.u.deepClone(nowItem),uni.$tm.u.deepClone(list.value)); }else{ // @ts-ignore isupload = beforeFileStart(uni.$tm.u.deepClone(nowItem),uni.$tm.u.deepClone(list.value)); } //跳过本次上传。 if(!isupload){ nowItem.status = "请更换文件"; nowItem.statusCode = STATUS_CODE.fail; activeIndex.value+=1; change(uni.$tm.u.deepClone(nowItem)) _upload(); return; } //正式进入上传阶段。 nowItem.status = "上传中.." nowItem.statusCode = STATUS_CODE.uploading; uploadTask = uni.uploadFile({ url:config.value.hostUrl, name:config.value.formName||"file", filePath:nowItem.url, header:config.value.header||{}, formData:{name:nowItem.name,...config.value.formData}, fail(result) { nowItem.status = "上传失败"; nowItem.statusCode = STATUS_CODE.fail; uni.showToast({ title: "上传失败", icon: "none", mask: true }); fail(uni.$tm.u.deepClone(nowItem),uni.$tm.u.deepClone(list.value)); activeIndex.value+=1; }, success(result) { if(result.statusCode !==config.value.code){ nowItem.status = "上传失败"; nowItem.statusCode = STATUS_CODE.fail; uni.showToast({ title: result.errMsg, icon: "none", mask: true }); fail(uni.$tm.u.deepClone(nowItem),uni.$tm.u.deepClone(list.value)); activeIndex.value+=1; return; } let temp = uni.$tm.u.deepClone(nowItem); temp.status = "上传成功"; temp.statusCode = STATUS_CODE.success; temp.progress = 100; temp.response = result.data; let chuliitem = successAfter(temp,uni.$tm.u.deepClone(list.value)) for(let key in chuliitem){ nowItem[key] = chuliitem[key]; } success(uni.$tm.u.deepClone(nowItem),uni.$tm.u.deepClone(list.value)) activeIndex.value+=1; }, complete(result) { complete(uni.$tm.u.deepClone(nowItem),uni.$tm.u.deepClone(list.value)); change(uni.$tm.u.deepClone(nowItem)) if (isStopUpload) return _upload(); }, }) uploadTask.onProgressUpdate((result)=>{ nowItem.progress = result.progress; nowItem.statusCode = STATUS_CODE.uploading; nowItem.status = "进度:"+parseInt(result.progress.toFixed(1))+"%"; }) } /** 添加事件函数 */ function addEventListener( type:'change'|'beforeRemove'|'beforeStart'|'beforeFileStart'|'successAfter'|'success'|'fail'|'complete'|'uploadComplete'|'chooseFileAfter', Fun:any ){ if(type == 'change'){ change = Fun; }else if(type == 'beforeRemove'){ beforeRemove = Fun; }else if(type == 'beforeStart'){ beforeStart = Fun; }else if(type == 'beforeFileStart'){ beforeFileStart = Fun; }else if(type == 'successAfter'){ successAfter = Fun; }else if(type == 'success'){ success = Fun; }else if(type == 'fail'){ fail = Fun; }else if(type == 'complete'){ complete = Fun; }else if(type == 'uploadComplete'){ uploadComplete = Fun; }else if(type == 'chooseFileAfter'){ chooseFileAfter = Fun; } } watchEffect(()=>{ if(list.value.length>=config.value.maxCount){ isLimitCount.value = true; }else{ isLimitCount.value = false; } }) return { config, choose, files: list, remove, addFile, start, stop, clearFile, getFile, uploading, activeIndex, isLimitCount, addEventListener } }