888 lines
26 KiB
Vue
888 lines
26 KiB
Vue
|
|
<template>
|
|||
|
|
<view
|
|||
|
|
:style="tools._style([customStyle])"
|
|||
|
|
:class="classPrefix + ' ' + tClass"
|
|||
|
|
>
|
|||
|
|
<t-grid
|
|||
|
|
:gutter="gutter"
|
|||
|
|
:border="false"
|
|||
|
|
align="center"
|
|||
|
|
:column="column"
|
|||
|
|
:custom-style="draggable ? 'overflow: visible' : ''"
|
|||
|
|
>
|
|||
|
|
<block v-if="!dragLayout">
|
|||
|
|
<t-grid-item
|
|||
|
|
v-for="(file, index) in customFiles"
|
|||
|
|
:key="index"
|
|||
|
|
:t-class="classPrefix + '__grid ' + classPrefix + '__grid-file'"
|
|||
|
|
:t-class-content="classPrefix + '__grid-content'"
|
|||
|
|
aria-role="presentation"
|
|||
|
|
>
|
|||
|
|
<view
|
|||
|
|
:class="classPrefix + '__wrapper ' + (disabled ? classPrefix + '__wrapper--disabled' : '')"
|
|||
|
|
:style="gridItemStyle"
|
|||
|
|
:aria-role="ariaRole || getWrapperAriaRole(file)"
|
|||
|
|
:aria-label="ariaLabel || getWrapperAriaLabel(file)"
|
|||
|
|
>
|
|||
|
|
<t-image
|
|||
|
|
v-if="file.type !== 'video'"
|
|||
|
|
:data-file="file"
|
|||
|
|
:data-index="index"
|
|||
|
|
:t-class="classPrefix + '__thumbnail'"
|
|||
|
|
:custom-style="(imageProps && imageProps.style) || ''"
|
|||
|
|
:src="file.thumb || file.url"
|
|||
|
|
:mode="(imageProps && imageProps.mode) || 'aspectFill'"
|
|||
|
|
:error="(imageProps && imageProps.error) || 'default'"
|
|||
|
|
:lazy="(imageProps && imageProps.lazy) || false"
|
|||
|
|
:loading="(imageProps && imageProps.loading) || 'default'"
|
|||
|
|
:shape="(imageProps && imageProps.shape) || 'round'"
|
|||
|
|
:webp="(imageProps && imageProps.webp) || false"
|
|||
|
|
:show-menu-by-longpress="(imageProps && imageProps.showMenuByLongpress) || false"
|
|||
|
|
@click="onPreview($event, { file, index })"
|
|||
|
|
/>
|
|||
|
|
<video
|
|||
|
|
v-if="file.type === 'video'"
|
|||
|
|
:class="classPrefix + '__thumbnail'"
|
|||
|
|
:src="file.url"
|
|||
|
|
:poster="file.thumb"
|
|||
|
|
controls
|
|||
|
|
:autoplay="false"
|
|||
|
|
objectFit="contain"
|
|||
|
|
:data-file="file"
|
|||
|
|
@click.stop="onFileClick"
|
|||
|
|
/>
|
|||
|
|
<view
|
|||
|
|
v-if="file.status && file.status != 'done'"
|
|||
|
|
:class="classPrefix + '__progress-mask'"
|
|||
|
|
:data-index="index"
|
|||
|
|
:data-file="file"
|
|||
|
|
@click.stop="onFileClick"
|
|||
|
|
>
|
|||
|
|
<block v-if="file.status == 'loading'">
|
|||
|
|
<t-icon
|
|||
|
|
:t-class="classPrefix + '__progress-loading'"
|
|||
|
|
name="loading"
|
|||
|
|
size="48rpx"
|
|||
|
|
aria-hidden
|
|||
|
|
/>
|
|||
|
|
<view :class="classPrefix + '__progress-text'">
|
|||
|
|
{{ file.percent ? file.percent + '%' : '上传中...' }}
|
|||
|
|
</view>
|
|||
|
|
</block>
|
|||
|
|
<t-icon
|
|||
|
|
v-else
|
|||
|
|
:name="file.status == 'reload' ? 'refresh' : 'close-circle'"
|
|||
|
|
size="48rpx"
|
|||
|
|
aria-hidden
|
|||
|
|
/>
|
|||
|
|
<view
|
|||
|
|
v-if="file.status == 'reload' || file.status == 'failed'"
|
|||
|
|
:class="classPrefix + '__progress-text'"
|
|||
|
|
>
|
|||
|
|
{{ file.status == 'reload' ? '重新上传' : '上传失败' }}
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
<view
|
|||
|
|
v-if="tools.isBoolean(file.removeBtn) ? file.removeBtn : removeBtn"
|
|||
|
|
:class="classPrefix + '__close-btn hotspot-expanded'"
|
|||
|
|
:data-index="index"
|
|||
|
|
aria-role="button"
|
|||
|
|
aria-label="删除"
|
|||
|
|
@click.stop="onDelete"
|
|||
|
|
>
|
|||
|
|
<t-icon
|
|||
|
|
name="close"
|
|||
|
|
size="32rpx"
|
|||
|
|
color="#fff"
|
|||
|
|
/>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</t-grid-item>
|
|||
|
|
<t-grid-item
|
|||
|
|
v-if="addBtn && customLimit > 0"
|
|||
|
|
:t-class="classPrefix + '__grid'"
|
|||
|
|
:t-class-content="classPrefix + '__grid-content'"
|
|||
|
|
aria-label="上传"
|
|||
|
|
@click="onAddTap"
|
|||
|
|
>
|
|||
|
|
<view
|
|||
|
|
:class="classPrefix + '__wrapper'"
|
|||
|
|
:style="gridItemStyle"
|
|||
|
|
>
|
|||
|
|
<slot name="add-content" />
|
|||
|
|
<block v-if="addContent">
|
|||
|
|
{{ addContent }}
|
|||
|
|
</block>
|
|||
|
|
<view
|
|||
|
|
v-else
|
|||
|
|
:class="classPrefix + '__add-icon ' + (disabled ? classPrefix + '__add-icon--disabled' : '')"
|
|||
|
|
>
|
|||
|
|
<t-icon name="add" />
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</t-grid-item>
|
|||
|
|
</block>
|
|||
|
|
<block v-else>
|
|||
|
|
<view
|
|||
|
|
:class="classPrefix + '__drag'"
|
|||
|
|
:list="dragList"
|
|||
|
|
:style="dragWrapStyle + ';'"
|
|||
|
|
:drag-base-data="dragBaseData"
|
|||
|
|
>
|
|||
|
|
<view
|
|||
|
|
v-for="(file, index) in customFiles"
|
|||
|
|
:key="index"
|
|||
|
|
:ref="classPrefix + '__drag-item'"
|
|||
|
|
:class="getDragItemClass(index)"
|
|||
|
|
:style="getDragItemStyle(index)"
|
|||
|
|
:data-index="index"
|
|||
|
|
@longpress="parseEventDynamicCode($event, 'longPress', index)"
|
|||
|
|
@touchmove.stop.prevent="parseEventDynamicCode($event, dragging ? 'touchMove' : '', index)"
|
|||
|
|
@touchend.stop.prevent="parseEventDynamicCode($event, dragging ? 'touchEnd' : '', index)"
|
|||
|
|
>
|
|||
|
|
<t-grid-item
|
|||
|
|
:t-class="classPrefix + '__grid ' + classPrefix + '__grid-file'"
|
|||
|
|
:t-class-content="classPrefix + '__grid-content'"
|
|||
|
|
aria-role="presentation"
|
|||
|
|
custom-style="width: 100%"
|
|||
|
|
>
|
|||
|
|
<view
|
|||
|
|
:class="classPrefix + '__wrapper ' + (disabled ? classPrefix + '__wrapper--disabled' : '')"
|
|||
|
|
:style="gridItemStyle + ';'"
|
|||
|
|
:aria-role="ariaRole || getWrapperAriaRole(file)"
|
|||
|
|
:aria-label="ariaLabel || getWrapperAriaLabel(file)"
|
|||
|
|
>
|
|||
|
|
<t-image
|
|||
|
|
v-if="file.type !== 'video'"
|
|||
|
|
:data-file="file"
|
|||
|
|
:data-index="index"
|
|||
|
|
:t-class="classPrefix + '__thumbnail'"
|
|||
|
|
:custom-style="(imageProps && imageProps.style) || ''"
|
|||
|
|
:src="file.thumb || file.url"
|
|||
|
|
:mode="(imageProps && imageProps.mode) || 'aspectFill'"
|
|||
|
|
:error="(imageProps && imageProps.error) || 'default'"
|
|||
|
|
:lazy="(imageProps && imageProps.lazy) || false"
|
|||
|
|
:loading="(imageProps && imageProps.loading) || 'default'"
|
|||
|
|
:shape="(imageProps && imageProps.shape) || 'round'"
|
|||
|
|
:webp="(imageProps && imageProps.webp) || false"
|
|||
|
|
:show-menu-by-longpress="(imageProps && imageProps.showMenuByLongpress) || false"
|
|||
|
|
@click="onPreview($event, { file, index })"
|
|||
|
|
/>
|
|||
|
|
<video
|
|||
|
|
v-if="file.type === 'video'"
|
|||
|
|
:class="classPrefix + '__thumbnail'"
|
|||
|
|
:src="file.url"
|
|||
|
|
:poster="file.thumb"
|
|||
|
|
controls
|
|||
|
|
:autoplay="false"
|
|||
|
|
objectFit="contain"
|
|||
|
|
:data-file="file"
|
|||
|
|
@click.stop="onFileClick"
|
|||
|
|
/>
|
|||
|
|
<view
|
|||
|
|
v-if="file.status && file.status != 'done'"
|
|||
|
|
:class="classPrefix + '__progress-mask'"
|
|||
|
|
:data-index="index"
|
|||
|
|
:data-file="file"
|
|||
|
|
@click.stop="onFileClick"
|
|||
|
|
>
|
|||
|
|
<block v-if="file.status == 'loading'">
|
|||
|
|
<t-icon
|
|||
|
|
:t-class="classPrefix + '__progress-loading'"
|
|||
|
|
name="loading"
|
|||
|
|
size="48rpx"
|
|||
|
|
aria-hidden
|
|||
|
|
/>
|
|||
|
|
<view :class="classPrefix + '__progress-text'">
|
|||
|
|
{{ file.percent ? file.percent + '%' : '上传中...' }}
|
|||
|
|
</view>
|
|||
|
|
</block>
|
|||
|
|
<t-icon
|
|||
|
|
v-else
|
|||
|
|
:name="file.status == 'reload' ? 'refresh' : 'close-circle'"
|
|||
|
|
size="48rpx"
|
|||
|
|
aria-hidden
|
|||
|
|
/>
|
|||
|
|
<view
|
|||
|
|
v-if="file.status == 'reload' || file.status == 'failed'"
|
|||
|
|
:class="classPrefix + '__progress-text'"
|
|||
|
|
>
|
|||
|
|
{{ file.status == 'reload' ? '重新上传' : '上传失败' }}
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
<view
|
|||
|
|
v-if="tools.isBoolean(file.removeBtn) ? file.removeBtn : removeBtn"
|
|||
|
|
:class="classPrefix + '__close-btn hotspot-expanded'"
|
|||
|
|
:data-index="index"
|
|||
|
|
:data-url="file.url"
|
|||
|
|
aria-role="button"
|
|||
|
|
aria-label="删除"
|
|||
|
|
@click.stop="onDelete"
|
|||
|
|
>
|
|||
|
|
<t-icon
|
|||
|
|
name="close"
|
|||
|
|
size="32rpx"
|
|||
|
|
color="#fff"
|
|||
|
|
/>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</t-grid-item>
|
|||
|
|
</view>
|
|||
|
|
<view
|
|||
|
|
v-if="addBtn && customLimit > 0"
|
|||
|
|
:ref="classPrefix + '__drag-item'"
|
|||
|
|
:class="getDragItemClass(customFiles.length)"
|
|||
|
|
:style="getDragItemStyle(customFiles.length)"
|
|||
|
|
>
|
|||
|
|
<t-grid-item
|
|||
|
|
:t-class="classPrefix + '__grid'"
|
|||
|
|
:t-class-content="classPrefix + '__grid-content'"
|
|||
|
|
aria-label="上传"
|
|||
|
|
custom-style="width: 100%"
|
|||
|
|
@click="onAddTap"
|
|||
|
|
>
|
|||
|
|
<view
|
|||
|
|
:class="classPrefix + '__wrapper'"
|
|||
|
|
:style="gridItemStyle"
|
|||
|
|
>
|
|||
|
|
<slot name="add-content" />
|
|||
|
|
<block v-if="addContent">
|
|||
|
|
{{ addContent }}
|
|||
|
|
</block>
|
|||
|
|
<view
|
|||
|
|
v-else
|
|||
|
|
:class="classPrefix + '__add-icon ' + (disabled ? classPrefix + '__add-icon--disabled' : '')"
|
|||
|
|
>
|
|||
|
|
<t-icon name="add" />
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</t-grid-item>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</block>
|
|||
|
|
</t-grid>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
<script>
|
|||
|
|
import TGrid from '../grid/grid';
|
|||
|
|
import TGridItem from '../grid-item/grid-item';
|
|||
|
|
import TIcon from '../icon/icon';
|
|||
|
|
import TImage from '../image/image';
|
|||
|
|
import { uniComponent } from '../common/src/index';
|
|||
|
|
import props from './props';
|
|||
|
|
import { prefix } from '../common/config';
|
|||
|
|
import { isOverSize, coalesce, isWxWork, isPC } from '../common/utils';
|
|||
|
|
import { isObject } from '../common/validator';
|
|||
|
|
import tools from '../common/utils.wxs';
|
|||
|
|
import {
|
|||
|
|
getWrapperAriaRole,
|
|||
|
|
getWrapperAriaLabel,
|
|||
|
|
} from './upload.computed.js';
|
|||
|
|
import {
|
|||
|
|
longPress,
|
|||
|
|
touchMove,
|
|||
|
|
touchEnd,
|
|||
|
|
baseDataObserver,
|
|||
|
|
listObserver,
|
|||
|
|
} from './drag.computed.js';
|
|||
|
|
import { parseEventDynamicCode } from '../common/event/dynamic';
|
|||
|
|
|
|||
|
|
|
|||
|
|
const name = `${prefix}-upload`;
|
|||
|
|
|
|||
|
|
const makeMethods = () => [
|
|||
|
|
[longPress, 'longPress'],
|
|||
|
|
[touchMove, 'touchMove'],
|
|||
|
|
[touchEnd, 'touchEnd'],
|
|||
|
|
[baseDataObserver, 'baseDataObserver'],
|
|||
|
|
[listObserver, 'listObserver'],
|
|||
|
|
].reduce((acc, item) => {
|
|||
|
|
const func = item[0];
|
|||
|
|
return {
|
|||
|
|
...acc,
|
|||
|
|
[item[1]](...args) {
|
|||
|
|
func.call(this, ...args);
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
}, {});
|
|||
|
|
|
|||
|
|
|
|||
|
|
export default uniComponent({
|
|||
|
|
name,
|
|||
|
|
options: {
|
|||
|
|
styleIsolation: 'shared',
|
|||
|
|
},
|
|||
|
|
controlledProps: [
|
|||
|
|
{
|
|||
|
|
key: 'files',
|
|||
|
|
event: 'success',
|
|||
|
|
},
|
|||
|
|
],
|
|||
|
|
externalClasses: [`${prefix}-class`],
|
|||
|
|
components: {
|
|||
|
|
TGrid,
|
|||
|
|
TGridItem,
|
|||
|
|
TIcon,
|
|||
|
|
TImage,
|
|||
|
|
},
|
|||
|
|
props: {
|
|||
|
|
...props,
|
|||
|
|
},
|
|||
|
|
data() {
|
|||
|
|
return {
|
|||
|
|
classPrefix: name,
|
|||
|
|
prefix,
|
|||
|
|
current: false,
|
|||
|
|
proofs: [],
|
|||
|
|
customFiles: [], // 内部动态修改的files
|
|||
|
|
customLimit: 0, // 内部动态修改的limit
|
|||
|
|
column: 4,
|
|||
|
|
dragBaseData: {}, // 拖拽所需要页面数据
|
|||
|
|
rows: 0, // 行数
|
|||
|
|
dragWrapStyle: '', // 拖拽容器的样式
|
|||
|
|
dragList: [], // 拖拽的数据列
|
|||
|
|
dragging: true, // 是否开始拖拽
|
|||
|
|
dragLayout: false, // 是否开启拖拽布局
|
|||
|
|
tools,
|
|||
|
|
|
|||
|
|
gridItemStyle: '',
|
|||
|
|
|
|||
|
|
fakeState: {},
|
|||
|
|
|
|||
|
|
dragItemClassList: [],
|
|||
|
|
dragItemStyleList: [],
|
|||
|
|
};
|
|||
|
|
},
|
|||
|
|
watch: {
|
|||
|
|
files: {
|
|||
|
|
handler() {
|
|||
|
|
this.onWatchFilesLimit();
|
|||
|
|
},
|
|||
|
|
deep: true,
|
|||
|
|
},
|
|||
|
|
max: 'onWatchFilesLimit',
|
|||
|
|
draggable: {
|
|||
|
|
handler() {
|
|||
|
|
this.onWatchFilesLimit();
|
|||
|
|
},
|
|||
|
|
deep: true,
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
gridConfig: {
|
|||
|
|
handler() {
|
|||
|
|
this.updateGrid();
|
|||
|
|
},
|
|||
|
|
deep: true,
|
|||
|
|
},
|
|||
|
|
dragList: {
|
|||
|
|
handler(val) {
|
|||
|
|
setTimeout(() => {
|
|||
|
|
this.listObserver(val);
|
|||
|
|
}, 33);
|
|||
|
|
},
|
|||
|
|
deep: true,
|
|||
|
|
immediate: true,
|
|||
|
|
},
|
|||
|
|
dragBaseData: {
|
|||
|
|
handler(val) {
|
|||
|
|
this.baseDataObserver(val);
|
|||
|
|
},
|
|||
|
|
deep: true,
|
|||
|
|
immediate: true,
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
mounted() {
|
|||
|
|
this.handleLimit(this.files, this.max);
|
|||
|
|
this.updateGrid();
|
|||
|
|
},
|
|||
|
|
methods: {
|
|||
|
|
getWrapperAriaRole,
|
|||
|
|
getWrapperAriaLabel,
|
|||
|
|
|
|||
|
|
...makeMethods(),
|
|||
|
|
|
|||
|
|
handleLimit(customFiles, max) {
|
|||
|
|
if (max === 0) {
|
|||
|
|
max = Number.MAX_SAFE_INTEGER;
|
|||
|
|
}
|
|||
|
|
this.customFiles = customFiles.length > max ? customFiles.slice(0, max) : customFiles;
|
|||
|
|
this.customLimit = max - customFiles.length;
|
|||
|
|
this.dragging = true;
|
|||
|
|
|
|||
|
|
this.initDragLayout();
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
triggerSuccessEvent(files) {
|
|||
|
|
this._trigger('success', { files: [...this.customFiles, ...files] });
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
triggerFailEvent(err) {
|
|||
|
|
this.$emit('fail', err);
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
onFileClick(e) {
|
|||
|
|
const { file, index } = e.currentTarget.dataset;
|
|||
|
|
this.$emit('click', { index, file });
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 由于小程序暂时在ios上不支持返回上传文件的fileType,这里用文件的后缀来判断
|
|||
|
|
* @param mediaType
|
|||
|
|
* @param tempFilePath
|
|||
|
|
* @returns string
|
|||
|
|
* @link https://developers.weixin.qq.com/community/develop/doc/00042820b28ee8fb41fc4d0c254c00
|
|||
|
|
*/
|
|||
|
|
getFileType(mediaType, tempFilePath, fileType) {
|
|||
|
|
if (fileType) return fileType; // 如果有返回fileType就直接用
|
|||
|
|
if (mediaType.length === 1) {
|
|||
|
|
// 在单选媒体类型的时候直接使用单选媒体类型
|
|||
|
|
return mediaType[0];
|
|||
|
|
}
|
|||
|
|
// 否则根据文件后缀进行判读
|
|||
|
|
const videoType = ['avi', 'wmv', 'mkv', 'mp4', 'mov', 'rm', '3gp', 'flv', 'mpg', 'rmvb'];
|
|||
|
|
const temp = tempFilePath.split('.');
|
|||
|
|
const postfix = temp[temp.length - 1];
|
|||
|
|
if (videoType.includes(postfix.toLocaleLowerCase())) {
|
|||
|
|
return 'video';
|
|||
|
|
}
|
|||
|
|
return 'image';
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 选中文件之后,计算一个随机的短文件名
|
|||
|
|
getRandFileName(filePath) {
|
|||
|
|
const extIndex = filePath.lastIndexOf('.');
|
|||
|
|
const extName = extIndex === -1 ? '' : filePath.substr(extIndex);
|
|||
|
|
return parseInt(`${Date.now()}${Math.floor(Math.random() * 900 + 100)}`, 10).toString(36) + extName;
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
checkFileSize(size, sizeLimit, fileType) {
|
|||
|
|
if (isOverSize(size, sizeLimit)) {
|
|||
|
|
let title = `${fileType === 'video' ? '视频' : '图片'}大小超过限制`;
|
|||
|
|
|
|||
|
|
if (isObject(sizeLimit)) {
|
|||
|
|
const { size: limitSize, message: limitMessage } = sizeLimit;
|
|||
|
|
title = limitMessage?.replace('{sizeLimit}', String(limitSize));
|
|||
|
|
}
|
|||
|
|
uni.showToast({ icon: 'none', title });
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
onDelete(e) {
|
|||
|
|
const { index } = e.currentTarget.dataset;
|
|||
|
|
this.deleteHandle(index);
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
deleteHandle(index) {
|
|||
|
|
const { customFiles } = this;
|
|||
|
|
const delFile = customFiles[index];
|
|||
|
|
this.$emit('remove', { index, file: delFile });
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
updateGrid() {
|
|||
|
|
let { gridConfig = {} } = this;
|
|||
|
|
if (!isObject(gridConfig)) gridConfig = {};
|
|||
|
|
const { column = 4, width = 160, height = 160 } = gridConfig;
|
|||
|
|
|
|||
|
|
this.gridItemStyle = `width:${width}rpx;height:${height}rpx`;
|
|||
|
|
this.column = column;
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
resetDragLayout() {
|
|||
|
|
this.dragBaseData = {};
|
|||
|
|
this.dragWrapStyle = '';
|
|||
|
|
this.dragLayout = false;
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
initDragLayout() {
|
|||
|
|
const { draggable, disabled, customFiles } = this;
|
|||
|
|
if (!draggable || disabled || customFiles.length === 0) {
|
|||
|
|
this.resetDragLayout();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
this.initDragList();
|
|||
|
|
setTimeout(() => {
|
|||
|
|
this.initDragBaseData();
|
|||
|
|
}, 33)
|
|||
|
|
;
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
initDragList() {
|
|||
|
|
let i = 0;
|
|||
|
|
const { column, customFiles, customLimit } = this;
|
|||
|
|
const dragList = [];
|
|||
|
|
customFiles.forEach((item, index) => {
|
|||
|
|
dragList.push({
|
|||
|
|
realKey: i, // 真实顺序
|
|||
|
|
sortKey: index, // 整体顺序
|
|||
|
|
tranX: `${(index % column) * 100}%`,
|
|||
|
|
tranY: `${Math.floor(index / column) * 100}%`,
|
|||
|
|
data: { ...item },
|
|||
|
|
});
|
|||
|
|
i += 1;
|
|||
|
|
});
|
|||
|
|
if (customLimit > 0) {
|
|||
|
|
const listLength = dragList.length;
|
|||
|
|
dragList.push({
|
|||
|
|
realKey: listLength, // 真实顺序
|
|||
|
|
sortKey: listLength, // 整体顺序
|
|||
|
|
tranX: `${(listLength % column) * 100}%`,
|
|||
|
|
tranY: `${Math.floor(listLength / column) * 100}%`,
|
|||
|
|
fixed: true,
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
this.rows = Math.ceil(dragList.length / column);
|
|||
|
|
|
|||
|
|
this.dragList = dragList;
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
initDragBaseData() {
|
|||
|
|
const { classPrefix, rows, column } = this;
|
|||
|
|
|
|||
|
|
let query;
|
|||
|
|
// #ifdef H5 || APP-PLUS
|
|||
|
|
query = uni.createSelectorQuery().in(this);
|
|||
|
|
// #endif
|
|||
|
|
if (!query) {
|
|||
|
|
query = this.createSelectorQuery();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
let selectorGridItem;
|
|||
|
|
let selectorGrid;
|
|||
|
|
// #ifdef H5 || APP-PLUS
|
|||
|
|
selectorGridItem = '.t-grid-item';
|
|||
|
|
selectorGrid = '.t-grid';
|
|||
|
|
// #endif
|
|||
|
|
|
|||
|
|
if (!selectorGridItem) {
|
|||
|
|
selectorGridItem = `.${classPrefix} >>> .t-grid-item`;
|
|||
|
|
selectorGrid = `.${classPrefix} >>> .t-grid`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
query.select(selectorGridItem).boundingClientRect();
|
|||
|
|
query.select(selectorGrid).boundingClientRect();
|
|||
|
|
query.selectViewport().scrollOffset();
|
|||
|
|
query.exec((res) => {
|
|||
|
|
const [{ width, height }, { left, top }, { scrollTop }] = res;
|
|||
|
|
const dragBaseData = {
|
|||
|
|
rows,
|
|||
|
|
classPrefix,
|
|||
|
|
itemWidth: width,
|
|||
|
|
itemHeight: height,
|
|||
|
|
wrapLeft: left,
|
|||
|
|
wrapTop: top + scrollTop,
|
|||
|
|
columns: column,
|
|||
|
|
};
|
|||
|
|
const dragWrapStyle = `height: ${rows * height}px`;
|
|||
|
|
|
|||
|
|
this.dragBaseData = dragBaseData;
|
|||
|
|
this.dragWrapStyle = dragWrapStyle;
|
|||
|
|
this.dragLayout = true;
|
|||
|
|
|
|||
|
|
|
|||
|
|
// 为了给拖拽元素加上拖拽方法,同时控制不拖拽时不取消穿透
|
|||
|
|
const timer = setTimeout(() => {
|
|||
|
|
this.dragging = false;
|
|||
|
|
clearTimeout(timer);
|
|||
|
|
}, 0);
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
getPreviewMediaSources() {
|
|||
|
|
const previewMediaSources = [];
|
|||
|
|
this.customFiles.forEach((ele) => {
|
|||
|
|
const mediaSource = {
|
|||
|
|
url: ele.url,
|
|||
|
|
type: ele.type,
|
|||
|
|
poster: ele.thumb || undefined,
|
|||
|
|
};
|
|||
|
|
previewMediaSources.push(mediaSource);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return previewMediaSources;
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
onPreview(e) {
|
|||
|
|
this.onFileClick(e);
|
|||
|
|
const { preview } = this;
|
|||
|
|
|
|||
|
|
if (!preview) return;
|
|||
|
|
|
|||
|
|
const usePreviewMedia = this.customFiles.some(file => file.type === 'video');
|
|||
|
|
if (usePreviewMedia) {
|
|||
|
|
this.onPreviewMedia(e);
|
|||
|
|
} else {
|
|||
|
|
this.onPreviewImage(e);
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
onPreviewImage(e) {
|
|||
|
|
const { index } = e.currentTarget.dataset;
|
|||
|
|
const urls = this.customFiles.filter(file => file.percent !== -1).map(file => file.url);
|
|||
|
|
const current = this.customFiles[index]?.url;
|
|||
|
|
uni.previewImage({
|
|||
|
|
urls,
|
|||
|
|
current,
|
|||
|
|
fail() {
|
|||
|
|
uni.showToast({ title: '预览图片失败', icon: 'none' });
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
onPreviewMedia(e) {
|
|||
|
|
const { index: current } = e.currentTarget.dataset;
|
|||
|
|
const sources = this.getPreviewMediaSources();
|
|||
|
|
uni.previewMedia({
|
|||
|
|
sources,
|
|||
|
|
current,
|
|||
|
|
fail() {
|
|||
|
|
uni.showToast({ title: '预览视频失败', icon: 'none' });
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
uploadFiles(files) {
|
|||
|
|
return Promise.resolve().then(() => {
|
|||
|
|
// 开始调用上传函数
|
|||
|
|
const task = this.data.requestMethod(files);
|
|||
|
|
if (task instanceof Promise) {
|
|||
|
|
return task;
|
|||
|
|
}
|
|||
|
|
return Promise.resolve({});
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
startUpload(files) {
|
|||
|
|
// 如果传入了上传函数,则进度设为0并开始上传,否则跳过上传
|
|||
|
|
if (typeof this.requestMethod === 'function') {
|
|||
|
|
return this.uploadFiles(files)
|
|||
|
|
.then(() => {
|
|||
|
|
files.forEach((file) => {
|
|||
|
|
file.percent = 100;
|
|||
|
|
});
|
|||
|
|
this.triggerSuccessEvent(files);
|
|||
|
|
})
|
|||
|
|
.catch((err) => {
|
|||
|
|
this.triggerFailEvent(err);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果没有上传函数,success事件与微信api上传成功关联
|
|||
|
|
this.triggerSuccessEvent(files);
|
|||
|
|
|
|||
|
|
this.handleLimit(this.customFiles, this.max);
|
|||
|
|
return Promise.resolve();
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
onWatchFilesLimit() {
|
|||
|
|
this.handleLimit(this.files, this.max);
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
onAddTap() {
|
|||
|
|
const { disabled, mediaType, source } = this;
|
|||
|
|
if (disabled) return;
|
|||
|
|
if (source === 'media') {
|
|||
|
|
this.chooseMedia(mediaType);
|
|||
|
|
} else {
|
|||
|
|
this.chooseMessageFile(mediaType);
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
chooseMedia(mediaType) {
|
|||
|
|
const { customLimit } = this;
|
|||
|
|
const { config, sizeLimit } = this;
|
|||
|
|
let func = 'chooseMedia';
|
|||
|
|
// #ifdef H5 || MP-ALIPAY
|
|||
|
|
func = 'chooseImage';
|
|||
|
|
// #endif
|
|||
|
|
// #ifdef MP-WEIXIN
|
|||
|
|
if (isPC || isWxWork) {
|
|||
|
|
func = 'chooseImage';
|
|||
|
|
}
|
|||
|
|
// #endif
|
|||
|
|
uni[func]({
|
|||
|
|
count: Math.min(20, customLimit),
|
|||
|
|
mediaType,
|
|||
|
|
...config,
|
|||
|
|
success: (res) => {
|
|||
|
|
const files = [];
|
|||
|
|
|
|||
|
|
// 支持单/多文件
|
|||
|
|
res.tempFiles.forEach((temp) => {
|
|||
|
|
const { size, fileType, tempFilePath, width, height, duration, thumbTempFilePath, ...res } = temp;
|
|||
|
|
|
|||
|
|
if (this.checkFileSize(size, sizeLimit, fileType)) return;
|
|||
|
|
|
|||
|
|
|
|||
|
|
const name = temp.name || this.getRandFileName(tempFilePath);
|
|||
|
|
files.push({
|
|||
|
|
name,
|
|||
|
|
type: this.getFileType(mediaType, temp.name || tempFilePath, fileType),
|
|||
|
|
url: tempFilePath,
|
|||
|
|
size,
|
|||
|
|
width,
|
|||
|
|
height,
|
|||
|
|
duration,
|
|||
|
|
thumb: thumbTempFilePath,
|
|||
|
|
percent: 0,
|
|||
|
|
...res,
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
this.afterSelect(files);
|
|||
|
|
},
|
|||
|
|
fail: (err) => {
|
|||
|
|
this.triggerFailEvent(err);
|
|||
|
|
},
|
|||
|
|
complete: (res) => {
|
|||
|
|
this.$emit('complete', res);
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
chooseMessageFile(mediaType) {
|
|||
|
|
const { customLimit } = this;
|
|||
|
|
const { config, sizeLimit } = this;
|
|||
|
|
uni.chooseMessageFile({
|
|||
|
|
count: Math.min(100, customLimit),
|
|||
|
|
type: Array.isArray(mediaType) ? 'all' : mediaType,
|
|||
|
|
...config,
|
|||
|
|
success: (res) => {
|
|||
|
|
const files = [];
|
|||
|
|
|
|||
|
|
// 支持单/多文件
|
|||
|
|
res.tempFiles.forEach((temp) => {
|
|||
|
|
const { size, type: fileType, path: tempFilePath, ...res } = temp;
|
|||
|
|
|
|||
|
|
if (this.checkFileSize(size, sizeLimit, fileType)) return;
|
|||
|
|
|
|||
|
|
const name = this.getRandFileName(tempFilePath);
|
|||
|
|
files.push({
|
|||
|
|
name,
|
|||
|
|
type: this.getFileType(mediaType, tempFilePath, fileType),
|
|||
|
|
url: tempFilePath,
|
|||
|
|
size,
|
|||
|
|
percent: 0,
|
|||
|
|
...res,
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
this.afterSelect(files);
|
|||
|
|
},
|
|||
|
|
fail: err => this.triggerFailEvent(err),
|
|||
|
|
complete: res => this.$emit('complete', res),
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
afterSelect(files) {
|
|||
|
|
this._trigger('select-change', {
|
|||
|
|
files: [...this.customFiles],
|
|||
|
|
currentSelectedFiles: [files],
|
|||
|
|
});
|
|||
|
|
this._trigger('add', { files });
|
|||
|
|
this.startUpload(files);
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
dragVibrate(e) {
|
|||
|
|
const { vibrateType } = e;
|
|||
|
|
const { draggable } = this;
|
|||
|
|
const dragVibrate = coalesce(draggable?.vibrate, true);
|
|||
|
|
const dragCollisionVibrate = draggable?.collisionVibrate;
|
|||
|
|
if ((dragVibrate && vibrateType === 'longPress') || (dragCollisionVibrate && vibrateType === 'touchMove')) {
|
|||
|
|
uni.vibrateShort({
|
|||
|
|
type: 'light',
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
dragStatusChange(e) {
|
|||
|
|
const { dragging } = e;
|
|||
|
|
this.dragging = dragging;
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
dragEnd(e) {
|
|||
|
|
const { dragCollisionList } = e;
|
|||
|
|
let files = [];
|
|||
|
|
if (dragCollisionList.length === 0) {
|
|||
|
|
files = this.customFiles;
|
|||
|
|
} else {
|
|||
|
|
files = dragCollisionList.reduce((list, item) => {
|
|||
|
|
const { realKey, data, fixed } = item;
|
|||
|
|
if (!fixed) {
|
|||
|
|
list[realKey] = {
|
|||
|
|
...data,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
return list;
|
|||
|
|
}, []);
|
|||
|
|
}
|
|||
|
|
this.triggerDropEvent(files);
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
triggerDropEvent(files) {
|
|||
|
|
const { transition } = this;
|
|||
|
|
if (transition.backTransition) {
|
|||
|
|
const timer = setTimeout(() => {
|
|||
|
|
this.$emit('drop', { files });
|
|||
|
|
clearTimeout(timer);
|
|||
|
|
}, transition.duration);
|
|||
|
|
} else {
|
|||
|
|
this.$emit('drop', { files });
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
getState() {
|
|||
|
|
return this.fakeState || {};
|
|||
|
|
},
|
|||
|
|
callMethod(...args) {
|
|||
|
|
return this[args[0]]?.(...args.slice(1));
|
|||
|
|
},
|
|||
|
|
parseEventDynamicCode,
|
|||
|
|
setDragItemClass(index, operation, val) {
|
|||
|
|
if (!this.dragItemClassList[index]) {
|
|||
|
|
this.dragItemClassList[index] = [];
|
|||
|
|
}
|
|||
|
|
const valList = Array.isArray(val) ? val : [val];
|
|||
|
|
if (operation === 'add') {
|
|||
|
|
this.dragItemClassList[index].push(...valList);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (operation === 'remove') {
|
|||
|
|
this.dragItemClassList[index] = this.dragItemClassList[index].filter(item => !valList.includes(item));
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
getDragItemClass(index) {
|
|||
|
|
const { classPrefix } = this;
|
|||
|
|
const base = [
|
|||
|
|
`${classPrefix}__drag-item`,
|
|||
|
|
];
|
|||
|
|
return [
|
|||
|
|
...base,
|
|||
|
|
...(this.dragItemClassList[index] || []),
|
|||
|
|
].join(' ');
|
|||
|
|
},
|
|||
|
|
setDragItemStyle(index, val) {
|
|||
|
|
if (!this.dragItemStyleList[index]) {
|
|||
|
|
this.dragItemStyleList[index] = [];
|
|||
|
|
}
|
|||
|
|
this.dragItemStyleList[index].push(val);
|
|||
|
|
},
|
|||
|
|
getDragItemStyle(index) {
|
|||
|
|
const { column, transition } = this;
|
|||
|
|
const base = [
|
|||
|
|
`width: ${100 / column}%`,
|
|||
|
|
`--td-upload-drag-transition-duration: ${transition.duration}ms`,
|
|||
|
|
`--td-upload-drag-transition-timing-function: ${transition.timingFunction}`,
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
return [
|
|||
|
|
...base,
|
|||
|
|
...(this.dragItemStyleList[index] || []),
|
|||
|
|
].join(';');
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
<style scoped>
|
|||
|
|
@import './upload.css';
|
|||
|
|
</style>
|