绩效考核手机版
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.

428 lines
14 KiB

3 years ago
<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>