You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
427 lines
14 KiB
427 lines
14 KiB
<template>
|
|
<view class="s-upload" :class="custom_class" :style="custom_style">
|
|
<s-grid
|
|
v-if="!custom"
|
|
:square="square"
|
|
:border="false"
|
|
:column="column"
|
|
:gutter="gutter"
|
|
>
|
|
<!-- 预览样式 -->
|
|
<template v-if="showFileList">
|
|
<s-grid-item
|
|
v-for="(item, index) of list"
|
|
:key="index"
|
|
hover-class="none"
|
|
:width="width"
|
|
:height="height"
|
|
:radius="radius"
|
|
>
|
|
<view class="s-upload__preview" @click="onPreviewItem(index)">
|
|
<s-image
|
|
v-if="item.isImage"
|
|
custom-class="s-upload__preview-image"
|
|
:mode="imageFit"
|
|
:src="item.thumb || item.url"
|
|
/>
|
|
<template v-else-if="item.isVideo">
|
|
<video
|
|
class="s-upload__preview-video"
|
|
:controls="false"
|
|
:show-center-play-btn="false"
|
|
:object-fit="videoFit"
|
|
:src="item.url"
|
|
:poster="item.thumb"
|
|
/>
|
|
<view v-if="item.status === 'success'" class="s-upload__play">
|
|
<s-icon name="play-circle" />
|
|
</view>
|
|
</template>
|
|
<view v-else class="s-upload__file">
|
|
<s-icon name="description" custom-class="s-upload__file-icon" />
|
|
<view class="s-upload__file-name s-ellipsis">
|
|
{{ item.name || item.url }}
|
|
</view>
|
|
</view>
|
|
<view
|
|
v-if="item.status === 'uploading' || item.status === 'failed'"
|
|
class="s-upload__mask"
|
|
>
|
|
<s-icon
|
|
v-if="item.status === 'failed'"
|
|
name="close"
|
|
custom-class="s-upload__mask-icon"
|
|
/>
|
|
<s-loading v-else custom-class="s-upload__mask-icon" />
|
|
<text v-if="item.message" class="s-upload__mask-message">
|
|
{{ item.message }}
|
|
</text>
|
|
</view>
|
|
<view
|
|
v-if="deletable && item.deletable"
|
|
class="s-upload__preview-delete"
|
|
:style="delete_btn_style"
|
|
@click.stop="onDeleteItem(index)"
|
|
>
|
|
<s-icon name="cross" custom-class="s-upload__preview-delete-icon" />
|
|
</view>
|
|
</view>
|
|
</s-grid-item>
|
|
</template>
|
|
<!-- 上传按钮 -->
|
|
<s-grid-item
|
|
v-if="show_upload"
|
|
hover-class="none"
|
|
:width="width"
|
|
:height="height"
|
|
:radius="radius"
|
|
>
|
|
<view class="s-upload__slot" @click.stop="startUpload">
|
|
<slot name="trigger">
|
|
<view
|
|
class="s-upload__upload"
|
|
:class="{ 's-upload__upload--disabled': disabled }"
|
|
:style="upload_style"
|
|
>
|
|
<s-icon
|
|
:name="uploadIcon"
|
|
:size="uploadIconSize"
|
|
:color="uploadIconColor"
|
|
custom-class="s-upload__upload-icon"
|
|
/>
|
|
<text
|
|
v-if="uploadText"
|
|
class="s-upload__upload-text"
|
|
:style="upload_text_style"
|
|
>
|
|
{{ uploadText }}
|
|
</text>
|
|
</view>
|
|
</slot>
|
|
</view>
|
|
</s-grid-item>
|
|
</s-grid>
|
|
<slot :scopeParams="scopeParams" :list="list" :showUpload="show_upload"> </slot>
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
import componentMixin from '../../mixins/componentMixin';
|
|
import { isImageFile, chooseFile, isVideoFile, formatSize } from './utils';
|
|
import isPromise from '../../utils/isPromise';
|
|
import showToast from '../../utils/showToast';
|
|
|
|
/**
|
|
* s-upload 文件上传
|
|
* @description 用于将本地的图片或文件上传至服务器,并在上传过程中展示预览图和上传进度。目前 Upload 组件不包含将文件上传至服务器的接口逻辑,该步骤需要自行实现。
|
|
* @property {Boolean} custom 是否自定义显示列表和上传按钮,可通过ref触发上传方法
|
|
* @property {String} name 标识符,可以在回调函数的第二项参数中获取
|
|
* @property {String} accept = [image|media|file|video|all] 接受的文件类型
|
|
* @property {Array} fileList 上传的文件数据
|
|
* @property {Boolean} disabled 是否禁用文件上传
|
|
* @property {Boolean} multiple 是否开启图片多选,部分安卓机型不支持
|
|
* @property {Number|String} column 一行显示几个已上传内容
|
|
* @property {Number|String} gutter 已上传内容之间间隔
|
|
* @property {Number|String} width 上传容器和已上传内容宽度
|
|
* @property {Number|String} height 上传容器和已上传内容高度
|
|
* @property {Boolean} square 是否根据宽度适配上传容器高度为正方形
|
|
* @property {Number} maxSize 文件大小限制,单位为byte
|
|
* @property {Number} maxCount 文件上传数量限制
|
|
* @property {Boolean} deletable 是否展示删除按钮
|
|
* @property {String|Object} deleteBtnStyle 删除按钮样式
|
|
* @property {Boolean} showUpload 是否展示文件上传按钮
|
|
* @property {Boolean} showFileList 是否在上传完成后展示预览图
|
|
* @property {String} imageFit 预览图裁剪模式,可选值参考小程序image组件的mode属性
|
|
* @property {String} videoFit 可参考视频video组件object-fit选项
|
|
* @property {String} background 上传按钮背景
|
|
* @property {String|Object} uploadStyle 上传按钮样式
|
|
* @property {String} uploadText 上传区域提示文字
|
|
* @property {String|Object} uploadTextStyle 上传区域提示文字样式
|
|
* @property {String} uploadIcon 上传区域图标,可选值见 Icon 组件
|
|
* @property {Number|String} uploadIconSize 上传区域图标大小
|
|
* @property {String} uploadIconColor 上传区域图标颜色
|
|
* @property {Array<String>} sizeType [original,compressed] 所选的图片的尺寸, 当accept为image类型时设置所选图片的尺寸可选值为original compressed
|
|
* @property {Array<String>} capture [album,camera] 图片或者视频选取模式,当accept为image类型时设置capture可选值为camera可以直接调起摄像头
|
|
* @property {Boolean} compressed 当 accept 为 video 时生效,是否压缩视频,默认为false
|
|
* @property {Number} maxDuration 当 accept 为 video 时生效,拍摄视频最长拍摄时间,单位秒
|
|
* @property {String} camera [back,front] 当 accept 为 video 时生效,可选值为 back front
|
|
* @property {Boolean} previewFullImage 是否在点击后预览图片
|
|
* @property {Boolean} previewFullVideo 是否在点击后预览视频
|
|
* @property {Boolean} previewFullFile 是否在点击后预览除图片和视频的其它文件
|
|
* @property {Boolean} useBeforeRead 是否开启文件读取前事件
|
|
* @property {Function} beforeRead 文件读取前,在回调函数中返回 false 可终止文件读取
|
|
* @property {Function} afterRead 文件读取完成后,可在此处上传
|
|
* @event {Function} before-read ({file,name,index}) 绑定事件的同时需要将use-before-read属性设置为true
|
|
* @event {Function} after-read ({file,name,index}) 文件读取完成后
|
|
* @event {Function} oversize ({file,name,index}) 文件超出大小限制
|
|
* @event {Function} delete ({file,name,index}) 删除文件
|
|
* @event {Function} preview ({file,name,index}) 点击预览时触发
|
|
* @event {Function} preview-image ({file,name,index}) 点击预览图片时触发
|
|
* @event {Function} preview-video ({file,name,index}) 点击预览视频时触发
|
|
* @event {Function} preview-file ({file,name,index}) 点击预览文件时触发
|
|
* @example <s-upload :file-list="fileList" :after-read="afterRead" @delete="remove"/>
|
|
*/
|
|
export default {
|
|
name: 'SUpload',
|
|
mixins: [componentMixin],
|
|
props: {
|
|
custom: Boolean,
|
|
name: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
accept: {
|
|
type: String,
|
|
default: 'image',
|
|
},
|
|
fileList: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
disabled: Boolean,
|
|
multiple: Boolean,
|
|
column: {
|
|
type: [Number, String],
|
|
default: 3,
|
|
},
|
|
gutter: {
|
|
type: [Number, String],
|
|
default: 20,
|
|
},
|
|
width: [Number, String],
|
|
height: [Number, String],
|
|
background: String,
|
|
radius: [Number, String],
|
|
square: Boolean,
|
|
maxSize: {
|
|
type: Number,
|
|
default: Number.MAX_VALUE,
|
|
},
|
|
maxCount: {
|
|
type: Number,
|
|
default: 100,
|
|
},
|
|
deletable: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
deleteBtnStyle: [String, Object],
|
|
showUpload: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
showFileList: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
imageFit: {
|
|
type: String,
|
|
default: 'aspectFill',
|
|
},
|
|
videoFit: {
|
|
type: String,
|
|
default: 'cover',
|
|
},
|
|
uploadStyle: [String, Object],
|
|
uploadText: String,
|
|
uploadTextStyle: [String, Object],
|
|
uploadIcon: {
|
|
type: String,
|
|
default: 'plus',
|
|
},
|
|
uploadIconSize: [Number, String],
|
|
uploadIconColor: String,
|
|
sizeType: {
|
|
type: Array,
|
|
default: () => ['original', 'compressed'],
|
|
},
|
|
capture: {
|
|
type: Array,
|
|
default: () => ['album', 'camera'],
|
|
},
|
|
compressed: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
maxDuration: {
|
|
type: Number,
|
|
default: 60,
|
|
},
|
|
camera: {
|
|
type: String,
|
|
default: 'back',
|
|
},
|
|
previewFullImage: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
previewFullVideo: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
previewFullFile: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
useBeforeRead: Boolean,
|
|
beforeRead: Function,
|
|
afterRead: Function,
|
|
},
|
|
data() {
|
|
return {
|
|
list: [],
|
|
};
|
|
},
|
|
computed: {
|
|
upload_style() {
|
|
return this.$mergeStyle({
|
|
background: this.background,
|
|
}, this.uploadStyle);
|
|
},
|
|
upload_text_style() {
|
|
return this.$mergeStyle(this.uploadTextStyle);
|
|
},
|
|
delete_btn_style() {
|
|
return this.$mergeStyle(this.deleteBtnStyle);
|
|
},
|
|
show_upload() {
|
|
return this.list.length < this.maxCount && this.showUpload;
|
|
},
|
|
},
|
|
watch: {
|
|
fileList: {
|
|
immediate: true,
|
|
deep: true,
|
|
handler() {
|
|
this.formatFileList();
|
|
},
|
|
},
|
|
},
|
|
methods: {
|
|
formatFileList() {
|
|
this.list = this.fileList.map(item => ({
|
|
...item,
|
|
isImage: isImageFile(item),
|
|
isVideo: isVideoFile(item),
|
|
deletable: typeof item.deletable === 'boolean' ? item.deletable : true,
|
|
}));
|
|
},
|
|
getDetail(index) {
|
|
return {
|
|
name: this.name,
|
|
index: typeof index === 'number' ? index : this.fileList.length,
|
|
};
|
|
},
|
|
startUpload() {
|
|
const { maxCount, multiple, list, disabled } = this;
|
|
if (disabled) return;
|
|
chooseFile({
|
|
accept: this.accept,
|
|
multiple: this.multiple,
|
|
capture: this.capture,
|
|
compressed: this.compressed,
|
|
maxDuration: this.maxDuration,
|
|
sizeType: this.sizeType,
|
|
camera: this.camera,
|
|
maxCount: maxCount - list.length,
|
|
}).then(res => {
|
|
this.onBeforeRead(multiple ? res : res[0]);
|
|
}).catch(error => {
|
|
this.$emit('error', error);
|
|
});
|
|
},
|
|
onBeforeRead(file) {
|
|
const beforeRead = this.$getPropsFn('beforeRead');
|
|
const useBeforeRead = this.useBeforeRead;
|
|
let res = true;
|
|
if (beforeRead) {
|
|
res = beforeRead({ file, ...this.getDetail() });
|
|
}
|
|
if (useBeforeRead) {
|
|
res = new Promise((resolve, reject) => {
|
|
this.$emit('before-read', {
|
|
file,
|
|
...this.getDetail(),
|
|
callback: ok => ok ? resolve() : reject(),
|
|
});
|
|
});
|
|
}
|
|
if (!res) return;
|
|
if (isPromise(res)) {
|
|
res.then(data => this.onAfterRead(data || file));
|
|
} else {
|
|
this.onAfterRead(file);
|
|
}
|
|
},
|
|
onAfterRead(file) {
|
|
const { maxSize } = this;
|
|
const afterRead = this.$getPropsFn('afterRead');
|
|
const oversize = Array.isArray(file)
|
|
? file.some(item => item.size > maxSize)
|
|
: file.size > maxSize;
|
|
const options = { file, ...this.getDetail() };
|
|
if (oversize) {
|
|
showToast(`文件大小不能超过 ${formatSize(maxSize)}`);
|
|
this.$emit('oversize', options);
|
|
return;
|
|
}
|
|
if (afterRead) afterRead(options);
|
|
this.$emit('after-read', options);
|
|
},
|
|
onDeleteItem(index) {
|
|
this.$emit('delete', {
|
|
file: this.list[index],
|
|
...this.getDetail(index),
|
|
});
|
|
},
|
|
onPreviewItem(index) {
|
|
const file = this.list[index];
|
|
const options = { file, ...this.getDetail(index) };
|
|
this.$emit('preview', options);
|
|
if (file.isImage) {
|
|
this.$emit('preview-image', options);
|
|
this.previewFullImage && this.previewImage(index);
|
|
} else if (file.isVideo) {
|
|
this.$emit('preview-video', options);
|
|
this.previewFullVideo && this.previewVideo(index);
|
|
} else {
|
|
this.$emit('preview-file', options);
|
|
this.previewFullFile && this.previewFile(index);
|
|
}
|
|
},
|
|
previewImage(index) {
|
|
const list = this.list.filter(item => isImageFile(item));
|
|
uni.previewImage({
|
|
urls: list.map(item => item.url),
|
|
current: list.indexOf(this.list[index]),
|
|
fail() {
|
|
showToast('预览图片失败');
|
|
},
|
|
});
|
|
},
|
|
previewVideo(index) {
|
|
const list = this.list.filter(item => isVideoFile(item));
|
|
uni.previewMedia({
|
|
sources: list.map(item => ({ ...item, type: 'video' })),
|
|
current: list.indexOf(this.list[index]),
|
|
fail() {
|
|
showToast('预览视频失败');
|
|
},
|
|
});
|
|
},
|
|
previewFile(index) {
|
|
uni.downloadFile({
|
|
url: this.list[index].url,
|
|
success(res) {
|
|
uni.openDocument({
|
|
filePath: res.tempFilePath,
|
|
showMenu: true,
|
|
});
|
|
},
|
|
});
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style lang="scss" src="./index.scss"></style>
|
|
|