feat(uploader): add new key thumb & standardization file-list (#3673)

fix #3606, fix #3661
This commit is contained in:
rex 2020-10-15 11:01:51 +08:00 committed by GitHub
parent 8f21b5ed17
commit 36256cd28f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 224 additions and 133 deletions

View File

@ -5,7 +5,7 @@ Page({
fileList1: [], fileList1: [],
fileList2: [ fileList2: [
{ url: 'https://img.yzcdn.cn/vant/leaf.jpg' }, { 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' }], fileList3: [{ url: 'https://img.yzcdn.cn/vant/sand.jpg' }],
fileList4: [], fileList4: [],
@ -17,14 +17,14 @@ Page({
{ {
url: 'https://img.yzcdn.cn/vant/leaf.jpg', url: 'https://img.yzcdn.cn/vant/leaf.jpg',
status: 'uploading', status: 'uploading',
message: '上传中' message: '上传中',
}, },
{ {
url: 'https://img.yzcdn.cn/vant/tree.jpg', url: 'https://img.yzcdn.cn/vant/tree.jpg',
status: 'failed', status: 'failed',
message: '上传失败' message: '上传失败',
} },
] ],
}, },
beforeRead(event) { beforeRead(event) {
@ -39,6 +39,7 @@ Page({
afterRead(event) { afterRead(event) {
const { file, name } = event.detail; const { file, name } = event.detail;
console.log(JSON.stringify(file, null, 2));
const fileList = this.data[`fileList${name}`]; const fileList = this.data[`fileList${name}`];
this.setData({ [`fileList${name}`]: fileList.concat(file) }); this.setData({ [`fileList${name}`]: fileList.concat(file) });
@ -67,12 +68,12 @@ Page({
this.uploadFilePromise(`my-photo${index}.png`, file) this.uploadFilePromise(`my-photo${index}.png`, file)
); );
Promise.all(uploadTasks) Promise.all(uploadTasks)
.then(data => { .then((data) => {
wx.showToast({ title: '上传成功', icon: 'none' }); 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 }); this.setData({ cloudPath: data, fileList6: fileList });
}) })
.catch(e => { .catch((e) => {
wx.showToast({ title: '上传失败', icon: 'none' }); wx.showToast({ title: '上传失败', icon: 'none' });
console.log(e); console.log(e);
}); });
@ -82,7 +83,7 @@ Page({
uploadFilePromise(fileName, chooseResult) { uploadFilePromise(fileName, chooseResult) {
return wx.cloud.uploadFile({ return wx.cloud.uploadFile({
cloudPath: fileName, cloudPath: fileName,
filePath: chooseResult.path filePath: chooseResult.path,
}); });
} },
}); });

View File

@ -1,3 +1,5 @@
import { isNumber, isPlainObject } from './validator';
export function isDef(value: any): boolean { export function isDef(value: any): boolean {
return value !== undefined && value !== null; return value !== undefined && value !== null;
} }
@ -7,10 +9,6 @@ export function isObj(x: any): boolean {
return x !== null && (type === 'object' || type === 'function'); 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) { export function range(num: number, min: number, max: number) {
return Math.min(Math.max(num, min), max); 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( export function getRect(
this: WechatMiniprogram.Component.TrivialInstance, this: WechatMiniprogram.Component.TrivialInstance,
selector: string selector: string

View File

@ -0,0 +1,39 @@
export function isFunction(val: unknown): val is Function {
return typeof val === 'function';
}
export function isPlainObject(val: unknown): val is Record<string, unknown> {
return val !== null && typeof val === 'object' && !Array.isArray(val);
}
export function isPromise<T = unknown>(val: unknown): val is Promise<T> {
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<string, unknown> {
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);
}

View File

@ -27,6 +27,7 @@ Page({
data: { data: {
fileList: [], fileList: [],
}, },
afterRead(event) { afterRead(event) {
const { file } = event.detail; const { file } = event.detail;
// 当设置 mutiple 为 true 时, file 为数组格式,否则为对象格式 // 当设置 mutiple 为 true 时, file 为数组格式,否则为对象格式
@ -48,7 +49,7 @@ Page({
### 图片预览 ### 图片预览
通过向组件传入`file-list`属性,可以绑定已经上传的图片列表,并展示图片列表的预览图 通过向组件传入`file-list`属性,可以绑定已经上传的图片列表,并展示图片列表的预览图。file-list 的详细结构可见下方。
```html ```html
<van-uploader file-list="{{ fileList }}" /> <van-uploader file-list="{{ fileList }}" />
@ -58,10 +59,14 @@ Page({
Page({ Page({
data: { data: {
fileList: [ fileList: [
{ url: 'https://img.yzcdn.cn/vant/leaf.jpg', name: '图片1' }, {
url: 'https://img.yzcdn.cn/vant/leaf.jpg',
name: '图片1',
},
// Uploader 根据文件后缀来判断是否为图片文件 // Uploader 根据文件后缀来判断是否为图片文件
// 如果图片 URL 中不包含类型信息,可以添加 isImage 标记来声明 // 如果图片 URL 中不包含类型信息,可以添加 isImage 标记来声明
{ {
url: 'http://iph.href.lu?text=default',
url: 'http://iph.href.lu/60x60?text=default', url: 'http://iph.href.lu/60x60?text=default',
name: '图片2', name: '图片2',
isImage: true, 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 ```html
<van-uploader file-list="{{ fileList }}" deletable="{{ true }}" /> <van-uploader file-list="{{ fileList }}" deletable="{{ true }}" />
@ -153,6 +158,7 @@ Page({
```html ```html
<van-uploader <van-uploader
file-list="{{ fileList }}" file-list="{{ fileList }}"
accept="media"
use-before-read use-before-read
bind:before-read="beforeRead" bind:before-read="beforeRead"
bind:after-read="afterRead" bind:after-read="afterRead"
@ -237,13 +243,26 @@ uploadFilePromise(fileName, chooseResult) {
#### accept 的合法值 #### accept 的合法值
| 参数 | 说明 | | 参数 | 说明 |
| ------- | ------------------------------ | | ------- | ------------------------------------ |
| `media` | 图片和视频 | | `media` | 图片和视频 |
| `image` | 图片 | | `image` | 图片 |
| `video` | 视频 | | `video` | 视频 |
| `file` | 除了图片和视频之外的其它的文件 | | `file` | 从客户端会话选择图片和视频以外的文件 |
| `all` | 所有文件 | | `all` | 从客户端会话选择所有文件 |
### FileList
`file-list` 为一个对象数组,数组中的每一个对象包含以下 `key`
| 参数 | 说明 |
| --------- | ------------------------------------------------------ |
| `url` | 图片和视频的网络资源地址 |
| `name` | 文件名称,视频将在全屏预览时作为标题显示 |
| `thumb` | 图片缩略图或视频封面的网络资源地址,仅对图片和视频有效 |
| `type` | 文件类型,可选值`image` `video` `file` |
| `isImage` | 手动标记图片资源 |
| `isVideo` | 手动标记视频资源 |
### Slot ### Slot

View File

@ -1,6 +1,7 @@
import { VantComponent } from '../common/component'; import { VantComponent } from '../common/component';
import { isImageFile, isVideo, chooseFile, isPromise } from './utils'; import { isImageFile, chooseFile, isVideoFile } from './utils';
import { chooseImageProps, chooseVideoProps } from './shared'; import { chooseImageProps, chooseVideoProps } from './shared';
import { isBoolean, isPromise } from '../common/validator';
VantComponent({ VantComponent({
props: { props: {
@ -73,13 +74,13 @@ VantComponent({
const { fileList = [], maxCount } = this.data; const { fileList = [], maxCount } = this.data;
const lists = fileList.map((item) => ({ const lists = fileList.map((item) => ({
...item, ...item,
isImage: isImage: isImageFile(item),
typeof item.isImage === 'undefined' isVideo: isVideoFile(item),
? isImageFile(item) deletable: isBoolean(item.deletable) ? item.deletable : true,
: item.isImage,
deletable:
typeof item.deletable === 'undefined' ? true : item.deletable,
})); }));
console.log(lists);
this.setData({ lists, isInCount: lists.length < maxCount }); this.setData({ lists, isInCount: lists.length < maxCount });
}, },
@ -100,18 +101,9 @@ VantComponent({
maxCount: maxCount - lists.length, maxCount: maxCount - lists.length,
}) })
.then((res) => { .then((res) => {
let file = null; console.log(res);
if (isVideo(res, accept)) { this.onBeforeRead(multiple ? res : res[0]);
file = {
path: res.tempFilePath,
...res,
};
} else {
file = multiple ? res.tempFiles : res.tempFiles[0];
}
this.onBeforeRead(file);
}) })
.catch((error) => { .catch((error) => {
this.$emit('error', error); this.$emit('error', error);
@ -120,14 +112,14 @@ VantComponent({
onBeforeRead(file) { onBeforeRead(file) {
const { beforeRead, useBeforeRead } = this.data; const { beforeRead, useBeforeRead } = this.data;
let res: boolean | Promise<any> = true; let res: boolean | Promise<void> = true;
if (typeof beforeRead === 'function') { if (typeof beforeRead === 'function') {
res = beforeRead(file, this.getDetail()); res = beforeRead(file, this.getDetail());
} }
if (useBeforeRead) { if (useBeforeRead) {
res = new Promise((resolve, reject) => { res = new Promise<void>((resolve, reject) => {
this.$emit('before-read', { this.$emit('before-read', {
file, file,
...this.getDetail(), ...this.getDetail(),
@ -150,7 +142,7 @@ VantComponent({
}, },
onAfterRead(file) { onAfterRead(file) {
const { maxSize } = this.data; const { maxSize, afterRead } = this.data;
const oversize = Array.isArray(file) const oversize = Array.isArray(file)
? file.some((item) => item.size > maxSize) ? file.some((item) => item.size > maxSize)
: file.size > maxSize; : file.size > maxSize;
@ -160,8 +152,8 @@ VantComponent({
return; return;
} }
if (typeof this.data.afterRead === 'function') { if (typeof afterRead === 'function') {
this.data.afterRead(file, this.getDetail()); afterRead(file, this.getDetail());
} }
this.$emit('after-read', { file, ...this.getDetail() }); this.$emit('after-read', { file, ...this.getDetail() });
@ -184,32 +176,28 @@ VantComponent({
const item = lists[index]; const item = lists[index];
wx.previewImage({ wx.previewImage({
urls: lists urls: lists.filter((item) => isImageFile(item)).map((item) => item.url),
.filter((item) => item.isImage) current: item.url,
.map((item) => item.url || item.path),
current: item.url || item.path,
fail() { fail() {
wx.showToast({ title: '预览图片失败', icon: 'none' }); wx.showToast({ title: '预览图片失败', icon: 'none' });
}, },
}); });
}, },
// fix: accept 为 video 时不能展示视频
onPreviewVideo: function (event) { onPreviewVideo(event) {
if (!this.data.previewFullImage) return; if (!this.data.previewFullImage) return;
var index = event.currentTarget.dataset.index; const { index } = event.currentTarget.dataset;
var lists = this.data.lists; const { lists } = this.data;
wx.previewMedia({ wx.previewMedia({
sources: lists sources: lists
.filter(function (item) { .filter((item) => isVideoFile(item))
return item.isVideo; .map((item) => ({
}) ...item,
.map(function (item) { type: 'video',
item.type = 'video'; })),
item.url = item.url || item.path;
return item;
}),
current: index, current: index,
fail: function () { fail() {
wx.showToast({ title: '预览视频失败', icon: 'none' }); wx.showToast({ title: '预览视频失败', icon: 'none' });
}, },
}); });

View File

@ -14,7 +14,7 @@
<image <image
wx:if="{{ item.isImage }}" wx:if="{{ item.isImage }}"
mode="{{ imageFit }}" mode="{{ imageFit }}"
src="{{ item.url || item.path }}" src="{{ item.thumb || item.url }}"
alt="{{ item.name || ('图片' + index) }}" alt="{{ item.name || ('图片' + index) }}"
class="van-uploader__preview-image" class="van-uploader__preview-image"
style="width: {{ utils.addUnit(previewSize) }}; height: {{ utils.addUnit(previewSize) }};" style="width: {{ utils.addUnit(previewSize) }}; height: {{ utils.addUnit(previewSize) }};"
@ -23,21 +23,23 @@
/> />
<video <video
wx:elif="{{ item.isVideo }}" wx:elif="{{ item.isVideo }}"
src="{{item.url || item.path}}" src="{{ item.url }}"
autoplay="{{item.autoplay}}" title="{{ item.name || ('视频' + index) }}"
poster="{{ item.thumb }}"
autoplay="{{ item.autoplay }}"
class="van-uploader__preview-image" class="van-uploader__preview-image"
style="width: {{ utils.addUnit(previewSize) }}; height: {{ utils.addUnit(previewSize) }};" style="width: {{ utils.addUnit(previewSize) }}; height: {{ utils.addUnit(previewSize) }};"
data-index="{{ index }}" data-index="{{ index }}"
bind:tap="onPreviewVideo" bind:tap="onPreviewVideo"
> >
</video> </video>
<view <view
wx:else wx:else
class="van-uploader__file" class="van-uploader__file"
style="width: {{ utils.addUnit(previewSize) }}; height: {{ utils.addUnit(previewSize) }};" style="width: {{ utils.addUnit(previewSize) }}; height: {{ utils.addUnit(previewSize) }};"
> >
<van-icon name="description" class="van-uploader__file-icon" /> <van-icon name="description" class="van-uploader__file-icon" />
<view class="van-uploader__file-name van-ellipsis">{{ item.name || item.url || item.path }}</view> <view class="van-uploader__file-name van-ellipsis">{{ item.name || item.url }}</view>
</view> </view>
<view <view
wx:if="{{ item.status === 'uploading' || item.status === 'failed' }}" wx:if="{{ item.status === 'uploading' || item.status === 'failed' }}"
@ -59,7 +61,7 @@
<!-- 上传样式 --> <!-- 上传样式 -->
<block wx:if="{{ isInCount }}"> <block wx:if="{{ isInCount }}">
<view class="van-uploader__slot" bind:tap="startUpload"> <view class="van-uploader__slot" bindtap="startUpload">
<slot /> <slot />
</view> </view>

View File

@ -1,26 +1,24 @@
import { pickExclude } from '../common/utils';
import { isImageUrl, isVideoUrl } from '../common/validator';
interface File { interface File {
path: string; // 上传临时地址
url: string; // 上传临时地址 url: string; // 上传临时地址
size: number; // 上传大小 size?: number; // 上传大小
name: string; // 上传文件名称accept="image" 不存在 name?: string;
type: string; // 上传类型accept="image" 不存在 type: string; // 上传类型
time: number; // 上传时间accept="image" 不存在 duration?: number; // 上传时间
image: boolean; // 是否为图片 time?: number; // 消息文件时间
} isImage?: boolean;
isVideo?: boolean;
const IMAGE_REGEXP = /\.(jpeg|jpg|gif|png|svg|webp|jfif|bmp|dpg)/i;
function isImageUrl(url: string): boolean {
return IMAGE_REGEXP.test(url);
} }
export function isImageFile(item: File): boolean { export function isImageFile(item: File): boolean {
if (item.type) { if (item.isImage != null) {
return item.type.indexOf('image') === 0; return item.isImage;
} }
if (item.path) { if (item.type) {
return isImageUrl(item.path); return item.type === 'image';
} }
if (item.url) { if (item.url) {
@ -30,11 +28,62 @@ export function isImageFile(item: File): boolean {
return false; return false;
} }
export function isVideo( export function isVideoFile(item: File): boolean {
res: any, if (item.isVideo != null) {
accept: string return item.isVideo;
): res is WechatMiniprogram.ChooseVideoSuccessCallbackResult { }
return accept === 'video';
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<string, any>
) {
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({ export function chooseFile({
@ -46,66 +95,47 @@ export function chooseFile({
sizeType, sizeType,
camera, camera,
maxCount, maxCount,
}): Promise< }) {
| WechatMiniprogram.ChooseImageSuccessCallbackResult return new Promise((resolve, reject) => {
| WechatMiniprogram.ChooseMediaSuccessCallbackResult switch (accept) {
| WechatMiniprogram.ChooseVideoSuccessCallbackResult case 'image':
| WechatMiniprogram.ChooseMessageFileSuccessCallbackResult
> {
switch (accept) {
case 'image':
return new Promise((resolve, reject) => {
wx.chooseImage({ wx.chooseImage({
count: multiple ? Math.min(maxCount, 9) : 1, // 最多可以选择的数量如果不支持多选则数量为1 count: multiple ? Math.min(maxCount, 9) : 1,
sourceType: capture, // 选择图片的来源,相册还是相机 sourceType: capture,
sizeType, sizeType,
success: resolve, success: (res) => resolve(formatImage(res)),
fail: reject, fail: reject,
}); });
}); break;
case 'media': case 'media':
return new Promise((resolve, reject) => {
wx.chooseMedia({ wx.chooseMedia({
count: multiple ? Math.min(maxCount, 9) : 1, // 最多可以选择的数量如果不支持多选则数量为1 count: multiple ? Math.min(maxCount, 9) : 1,
sourceType: capture, // 选择图片的来源,相册还是相机 sourceType: capture,
maxDuration, maxDuration,
sizeType, sizeType,
camera, camera,
success: resolve, success: (res) => resolve(formatMedia(res)),
fail: reject, fail: reject,
}); });
}); break;
case 'video': case 'video':
return new Promise((resolve, reject) => {
wx.chooseVideo({ wx.chooseVideo({
sourceType: capture, sourceType: capture,
compressed, compressed,
maxDuration, maxDuration,
camera, camera,
success: resolve, success: (res) => resolve(formatVideo(res)),
fail: reject, fail: reject,
}); });
}); break;
default: default:
return new Promise((resolve, reject) => {
wx.chooseMessageFile({ wx.chooseMessageFile({
count: multiple ? maxCount : 1, // 最多可以选择的数量如果不支持多选则数量为1 count: multiple ? maxCount : 1,
type: 'file', type: accept,
success: resolve, success: (res) => resolve(formatFile(res)),
fail: reject, fail: reject,
}); });
}); break;
} }
} });
export function isFunction(val: unknown): val is Function {
return typeof val === 'function';
}
export function isObject(val: any): val is Record<any, any> {
return val !== null && typeof val === 'object';
}
export function isPromise<T = any>(val: unknown): val is Promise<T> {
return isObject(val) && isFunction(val.then) && isFunction(val.catch);
} }