diff --git a/example/pages/uploader/index.js b/example/pages/uploader/index.js index ef14f611..78f82fa9 100644 --- a/example/pages/uploader/index.js +++ b/example/pages/uploader/index.js @@ -5,7 +5,7 @@ Page({ fileList1: [], fileList2: [ { url: 'https://img.yzcdn.cn/vant/leaf.jpg' }, - { url: 'https://img.yzcdn.cn/vant/tree.jpg' } + { url: 'https://img.yzcdn.cn/vant/tree.jpg' }, ], fileList3: [{ url: 'https://img.yzcdn.cn/vant/sand.jpg' }], fileList4: [], @@ -17,14 +17,14 @@ Page({ { url: 'https://img.yzcdn.cn/vant/leaf.jpg', status: 'uploading', - message: '上传中' + message: '上传中', }, { url: 'https://img.yzcdn.cn/vant/tree.jpg', status: 'failed', - message: '上传失败' - } - ] + message: '上传失败', + }, + ], }, beforeRead(event) { @@ -39,6 +39,7 @@ Page({ afterRead(event) { const { file, name } = event.detail; + console.log(JSON.stringify(file, null, 2)); const fileList = this.data[`fileList${name}`]; this.setData({ [`fileList${name}`]: fileList.concat(file) }); @@ -67,12 +68,12 @@ Page({ this.uploadFilePromise(`my-photo${index}.png`, file) ); Promise.all(uploadTasks) - .then(data => { + .then((data) => { wx.showToast({ title: '上传成功', icon: 'none' }); - const fileList = data.map(item => ({ url: item.fileID })); + const fileList = data.map((item) => ({ url: item.fileID })); this.setData({ cloudPath: data, fileList6: fileList }); }) - .catch(e => { + .catch((e) => { wx.showToast({ title: '上传失败', icon: 'none' }); console.log(e); }); @@ -82,7 +83,7 @@ Page({ uploadFilePromise(fileName, chooseResult) { return wx.cloud.uploadFile({ cloudPath: fileName, - filePath: chooseResult.path + filePath: chooseResult.path, }); - } + }, }); diff --git a/packages/common/utils.ts b/packages/common/utils.ts index 9cf3d061..d3285f7d 100644 --- a/packages/common/utils.ts +++ b/packages/common/utils.ts @@ -1,3 +1,5 @@ +import { isNumber, isPlainObject } from './validator'; + export function isDef(value: any): boolean { return value !== undefined && value !== null; } @@ -7,10 +9,6 @@ export function isObj(x: any): boolean { return x !== null && (type === 'object' || type === 'function'); } -export function isNumber(value) { - return /^\d+(\.\d+)?$/.test(value); -} - export function range(num: number, min: number, max: number) { return Math.min(Math.max(num, min), max); } @@ -55,6 +53,20 @@ export function requestAnimationFrame(cb: Function) { }); } +export function pickExclude(obj: unknown, keys: string[]) { + if (!isPlainObject(obj)) { + return {}; + } + + return Object.keys(obj).reduce((prev, key) => { + if (!keys.includes(key)) { + prev[key] = obj[key]; + } + + return prev; + }, {}); +} + export function getRect( this: WechatMiniprogram.Component.TrivialInstance, selector: string diff --git a/packages/common/validator.ts b/packages/common/validator.ts new file mode 100644 index 00000000..f7089750 --- /dev/null +++ b/packages/common/validator.ts @@ -0,0 +1,39 @@ +export function isFunction(val: unknown): val is Function { + return typeof val === 'function'; +} + +export function isPlainObject(val: unknown): val is Record { + return val !== null && typeof val === 'object' && !Array.isArray(val); +} + +export function isPromise(val: unknown): val is Promise { + return isPlainObject(val) && isFunction(val.then) && isFunction(val.catch); +} + +export function isDef(value: any): boolean { + return value !== undefined && value !== null; +} + +export function isObj(x: unknown): x is Record { + const type = typeof x; + return x !== null && (type === 'object' || type === 'function'); +} + +export function isNumber(value: string) { + return /^\d+(\.\d+)?$/.test(value); +} + +export function isBoolean(value: unknown): value is boolean { + return typeof value === 'boolean'; +} + +const IMAGE_REGEXP = /\.(jpeg|jpg|gif|png|svg|webp|jfif|bmp|dpg)/i; +const VIDEO_REGEXP = /\.(mp4|mpg|mpeg|dat|asf|avi|rm|rmvb|mov|wmv|flv|mkv)/i; + +export function isImageUrl(url: string): boolean { + return IMAGE_REGEXP.test(url); +} + +export function isVideoUrl(url: string): boolean { + return VIDEO_REGEXP.test(url); +} diff --git a/packages/uploader/README.md b/packages/uploader/README.md index 6ba1f0df..3c03a98b 100644 --- a/packages/uploader/README.md +++ b/packages/uploader/README.md @@ -27,6 +27,7 @@ Page({ data: { fileList: [], }, + afterRead(event) { const { file } = event.detail; // 当设置 mutiple 为 true 时, file 为数组格式,否则为对象格式 @@ -48,7 +49,7 @@ Page({ ### 图片预览 -通过向组件传入`file-list`属性,可以绑定已经上传的图片列表,并展示图片列表的预览图 +通过向组件传入`file-list`属性,可以绑定已经上传的图片列表,并展示图片列表的预览图。file-list 的详细结构可见下方。 ```html @@ -58,10 +59,14 @@ Page({ Page({ data: { fileList: [ - { url: 'https://img.yzcdn.cn/vant/leaf.jpg', name: '图片1' }, + { + url: 'https://img.yzcdn.cn/vant/leaf.jpg', + name: '图片1', + }, // Uploader 根据文件后缀来判断是否为图片文件 // 如果图片 URL 中不包含类型信息,可以添加 isImage 标记来声明 { + url: 'http://iph.href.lu?text=default', url: 'http://iph.href.lu/60x60?text=default', name: '图片2', isImage: true, @@ -74,7 +79,7 @@ Page({ ### 图片编辑状态 -通过`deletable `可以标识所有图片或者单张图片是否可删除。如果`Props `的全局`deletable `为`false`,则所有图片都不展示删除按钮;如果`Props `的全局`deletable `为`true`,则可通过设置每一个图片对象里的`deletable `来控制每一张图片是否显示删除按钮,如果图片对象里不设置则默认为`true`。 +通过`deletable`可以标识所有图片或者单张图片是否可删除。如果`Props`的全局`deletable`为`false`,则所有图片都不展示删除按钮;如果`Props`的全局`deletable`为`true`,则可通过设置每一个图片对象里的`deletable`来控制每一张图片是否显示删除按钮,如果图片对象里不设置则默认为`true`。 ```html @@ -153,6 +158,7 @@ Page({ ```html ({ ...item, - isImage: - typeof item.isImage === 'undefined' - ? isImageFile(item) - : item.isImage, - deletable: - typeof item.deletable === 'undefined' ? true : item.deletable, + isImage: isImageFile(item), + isVideo: isVideoFile(item), + deletable: isBoolean(item.deletable) ? item.deletable : true, })); + + console.log(lists); + this.setData({ lists, isInCount: lists.length < maxCount }); }, @@ -100,18 +101,9 @@ VantComponent({ maxCount: maxCount - lists.length, }) .then((res) => { - let file = null; + console.log(res); - if (isVideo(res, accept)) { - file = { - path: res.tempFilePath, - ...res, - }; - } else { - file = multiple ? res.tempFiles : res.tempFiles[0]; - } - - this.onBeforeRead(file); + this.onBeforeRead(multiple ? res : res[0]); }) .catch((error) => { this.$emit('error', error); @@ -120,14 +112,14 @@ VantComponent({ onBeforeRead(file) { const { beforeRead, useBeforeRead } = this.data; - let res: boolean | Promise = true; + let res: boolean | Promise = true; if (typeof beforeRead === 'function') { res = beforeRead(file, this.getDetail()); } if (useBeforeRead) { - res = new Promise((resolve, reject) => { + res = new Promise((resolve, reject) => { this.$emit('before-read', { file, ...this.getDetail(), @@ -150,7 +142,7 @@ VantComponent({ }, onAfterRead(file) { - const { maxSize } = this.data; + const { maxSize, afterRead } = this.data; const oversize = Array.isArray(file) ? file.some((item) => item.size > maxSize) : file.size > maxSize; @@ -160,8 +152,8 @@ VantComponent({ return; } - if (typeof this.data.afterRead === 'function') { - this.data.afterRead(file, this.getDetail()); + if (typeof afterRead === 'function') { + afterRead(file, this.getDetail()); } this.$emit('after-read', { file, ...this.getDetail() }); @@ -184,32 +176,28 @@ VantComponent({ const item = lists[index]; wx.previewImage({ - urls: lists - .filter((item) => item.isImage) - .map((item) => item.url || item.path), - current: item.url || item.path, + urls: lists.filter((item) => isImageFile(item)).map((item) => item.url), + current: item.url, fail() { wx.showToast({ title: '预览图片失败', icon: 'none' }); }, }); }, - // fix: accept 为 video 时不能展示视频 - onPreviewVideo: function (event) { + + onPreviewVideo(event) { if (!this.data.previewFullImage) return; - var index = event.currentTarget.dataset.index; - var lists = this.data.lists; + const { index } = event.currentTarget.dataset; + const { lists } = this.data; + wx.previewMedia({ sources: lists - .filter(function (item) { - return item.isVideo; - }) - .map(function (item) { - item.type = 'video'; - item.url = item.url || item.path; - return item; - }), + .filter((item) => isVideoFile(item)) + .map((item) => ({ + ...item, + type: 'video', + })), current: index, - fail: function () { + fail() { wx.showToast({ title: '预览视频失败', icon: 'none' }); }, }); diff --git a/packages/uploader/index.wxml b/packages/uploader/index.wxml index c7ebf945..111e64c5 100644 --- a/packages/uploader/index.wxml +++ b/packages/uploader/index.wxml @@ -14,7 +14,7 @@ {{ item.name || ('图片' + index) }} + - {{ item.name || item.url || item.path }} + {{ item.name || item.url }} - + diff --git a/packages/uploader/utils.ts b/packages/uploader/utils.ts index 47b8e052..2449286b 100644 --- a/packages/uploader/utils.ts +++ b/packages/uploader/utils.ts @@ -1,26 +1,24 @@ +import { pickExclude } from '../common/utils'; +import { isImageUrl, isVideoUrl } from '../common/validator'; + interface File { - path: string; // 上传临时地址 url: string; // 上传临时地址 - size: number; // 上传大小 - name: string; // 上传文件名称,accept="image" 不存在 - type: string; // 上传类型,accept="image" 不存在 - time: number; // 上传时间,accept="image" 不存在 - image: boolean; // 是否为图片 -} - -const IMAGE_REGEXP = /\.(jpeg|jpg|gif|png|svg|webp|jfif|bmp|dpg)/i; - -function isImageUrl(url: string): boolean { - return IMAGE_REGEXP.test(url); + size?: number; // 上传大小 + name?: string; + type: string; // 上传类型 + duration?: number; // 上传时间 + time?: number; // 消息文件时间 + isImage?: boolean; + isVideo?: boolean; } export function isImageFile(item: File): boolean { - if (item.type) { - return item.type.indexOf('image') === 0; + if (item.isImage != null) { + return item.isImage; } - if (item.path) { - return isImageUrl(item.path); + if (item.type) { + return item.type === 'image'; } if (item.url) { @@ -30,11 +28,62 @@ export function isImageFile(item: File): boolean { return false; } -export function isVideo( - res: any, - accept: string -): res is WechatMiniprogram.ChooseVideoSuccessCallbackResult { - return accept === 'video'; +export function isVideoFile(item: File): boolean { + if (item.isVideo != null) { + return item.isVideo; + } + + if (item.type) { + return item.type === 'video'; + } + + if (item.url) { + return isVideoUrl(item.url); + } + + return false; +} + +function formatImage( + res: WechatMiniprogram.ChooseImageSuccessCallbackResult +): File[] { + return res.tempFiles.map((item) => ({ + ...pickExclude(item, ['path']), + type: 'image', + url: item.path, + thumb: item.path, + })); +} + +function formatVideo( + res: WechatMiniprogram.ChooseVideoSuccessCallbackResult & Record +) { + return [ + { + ...pickExclude(res, ['tempFilePath', 'thumbTempFilePath', 'errMsg']), + type: 'video', + url: res.tempFilePath, + thumb: res.thumbTempFilePath, + }, + ]; +} + +function formatMedia(res: WechatMiniprogram.ChooseMediaSuccessCallbackResult) { + return res.tempFiles.map((item) => ({ + ...pickExclude(item, ['fileType', 'thumbTempFilePath', 'tempFilePath']), + type: res.type, + url: item.tempFilePath, + thumb: res.type === 'video' ? item.thumbTempFilePath : item.tempFilePath, + })); +} + +function formatFile( + res: WechatMiniprogram.ChooseMessageFileSuccessCallbackResult +) { + return res.tempFiles.map((item) => ({ + ...pickExclude(item, ['path']), + url: item.path, + })); } export function chooseFile({ @@ -46,66 +95,47 @@ export function chooseFile({ sizeType, camera, maxCount, -}): Promise< - | WechatMiniprogram.ChooseImageSuccessCallbackResult - | WechatMiniprogram.ChooseMediaSuccessCallbackResult - | WechatMiniprogram.ChooseVideoSuccessCallbackResult - | WechatMiniprogram.ChooseMessageFileSuccessCallbackResult -> { - switch (accept) { - case 'image': - return new Promise((resolve, reject) => { +}) { + return new Promise((resolve, reject) => { + switch (accept) { + case 'image': wx.chooseImage({ - count: multiple ? Math.min(maxCount, 9) : 1, // 最多可以选择的数量,如果不支持多选则数量为1 - sourceType: capture, // 选择图片的来源,相册还是相机 + count: multiple ? Math.min(maxCount, 9) : 1, + sourceType: capture, sizeType, - success: resolve, + success: (res) => resolve(formatImage(res)), fail: reject, }); - }); - case 'media': - return new Promise((resolve, reject) => { + break; + case 'media': wx.chooseMedia({ - count: multiple ? Math.min(maxCount, 9) : 1, // 最多可以选择的数量,如果不支持多选则数量为1 - sourceType: capture, // 选择图片的来源,相册还是相机 + count: multiple ? Math.min(maxCount, 9) : 1, + sourceType: capture, maxDuration, sizeType, camera, - success: resolve, + success: (res) => resolve(formatMedia(res)), fail: reject, }); - }); - case 'video': - return new Promise((resolve, reject) => { + break; + case 'video': wx.chooseVideo({ sourceType: capture, compressed, maxDuration, camera, - success: resolve, + success: (res) => resolve(formatVideo(res)), fail: reject, }); - }); - default: - return new Promise((resolve, reject) => { + break; + default: wx.chooseMessageFile({ - count: multiple ? maxCount : 1, // 最多可以选择的数量,如果不支持多选则数量为1 - type: 'file', - success: resolve, + count: multiple ? maxCount : 1, + type: accept, + success: (res) => resolve(formatFile(res)), fail: reject, }); - }); - } -} - -export function isFunction(val: unknown): val is Function { - return typeof val === 'function'; -} - -export function isObject(val: any): val is Record { - return val !== null && typeof val === 'object'; -} - -export function isPromise(val: unknown): val is Promise { - return isObject(val) && isFunction(val.then) && isFunction(val.catch); + break; + } + }); }