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: [],
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,
});
}
},
});

View File

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

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

View File

@ -1,6 +1,7 @@
import { VantComponent } from '../common/component';
import { isImageFile, isVideo, chooseFile, isPromise } from './utils';
import { isImageFile, chooseFile, isVideoFile } from './utils';
import { chooseImageProps, chooseVideoProps } from './shared';
import { isBoolean, isPromise } from '../common/validator';
VantComponent({
props: {
@ -73,13 +74,13 @@ VantComponent({
const { fileList = [], maxCount } = this.data;
const lists = fileList.map((item) => ({
...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<any> = true;
let res: boolean | Promise<void> = true;
if (typeof beforeRead === 'function') {
res = beforeRead(file, this.getDetail());
}
if (useBeforeRead) {
res = new Promise((resolve, reject) => {
res = new Promise<void>((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' });
},
});

View File

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

View File

@ -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<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({
@ -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<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);
break;
}
});
}