first commit

This commit is contained in:
lingxiao865
2026-02-10 08:05:03 +08:00
commit c5af079d8c
1094 changed files with 97530 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
:: BASE_DOC ::
## API
### Upload Props
name | type | default | description | required
-- | -- | -- | -- | --
custom-style | Object | - | CSS(Cascading Style Sheets) | N
add-btn | Boolean | true | \- | N
add-content | String | - | \- | N
allow-upload-duplicate-file | Boolean | false | allow to upload duplicate name files | N
config | Object | - | Typescript`UploadMpConfig` `type UploadMpConfig = ImageConfig \| VideoConfig` `interface ImageConfig { count?: number; sizeType?: Array<SizeTypeValues>; sourceType?: Array<SourceTypeValues> }` `type SizeTypeValues = 'original' \| 'compressed'` `type SourceTypeValues = 'album' \| 'camera'` `interface VideoConfig { sourceType?: Array<SourceTypeValues>; compressed?: boolean; maxDuration?: number; camera?: 'back' \| 'front' }`。[see more ts definition](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/upload/type.ts) | N
disabled | Boolean | undefined | make upload to be disabled | N
draggable | Boolean / Object | - | Typescript`boolean \| {vibrate?: boolean; collisionVibrate?: boolean}` | N
files | Array | - | `v-model:files` is supported。Typescript`Array<UploadFile>` `interface UploadFile { url: string; name?: string; size?: number; type?: 'image' \| 'video'; percent?: number; status: 'loading' \| 'reload' \| 'failed' \| 'done' }`。[see more ts definition](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/upload/type.ts) | N
default-files | Array | - | uncontrolled property。Typescript`Array<UploadFile>` `interface UploadFile { url: string; name?: string; size?: number; type?: 'image' \| 'video'; percent?: number; status: 'loading' \| 'reload' \| 'failed' \| 'done' }`。[see more ts definition](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/upload/type.ts) | N
grid-config | Object | - | Typescript`{column?: number; width?: number; height?: number;}` | N
gutter | Number | 16 | \- | N
image-props | Object | - | Typescript`ImageProps`[Image API Documents](./image?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/upload/type.ts) | N
max | Number | 0 | max count of files limit | N
media-type | Array | ['image', 'video'] | Typescript`Array<MediaType>` `type MediaType = 'image' \| 'video'`。[see more ts definition](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/upload/type.ts) | N
preview | Boolean | true | \- | N
remove-btn | Boolean | true | \- | N
request-method | Function | - | Typescript`any` | N
size-limit | Number / Object | - | files size limit。Typescript`number \| SizeLimitObj` `interface SizeLimitObj { size: number; unit: SizeUnit ; message?: string }` `type SizeUnitArray = ['B', 'KB', 'MB', 'GB']` `type SizeUnit = SizeUnitArray[number]`。[see more ts definition](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/upload/type.ts) | N
source | String | media | options: media/messageFile | N
transition | Object | `{backTransition: true, duration: 300, timingFunction: 'ease'}` | Typescript`Transition` `interface Transition { backTransition?: boolean, duration?: number, timingFunction?: string }`。[see more ts definition](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/upload/type.ts) | N
### Upload Events
name | params | description
-- | -- | --
add | `(context: { files: MediaContext })` | \-
click | `(context: { index: number; file: VideoContext \| ImageContext })` | \-
complete | \- | \-
drop | `(context: { files: MediaContext }) ` | \-
fail | \- | \-
remove | `(context: { index: number; file: UploadFile })` | \-
select-change | `(context: { files: MediaContext[]; currentSelectedFiles: MediaContext[] })` | \-
success | `(context: { files: MediaContext })` | [see more ts definition](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/upload/type.ts)。<br/>`type MediaContext = VideoContext[] \| ImageContext[]`<br/><br/>`interface VideoContext { name?: string; type?: string; url?: string; duration?: number; size?: number; width?: number; height?: number; thumb: string; progress: number }`<br/><br/>`interface ImageContext { name: string; type: string; url: string; size: number; width: number; height: number; progress: number }`<br/>
### Upload Slots
name | Description
-- | --
add-content | \-
### CSS Variables
The component provides the following CSS variables, which can be used to customize styles.
Name | Default Value | Description
-- | -- | --
--td-upload-add-bg-color | @bg-color-secondarycontainer | -
--td-upload-add-color | @text-color-placeholder | -
--td-upload-add-disabled-bg-color | @bg-color-component-disabled | -
--td-upload-add-icon-disabled-color | @text-color-disabled | -
--td-upload-add-icon-size | 56rpx | -
--td-upload-disabled-mask | rgba(0, 0.6) | -
--td-upload-drag-transition-duration | --td-upload-drag-transition-duration | -
--td-upload-drag-transition-timing-function | --td-upload-drag-transition-timing-function | -
--td-upload-drag-z-index | 999 | -
--td-upload-radius | @radius-default | -

View File

@@ -0,0 +1,116 @@
---
title: Upload 上传
description: 用于相册读取或拉起拍照的图片上传功能。
spline: form
isComponent: true
---
## 引入
可在 `main.ts` 或在需要使用的页面或组件中引入。
```js
import TUpload from '@tdesign/uniapp/upload/upload.vue';
```
### 单选上传图片
图片上传有两种方式:
1 选择完所有图片之后,统一上传,因此选择完就直接展示
2 每次选择图片都上传,展示每次上传图片的进度
{{ single }}
### 多选上传图片
{{ multiple }}
### 长按拖拽排序图片
{{ drag }}
### 加载状态
支持多种状态:`loading``reload``failed`
其中 `loading` 还可以通过传入 `percent` 来区分是否展示进度。
{{ status }}
### 从聊天记录上选
使用 `wx.chooseMessageFile` 实现,需要基础版本库 `2.5.0+`
{{ messageFile }}
## FAQ
### 为什么 `Upload` 外层使用 `display: flex` 时会造成组件样式混乱?
`Upload` 是基于 `TGrid` 宫格实现,当外层使用 `display: flex` ,子元素会默认加上 `flex-grow: 0`,造成 `Upload` 组件整体宽度不足。可以通过给 `Upload` 组件节点加上 `flex-grow: 1` 处理。
## API
### Upload Props
名称 | 类型 | 默认值 | 描述 | 必传
-- | -- | -- | -- | --
custom-style | Object | - | 自定义样式 | N
add-btn | Boolean | true | 添加按钮 | N
add-content | String | - | 添加按钮内容 | N
allow-upload-duplicate-file | Boolean | false | 是否允许重复上传相同文件名的文件 | N
config | Object | - | 图片上传配置,视频上传配置,文件上传配置等,包含图片尺寸、图片来源、视频来源、视频拍摄最长时间等。更多细节查看小程序官网。[图片上传](https://developers.weixin.qq.com/miniprogram/dev/api/media/image/wx.chooseImage.html)。[视频上传](https://developers.weixin.qq.com/miniprogram/dev/api/media/video/wx.chooseVideo.html)。TS 类型:`UploadMpConfig` `type UploadMpConfig = ImageConfig \| VideoConfig` `interface ImageConfig { count?: number; sizeType?: Array<SizeTypeValues>; sourceType?: Array<SourceTypeValues> }` `type SizeTypeValues = 'original' \| 'compressed'` `type SourceTypeValues = 'album' \| 'camera'` `interface VideoConfig { sourceType?: Array<SourceTypeValues>; compressed?: boolean; maxDuration?: number; camera?: 'back' \| 'front' }`。[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/upload/type.ts) | N
disabled | Boolean | undefined | 是否禁用组件 | N
draggable | Boolean / Object | - | 是否支持拖拽排序。长按时是否振动,碰撞时是否振动。示例一:`true`。示例二:`{ vibrate: true, collisionVibrate: true }`。TS 类型:`boolean \| {vibrate?: boolean; collisionVibrate?: boolean}` | N
files | Array | - | 已上传文件列表。支持语法糖 `v-model:files`。TS 类型:`Array<UploadFile>` `interface UploadFile { url: string; name?: string; size?: number; type?: 'image' \| 'video'; percent?: number; status: 'loading' \| 'reload' \| 'failed' \| 'done' }`。[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/upload/type.ts) | N
default-files | Array | - | 已上传文件列表。非受控属性。TS 类型:`Array<UploadFile>` `interface UploadFile { url: string; name?: string; size?: number; type?: 'image' \| 'video'; percent?: number; status: 'loading' \| 'reload' \| 'failed' \| 'done' }`。[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/upload/type.ts) | N
grid-config | Object | - | upload组件每行上传图片列数以及图片的宽度和高度。TS 类型:`{column?: number; width?: number; height?: number;}` | N
gutter | Number | 16 | 预览窗格的 `gutter` 大小,单位 rpx | N
image-props | Object | - | 透传 Image 组件全部属性。TS 类型:`ImageProps`[Image API Documents](./image?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/upload/type.ts) | N
max | Number | 0 | 用于控制文件上传数量,值为 0 则不限制 | N
media-type | Array | ['image', 'video'] | 支持上传的文件类型图片或视频。TS 类型:`Array<MediaType>` `type MediaType = 'image' \| 'video'`。[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/upload/type.ts) | N
preview | Boolean | true | 是否支持图片预览,文件没有预览 | N
remove-btn | Boolean | true | 移除按钮 | N
request-method | Function | - | 自定义上传方法。TS 类型:`any` | N
size-limit | Number / Object | - | 图片文件大小限制,默认单位 KB。可选单位有`'B' \| 'KB' \| 'MB' \| 'GB'`。示例一:`1000`。示例二:`{ size: 2, unit: 'MB', message: '图片大小不超过 {sizeLimit} MB' }`。TS 类型:`number \| SizeLimitObj` `interface SizeLimitObj { size: number; unit: SizeUnit ; message?: string }` `type SizeUnitArray = ['B', 'KB', 'MB', 'GB']` `type SizeUnit = SizeUnitArray[number]`。[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/upload/type.ts) | N
source | String | media | 来源。可选项media/messageFile | N
transition | Object | `{backTransition: true, duration: 300, timingFunction: 'ease'}` | 拖拽位置移动时的过渡参数,`duration`单位为ms。TS 类型:`Transition` `interface Transition { backTransition?: boolean, duration?: number, timingFunction?: string }`。[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/upload/type.ts) | N
### Upload Events
名称 | 参数 | 描述
-- | -- | --
add | `(context: { files: MediaContext })` | 选择后触发,仅包含本次选择的照片;`url` 表示选定视频的临时文件路径 (本地路径)。`duration` 表示选定视频的时间长度。`size`选定视频的数据量大小。更多描述参考 wx.chooseMedia 小程序官网描述
click | `(context: { index: number; file: VideoContext \| ImageContext })` | 点击已选文件时触发;常用于重新上传
complete | \- | 上传成功或失败后触发
drop | `(context: { files: MediaContext }) ` | 拖拽结束后触发,包含所有上传的文件(拖拽后的文件顺序);`url` 表示选定视频的临时文件路径 (本地路径)。`duration` 表示选定视频的时间长度。`size` 选定视频的数据量大小。更多描述参考 wx.chooseMedia 小程序官网描述
fail | \- | 上传失败后触发
remove | `(context: { index: number; file: UploadFile })` | 移除文件时触发
select-change | `(context: { files: MediaContext[]; currentSelectedFiles: MediaContext[] })` | 选择文件或图片之后,上传之前,触发该事件。<br />`files` 表示之前已经上传完成的文件列表。<br />`currentSelectedFiles` 表示本次上传选中的文件列表
success | `(context: { files: MediaContext })` | 上传成功后触发,包含所有上传的文件;`url` 表示选定视频的临时文件路径 (本地路径)。`duration` 表示选定视频的时间长度。`size`选定视频的数据量大小。更多描述参考 wx.chooseMedia 小程序官网描述。[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/upload/type.ts)。<br/>`type MediaContext = VideoContext[] \| ImageContext[]`<br/><br/>`interface VideoContext { name?: string; type?: string; url?: string; duration?: number; size?: number; width?: number; height?: number; thumb: string; progress: number }`<br/><br/>`interface ImageContext { name: string; type: string; url: string; size: number; width: number; height: number; progress: number }`<br/>
### Upload Slots
名称 | 描述
-- | --
add-content | 自定义 `add-content` 显示内容
### CSS Variables
组件提供了下列 CSS 变量,可用于自定义样式。
名称 | 默认值 | 描述
-- | -- | --
--td-upload-add-bg-color | @bg-color-secondarycontainer | -
--td-upload-add-color | @text-color-placeholder | -
--td-upload-add-disabled-bg-color | @bg-color-component-disabled | -
--td-upload-add-icon-disabled-color | @text-color-disabled | -
--td-upload-add-icon-size | 56rpx | -
--td-upload-disabled-mask | rgba(0, 0.6) | -
--td-upload-drag-transition-duration | --td-upload-drag-transition-duration | -
--td-upload-drag-transition-timing-function | --td-upload-drag-transition-timing-function | -
--td-upload-drag-z-index | 999 | -
--td-upload-radius | @radius-default | -

View File

@@ -0,0 +1,227 @@
/* eslint-disable no-plusplus */
let classPrefix = '';
let startIndex = 0;
let endIndex = 0;
let dragCollisionList = [];
const isOutRange = function (x1, y1, x2, y2, x3, y3) {
return x1 < 0 || x1 >= y1 || x2 < 0 || x2 >= y2 || x3 < 0 || x3 >= y3;
};
const sortCore = function (sKey, eKey, st) {
const _ = st.dragBaseData;
const excludeFix = function (cKey, type) {
if (st.list[cKey].fixed) {
// fixed 元素位置不会变化, 这里直接用 cKey(sortKey) 获取, 更加快捷
type ? --cKey : ++cKey;
return excludeFix(cKey, type);
}
return cKey;
};
// 先获取到 endKey 对应的 realKey, 防止下面排序过程中该 realKey 被修改
let endRealKey = -1;
st.list.forEach((item) => {
if (item.sortKey === eKey) endRealKey = item.realKey;
});
return st.list.map((item) => {
if (item.fixed) return item;
let cKey = item.sortKey;
let rKey = item.realKey;
if (sKey < eKey) {
// 正序拖动
if (cKey > sKey && cKey <= eKey) {
--rKey;
cKey = excludeFix(--cKey, true);
} else if (cKey === sKey) {
rKey = endRealKey;
cKey = eKey;
}
} else if (sKey > eKey) {
// 倒序拖动
if (cKey >= eKey && cKey < sKey) {
++rKey;
cKey = excludeFix(++cKey, false);
} else if (cKey === sKey) {
rKey = endRealKey;
cKey = eKey;
}
}
if (item.sortKey !== cKey) {
item.tranX = `${(cKey % _.columns) * 100}%`;
item.tranY = `${Math.floor(cKey / _.columns) * 100}%`;
item.sortKey = cKey;
item.realKey = rKey;
}
return item;
});
};
function triggerCustomEvent(list, type, ins) {
const _list = [];
const listData = [];
list.forEach((item) => {
_list[item.sortKey] = item;
});
_list.forEach((item) => {
if (!item.extraNode) {
listData.push(item.data);
}
});
ins.$emit(type, { listData });
}
export function longPress(event, index) {
const st = this.getState();
const _ = st.dragBaseData;
const sTouch = event.changedTouches[0];
if (!sTouch) return;
st.cur = index;
// compile error
// longPressIndex = st.cur;
// 初始项是固定项则返回
const item = st.list[st.cur];
if (item && item.fixed) return;
// 如果已经在 drag 中则返回, 防止多指触发 drag 动作, touchstart 事件中有效果
if (st.dragging) return;
st.dragging = true;
this.callMethod('dragStatusChange', { dragging: true });
// 计算X,Y轴初始位移, 使 item 中心移动到点击处, 单列时候X轴初始不做位移
st.tranX = _.columns === 1 ? 0 : sTouch.pageX - (_.itemWidth / 2 + _.wrapLeft);
st.tranY = sTouch.pageY - (_.itemHeight / 2 + _.wrapTop);
st.sId = sTouch.identifier;
this.setDragItemStyle(index, `transform: translate3d(${st.tranX}px, ${st.tranY}px, 0)`);
st.list.forEach((item, index) => {
this.setDragItemClass(index, 'remove', [`${classPrefix}__drag--tran`, `${classPrefix}__drag--cur`]);
this.setDragItemClass(index, 'add', index === st.cur ? `${classPrefix}__drag--cur` : `${classPrefix}__drag--tran`);
});
this.callMethod('dragVibrate', { vibrateType: 'longPress' });
}
export function touchMove(event, index) {
// const ins = event.instance;
const st = this.getState();
const _ = st.dragBaseData;
const mTouch = event.changedTouches[0];
if (!mTouch) return;
if (!st.dragging) return;
// 如果不是同一个触发点则返回
if (st.sId !== mTouch.identifier) return;
// 计算X,Y轴位移, 单列时候X轴初始不做位移
const tranX = _.columns === 1 ? 0 : mTouch.pageX - (_.itemWidth / 2 + _.wrapLeft);
const tranY = mTouch.pageY - (_.itemHeight / 2 + _.wrapTop);
// 到顶到底自动滑动
if (mTouch.clientY > _.windowHeight - _.itemHeight - _.realBottomSize) {
// 当前触摸点pageY + item高度 - (屏幕高度 - 底部固定区域高度)
this.callMethod('pageScroll', {
scrollTop: mTouch.pageY + _.itemHeight - (_.windowHeight - _.realBottomSize),
});
} else if (mTouch.clientY < _.itemHeight + _.realTopSize) {
// 当前触摸点pageY - item高度 - 顶部固定区域高度
this.callMethod('pageScroll', {
scrollTop: mTouch.pageY - _.itemHeight - _.realTopSize,
});
}
// 设置当前激活元素偏移量
this.setDragItemStyle(index, `transform: translate3d(${tranX}px, ${tranY}px, 0)`);
const startKey = st.list[st.cur].sortKey;
const curX = Math.round(tranX / _.itemWidth);
const curY = Math.round(tranY / _.itemHeight);
const endKey = curX + _.columns * curY;
// 目标项是固定项则返回
const item = st.list[endKey];
if (item && item.fixed) return;
// X轴或Y轴超出范围则返回
if (isOutRange(curX, _.columns, curY, _.rows, endKey, st.list.length)) return;
// 防止拖拽过程中发生乱序问题
if (startKey === endKey || startKey === st.preStartKey) return;
st.preStartKey = startKey;
dragCollisionList = sortCore(startKey, endKey, st);
startIndex = startKey;
endIndex = endKey;
st.list.forEach((_, index) => {
const item = dragCollisionList[index];
if (index !== st.cur) {
this.setDragItemStyle(index, `transform: translate3d(${item.tranX},${item.tranY}, 0)`);
}
});
this.callMethod('dragVibrate', { vibrateType: 'touchMove' });
this.callMethod('dragCollision', {
dragCollisionList,
startIndex,
endIndex,
});
triggerCustomEvent(dragCollisionList, 'change', this);
}
export function touchEnd(event, index) {
const st = this.getState();
if (!st.dragging) return;
triggerCustomEvent(st.list, 'sortend', this);
this.setDragItemClass(index, 'remove', `${classPrefix}__drag--cur`);
this.setDragItemClass(index, 'add', `${classPrefix}__drag--tran`);
this.setDragItemStyle(index, `transform: translate3d(${st.list[st.cur].tranX},${st.list[st.cur].tranY}, 0)`);
st.preStartKey = -1;
st.dragging = false;
this.callMethod('dragStatusChange', { dragging: false });
this.callMethod('dragEnd', {
dragCollisionList,
startIndex,
endIndex,
});
st.cur = -1;
st.tranX = 0;
st.tranY = 0;
}
export function baseDataObserver(newVal) {
if (newVal === undefined) return;
const st = this.getState();
st.dragBaseData = newVal;
classPrefix = newVal.classPrefix;
}
export function listObserver(newVal) {
if (newVal === undefined) return;
const st = this.getState();
st.list = newVal || [];
st.list.forEach((item, index) => {
this.setDragItemClass(index, 'remove', `${classPrefix}__drag--tran`);
this.setDragItemStyle(index, `transform: translate3d(${item.tranX},${item.tranY}, 0)`);
if (item.fixed) this.setDragItemClass(index, 'add', `${classPrefix}__drag--fixed`);
});
dragCollisionList = [];
}

View File

@@ -0,0 +1,136 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
import type { TdUploadProps } from './type';
export default {
/** 添加按钮 */
addBtn: {
type: Boolean,
default: true,
},
/** 添加按钮内容 */
addContent: {
type: String,
},
/** 是否允许重复上传相同文件名的文件 */
allowUploadDuplicateFile: Boolean,
/** 图片上传配置,视频上传配置,文件上传配置等,包含图片尺寸、图片来源、视频来源、视频拍摄最长时间等。更多细节查看小程序官网。[图片上传](https://developers.weixin.qq.com/miniprogram/dev/api/media/image/wx.chooseImage.html)。[视频上传](https://developers.weixin.qq.com/miniprogram/dev/api/media/video/wx.chooseVideo.html) */
config: {
type: Object,
},
/** 是否禁用组件 */
disabled: {
type: Boolean,
default: undefined,
},
/** 是否支持拖拽排序。长按时是否振动,碰撞时是否振动。示例一:`true`。示例二:`{ vibrate: true, collisionVibrate: true }` */
draggable: {
type: [Boolean, Object],
},
/** 已上传文件列表 */
files: {
type: Array,
},
/** 已上传文件列表,非受控属性 */
defaultFiles: {
type: Array,
},
/** upload组件每行上传图片列数以及图片的宽度和高度 */
gridConfig: {
type: Object,
},
/** 预览窗格的 `gutter` 大小,单位 rpx */
gutter: {
type: Number,
default: 16,
},
/** 透传 Image 组件全部属性 */
imageProps: {
type: Object,
},
/** 用于控制文件上传数量,值为 0 则不限制 */
max: {
type: Number,
default: 0,
},
/** 支持上传的文件类型,图片或视频 */
mediaType: {
type: Array,
default: ['image', 'video'],
},
/** 是否支持图片预览,文件没有预览 */
preview: {
type: Boolean,
default: true,
},
/** 移除按钮 */
removeBtn: {
type: Boolean,
default: true,
},
/** 自定义上传方法 */
requestMethod: {
type: Function,
},
/** 图片文件大小限制,默认单位 KB。可选单位有`'B' | 'KB' | 'MB' | 'GB'`。示例一:`1000`。示例二:`{ size: 2, unit: 'MB', message: '图片大小不超过 {sizeLimit} MB' }` */
sizeLimit: {
type: [Number, Object],
},
/** 来源 */
source: {
type: String,
default: 'media' as TdUploadProps['source'],
validator(val: TdUploadProps['source']): boolean {
if (!val) return true;
return ['media', 'messageFile'].includes(val);
},
},
/** 拖拽位置移动时的过渡参数,`duration`单位为ms */
transition: {
type: Object,
default: () => ({ backTransition: true, duration: 300, timingFunction: 'ease' }),
},
/** 选择后触发,仅包含本次选择的照片;`url` 表示选定视频的临时文件路径 (本地路径)。`duration` 表示选定视频的时间长度。`size`选定视频的数据量大小。更多描述参考 wx.chooseMedia 小程序官网描述 */
onAdd: {
type: Function,
default: () => ({}),
},
/** 点击已选文件时触发;常用于重新上传 */
onClick: {
type: Function,
default: () => ({}),
},
/** 上传成功或失败后触发 */
onComplete: {
type: Function,
default: () => ({}),
},
/** 拖拽结束后触发,包含所有上传的文件(拖拽后的文件顺序);`url` 表示选定视频的临时文件路径 (本地路径)。`duration` 表示选定视频的时间长度。`size` 选定视频的数据量大小。更多描述参考 wx.chooseMedia 小程序官网描述 */
onDrop: {
type: Function,
default: () => ({}),
},
/** 上传失败后触发 */
onFail: {
type: Function,
default: () => ({}),
},
/** 移除文件时触发 */
onRemove: {
type: Function,
default: () => ({}),
},
/** 选择文件或图片之后,上传之前,触发该事件。<br />`files` 表示之前已经上传完成的文件列表。<br />`currentSelectedFiles` 表示本次上传选中的文件列表 */
onSelectChange: {
type: Function,
default: () => ({}),
},
/** 上传成功后触发,包含所有上传的文件;`url` 表示选定视频的临时文件路径 (本地路径)。`duration` 表示选定视频的时间长度。`size`选定视频的数据量大小。更多描述参考 wx.chooseMedia 小程序官网描述 */
onSuccess: {
type: Function,
default: () => ({}),
},
};

View File

@@ -0,0 +1,197 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
import type { TdImageProps as ImageProps } from '../image/type';
export interface TdUploadProps {
/**
* 添加按钮
* @default true
*/
addBtn?: boolean;
/**
* 添加按钮内容
*/
addContent?: string;
/**
* 是否允许重复上传相同文件名的文件
* @default false
*/
allowUploadDuplicateFile?: boolean;
/**
* 图片上传配置,视频上传配置,文件上传配置等,包含图片尺寸、图片来源、视频来源、视频拍摄最长时间等。更多细节查看小程序官网。[图片上传](https://developers.weixin.qq.com/miniprogram/dev/api/media/image/wx.chooseImage.html)。[视频上传](https://developers.weixin.qq.com/miniprogram/dev/api/media/video/wx.chooseVideo.html)
*/
config?: UploadMpConfig;
/**
* 是否禁用组件
*/
disabled?: boolean;
/**
* 是否支持拖拽排序。长按时是否振动,碰撞时是否振动。示例一:`true`。示例二:`{ vibrate: true, collisionVibrate: true }`
*/
draggable?: boolean | { vibrate?: boolean; collisionVibrate?: boolean };
/**
* 已上传文件列表
*/
files?: Array<UploadFile>;
/**
* 已上传文件列表,非受控属性
*/
defaultFiles?: Array<UploadFile>;
/**
* upload组件每行上传图片列数以及图片的宽度和高度
*/
gridConfig?: { column?: number; width?: number; height?: number };
/**
* 预览窗格的 `gutter` 大小,单位 rpx
* @default 16
*/
gutter?: number;
/**
* 透传 Image 组件全部属性
*/
imageProps?: ImageProps;
/**
* 用于控制文件上传数量,值为 0 则不限制
* @default 0
*/
max?: number;
/**
* 支持上传的文件类型,图片或视频
* @default ['image', 'video']
*/
mediaType?: Array<MediaType>;
/**
* 是否支持图片预览,文件没有预览
* @default true
*/
preview?: boolean;
/**
* 移除按钮
* @default true
*/
removeBtn?: boolean;
/**
* 自定义上传方法
*/
requestMethod?: any;
/**
* 图片文件大小限制,默认单位 KB。可选单位有`'B' | 'KB' | 'MB' | 'GB'`。示例一:`1000`。示例二:`{ size: 2, unit: 'MB', message: '图片大小不超过 {sizeLimit} MB' }`
*/
sizeLimit?: number | SizeLimitObj;
/**
* 来源
* @default media
*/
source?: 'media' | 'messageFile';
/**
* 拖拽位置移动时的过渡参数,`duration`单位为ms
* @default `{backTransition: true, duration: 300, timingFunction: 'ease'}`
*/
transition?: Transition;
/**
* 选择后触发,仅包含本次选择的照片;`url` 表示选定视频的临时文件路径 (本地路径)。`duration` 表示选定视频的时间长度。`size`选定视频的数据量大小。更多描述参考 wx.chooseMedia 小程序官网描述
*/
onAdd?: (context: { files: MediaContext }) => void;
/**
* 点击已选文件时触发;常用于重新上传
*/
onClick?: (context: { index: number; file: VideoContext | ImageContext }) => void;
/**
* 上传成功或失败后触发
*/
onComplete?: () => void;
/**
* 拖拽结束后触发,包含所有上传的文件(拖拽后的文件顺序);`url` 表示选定视频的临时文件路径 (本地路径)。`duration` 表示选定视频的时间长度。`size` 选定视频的数据量大小。更多描述参考 wx.chooseMedia 小程序官网描述
*/
onDrop?: (context: { files: MediaContext }) => void;
/**
* 上传失败后触发
*/
onFail?: () => void;
/**
* 移除文件时触发
*/
onRemove?: (context: { index: number; file: UploadFile }) => void;
/**
* 选择文件或图片之后,上传之前,触发该事件。<br />`files` 表示之前已经上传完成的文件列表。<br />`currentSelectedFiles` 表示本次上传选中的文件列表
*/
onSelectChange?: (context: { files: MediaContext[]; currentSelectedFiles: MediaContext[] }) => void;
/**
* 上传成功后触发,包含所有上传的文件;`url` 表示选定视频的临时文件路径 (本地路径)。`duration` 表示选定视频的时间长度。`size`选定视频的数据量大小。更多描述参考 wx.chooseMedia 小程序官网描述
*/
onSuccess?: (context: { files: MediaContext }) => void;
}
export type UploadMpConfig = ImageConfig | VideoConfig;
export interface ImageConfig {
count?: number;
sizeType?: Array<SizeTypeValues>;
sourceType?: Array<SourceTypeValues>;
}
export type SizeTypeValues = 'original' | 'compressed';
export type SourceTypeValues = 'album' | 'camera';
export interface VideoConfig {
sourceType?: Array<SourceTypeValues>;
compressed?: boolean;
maxDuration?: number;
camera?: 'back' | 'front';
}
export interface UploadFile {
url: string;
name?: string;
size?: number;
type?: 'image' | 'video';
percent?: number;
status: 'loading' | 'reload' | 'failed' | 'done';
}
export type MediaType = 'image' | 'video';
export interface SizeLimitObj {
size: number;
unit: SizeUnit;
message?: string;
}
export type SizeUnitArray = ['B', 'KB', 'MB', 'GB'];
export type SizeUnit = SizeUnitArray[number];
export interface Transition {
backTransition?: boolean;
duration?: number;
timingFunction?: string;
}
export type MediaContext = VideoContext[] | ImageContext[];
export interface VideoContext {
name?: string;
type?: string;
url?: string;
duration?: number;
size?: number;
width?: number;
height?: number;
thumb: string;
progress: number;
}
export interface ImageContext {
name: string;
type: string;
url: string;
size: number;
width: number;
height: number;
progress: number;
}

View File

@@ -0,0 +1,13 @@
export function getWrapperAriaRole(file) {
return file.status && file.status != 'done' ? 'text' : 'button';
}
export function getWrapperAriaLabel(file) {
if (file.status && file.status != 'done') {
if (file.status == 'loading') {
return file.percent ? `上传中:${file.percent}%` : '上传中';
}
return file.status == 'reload' ? '重新上传' : '上传失败';
}
return file.type === 'video' ? '视频' : '图像';
}

View File

@@ -0,0 +1,116 @@
.t-upload {
position: relative;
}
:deep(.t-upload__grid)-content {
padding: 0;
}
:deep(.t-upload__grid)-file {
position: relative;
}
.t-upload__add-icon {
width: 100%;
height: 100%;
display: none;
align-items: center;
justify-content: center;
font-size: var(--td-upload-add-icon-size, 56rpx);
background-color: var(--td-upload-add-bg-color, var(--td-bg-color-secondarycontainer, var(--td-gray-color-1, #f3f3f3)));
color: var(--td-upload-add-color, var(--td-text-color-placeholder, var(--td-font-gray-3, rgba(0, 0, 0, 0.4))));
border-radius: var(--td-upload-radius, var(--td-radius-default, 12rpx));
}
.t-upload__add-icon--disabled {
background-color: var(--td-upload-add-disabled-bg-color, var(--td-bg-color-component-disabled, var(--td-gray-color-2, #eeeeee)));
color: var(--td-upload-add-icon-disabled-color, var(--td-text-color-disabled, var(--td-font-gray-4, rgba(0, 0, 0, 0.26))));
}
.t-upload__add-icon:only-child {
display: flex;
}
:deep(.t-upload__thumbnail) {
width: 100%;
height: 100%;
max-height: 100%;
overflow: hidden;
}
.t-upload__wrapper {
position: relative;
border-radius: var(--td-upload-radius, var(--td-radius-default, 12rpx));
overflow: hidden;
}
.t-upload__wrapper--disabled::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--td-upload-disabled-mask, rgba(0, 0.6));
z-index: 1;
}
.t-upload__close-btn {
position: absolute;
top: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
width: 40rpx;
height: 40rpx;
border-top-right-radius: var(--td-upload-radius, var(--td-radius-default, 12rpx));
border-bottom-left-radius: var(--td-upload-radius, var(--td-radius-default, 12rpx));
background-color: var(--td-font-gray-3, rgba(0, 0, 0, 0.4));
}
.t-upload__progress-mask {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: var(--td-font-gray-2, rgba(0, 0, 0, 0.6));
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: var(--td-upload-radius, var(--td-radius-default, 12rpx));
color: var(--td-text-color-anti, var(--td-font-white-1, #ffffff));
padding: 32rpx 0;
box-sizing: border-box;
}
.t-upload__progress-text {
font: var(--td-font-body-small, 24rpx / 40rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular));
margin-top: 8rpx;
}
:deep(.t-upload__progress-loading) {
animation: spin infinite linear 0.6s;
}
.t-upload__drag {
position: relative;
width: 100%;
--td-grid-item-bg-color: transparent;
}
.t-upload__drag-item {
position: absolute;
z-index: 1;
top: 0px;
left: 0px;
height: auto;
width: 100%;
}
.t-upload__drag--fixed {
z-index: 0;
}
.t-upload__drag--tran {
transition-property: transform;
transition-duration: var(--td-upload-drag-transition-duration);
transition-timing-function: var(--td-upload-drag-transition-timing-function);
}
.t-upload__drag--cur {
z-index: var(--td-upload-drag-z-index, 999);
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@@ -0,0 +1,887 @@
<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>