New Huajishe Check ChaoXing

This commit is contained in:
e2hang
2025-10-01 10:01:52 +08:00
parent 240b884eac
commit 80be8ae3cf
1094 changed files with 61709 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
:: BASE_DOC ::
## API
### Upload Props
name | type | default | description | required
-- | -- | -- | -- | --
style | Object | - | CSS(Cascading Style Sheets) | N
custom-style | Object | - | CSS(Cascading Style Sheets)used to set style on virtual component | N
add-content | String / Slot | - | [see more ts definition](https://github.com/Tencent/tdesign-miniprogram/blob/develop/src/common/common.ts) | 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/src/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 | - | 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/src/upload/type.ts) | N
default-files | Array | undefined | 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/src/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/src/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/src/upload/type.ts) | N
request-method | Function | - | \- | 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/src/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/src/upload/type.ts) | N
### Upload Events
name | params | description
-- | -- | --
add | `(files: MediaContext)` | [see more ts definition](https://github.com/Tencent/tdesign-miniprogram/tree/develop/src/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/>
click | `(file: VideoContext \| ImageContext)` | \-
complete | \- | \-
drop | `(files: MediaContext) ` | [see more ts definition](https://github.com/Tencent/tdesign-miniprogram/tree/develop/src/upload/type.ts)。<br/>`type MediaContext = VideoContext[] \| ImageContext[]; interface VideoContext { name?: string; type?: string; url?: string; duration?: number; size?: number; width?: number; height?: number; thumb: string; progress: number }; interface ImageContext { name: string; type: string; url: string; size: number; width: number; height: number; progress: number}`<br/>
fail | \- | \-
remove | `(index: number; file: UploadFile)` | \-
select-change | `(files: MediaContext[]; currentSelectedFiles: MediaContext[])` | \-
success | `(files: MediaContext)` | [see more ts definition](https://github.com/Tencent/tdesign-miniprogram/tree/develop/src/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/>
### 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 | @font-gray-3 | -
--td-upload-add-disabled-bg-color | @bg-color-component-disabled | -
--td-upload-add-icon-disabled-color | @text-color-disabled | -
--td-upload-add-icon-font-size | 56rpx | -
--td-upload-disabled-mask | rgba(255, 255, 255, 0.55) | -
--td-upload-radius | @radius-default | -
--td-upload-drag-z-index | 999 | -

View File

@@ -0,0 +1,109 @@
---
title: Upload 上传
description: 用于相册读取或拉起拍照的图片上传功能。
spline: form
isComponent: true
---
<span class="coverages-badge" style="margin-right: 10px"><img src="https://img.shields.io/badge/coverages%3A%20lines-90%25-blue" /></span><span class="coverages-badge" style="margin-right: 10px"><img src="https://img.shields.io/badge/coverages%3A%20functions-83%25-blue" /></span><span class="coverages-badge" style="margin-right: 10px"><img src="https://img.shields.io/badge/coverages%3A%20statements-89%25-blue" /></span><span class="coverages-badge" style="margin-right: 10px"><img src="https://img.shields.io/badge/coverages%3A%20branches-81%25-blue" /></span>
## 引入
全局引入,在 miniprogram 根目录下的`app.json`中配置,局部引入,在需要引入的页面或组件的`index.json`中配置。
```json
"usingComponents": {
"t-upload": "tdesign-miniprogram/upload/upload",
}
```
## 代码演示
<a href="https://developers.weixin.qq.com/s/jz6CGimj7NSG" title="在开发者工具中预览效果" target="_blank" rel="noopener noreferrer"> 在开发者工具中预览效果 </a>
<blockquote style="background-color: #d9e1ff; font-size: 15px; line-height: 26px;margin: 16px 0 0;padding: 16px; border-radius: 6px; color: #0052d9" >
<p>Tips: 请确保开发者工具为打开状态。导入开发者工具后依次执行npm i > 构建npm包 > 勾选 "将JS编译成ES5"</p>
</blockquote>
### 单选上传图片
图片上传有两种方式:
1 选择完所有图片之后,统一上传,因此选择完就直接展示
2 每次选择图片都上传,展示每次上传图片的进度
{{ single }}
### 多选上传图片
{{ multiple }}
### 长按拖拽排序图片
{{ drag }}
### 加载状态
支持多种状态:`loading``reload``failed`
其中 `loading` 还可以通过传入 `percent` 来区分是否展示进度。
{{ status }}
### 从聊天记录上选
使用 `wx.chooseMessageFile` 实现,需要基础版本库 `2.5.0+`
{{ messageFile }}
## API
### Upload Props
名称 | 类型 | 默认值 | 描述 | 必传
-- | -- | -- | -- | --
style | Object | - | 样式 | N
custom-style | Object | - | 样式,一般用于开启虚拟化组件节点场景 | N
add-content | String / Slot | - | 添加按钮内容。[通用类型定义](https://github.com/Tencent/tdesign-miniprogram/blob/develop/src/common/common.ts) | 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/src/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 | - | 已上传文件列表。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/src/upload/type.ts) | N
default-files | Array | undefined | 已上传文件列表。非受控属性。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/src/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/src/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/src/upload/type.ts) | N
request-method | Function | - | 自定义上传方法 | 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/src/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/src/upload/type.ts) | N
### Upload Events
名称 | 参数 | 描述
-- | -- | --
add | `(files: MediaContext)` | 选择后触发,仅包含本次选择的照片;`url` 表示选定视频的临时文件路径 (本地路径)。`duration` 表示选定视频的时间长度。`size`选定视频的数据量大小。更多描述参考 wx.chooseMedia 小程序官网描述。[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/tree/develop/src/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/>
click | `(file: VideoContext \| ImageContext)` | 点击已选文件时触发;常用于重新上传
complete | \- | 上传成功或失败后触发
drop | `(files: MediaContext) ` | 拖拽结束后触发,包含所有上传的文件(拖拽后的文件顺序);`url` 表示选定视频的临时文件路径 (本地路径)。`duration` 表示选定视频的时间长度。`size` 选定视频的数据量大小。更多描述参考 wx.chooseMedia 小程序官网描述。[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/tree/develop/src/upload/type.ts)。<br/>`type MediaContext = VideoContext[] \| ImageContext[]; interface VideoContext { name?: string; type?: string; url?: string; duration?: number; size?: number; width?: number; height?: number; thumb: string; progress: number }; interface ImageContext { name: string; type: string; url: string; size: number; width: number; height: number; progress: number}`<br/>
fail | \- | 上传失败后触发
remove | `(index: number; file: UploadFile)` | 移除文件时触发
select-change | `(files: MediaContext[]; currentSelectedFiles: MediaContext[])` | 选择文件或图片之后,上传之前,触发该事件。<br />`files` 表示之前已经上传完成的文件列表。<br />`currentSelectedFiles` 表示本次上传选中的文件列表
success | `(files: MediaContext)` | 上传成功后触发,包含所有上传的文件;`url` 表示选定视频的临时文件路径 (本地路径)。`duration` 表示选定视频的时间长度。`size`选定视频的数据量大小。更多描述参考 wx.chooseMedia 小程序官网描述。[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/tree/develop/src/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/>
### CSS Variables
组件提供了下列 CSS 变量,可用于自定义样式。
名称 | 默认值 | 描述
-- | -- | --
--td-upload-add-bg-color | @bg-color-secondarycontainer | -
--td-upload-add-color | @font-gray-3 | -
--td-upload-add-disabled-bg-color | @bg-color-component-disabled | -
--td-upload-add-icon-disabled-color | @text-color-disabled | -
--td-upload-add-icon-font-size | 56rpx | -
--td-upload-disabled-mask | rgba(255, 255, 255, 0.55) | -
--td-upload-radius | @radius-default | -
--td-upload-drag-z-index | 999 | -

View File

@@ -0,0 +1,238 @@
var classPrefix = '';
var startIndex = 0;
var endIndex = 0;
var dragCollisionList = [];
var isOutRange = function (x1, y1, x2, y2, x3, y3) {
return x1 < 0 || x1 >= y1 || x2 < 0 || x2 >= y2 || x3 < 0 || x3 >= y3;
};
var sortCore = function (sKey, eKey, st) {
var _ = st.dragBaseData;
var excludeFix = function (cKey, type) {
if (st.list[cKey].fixed) {
// fixed 元素位置不会变化, 这里直接用 cKey(sortKey) 获取, 更加快捷
type ? --cKey : ++cKey;
return excludeFix(cKey, type);
}
return cKey;
};
// 先获取到 endKey 对应的 realKey, 防止下面排序过程中该 realKey 被修改
var endRealKey = -1;
st.list.forEach(function (item) {
if (item.sortKey === eKey) endRealKey = item.realKey;
});
return st.list.map(function (item) {
if (item.fixed) return item;
var cKey = item.sortKey;
var 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;
});
};
var triggerCustomEvent = function (list, type, ins) {
var _list = [],
listData = [];
list.forEach(function (item) {
_list[item.sortKey] = item;
});
_list.forEach(function (item) {
if (!item.extraNode) {
listData.push(item.data);
}
});
ins.triggerEvent(type, { listData: listData });
};
var longPress = function (event, ownerInstance) {
var ins = event.instance;
var st = ownerInstance.getState();
var _ = st.dragBaseData;
var sTouch = event.changedTouches[0];
if (!sTouch) return;
st.cur = ins.getDataset().index;
longPressIndex = st.cur;
// 初始项是固定项则返回
var item = st.list[st.cur];
if (item && item.fixed) return;
// 如果已经在 drag 中则返回, 防止多指触发 drag 动作, touchstart 事件中有效果
if (st.dragging) return;
st.dragging = true;
ownerInstance.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;
ins.setStyle({
transform: 'translate3d(' + st.tranX + 'px, ' + st.tranY + 'px, 0)',
});
st.itemsInstance.forEach(function (item, index) {
item.removeClass(classPrefix + '__drag--tran').removeClass(classPrefix + '__drag--cur');
item.addClass(index === st.cur ? classPrefix + '__drag--cur' : classPrefix + '__drag--tran');
});
ownerInstance.callMethod('dragVibrate', { vibrateType: 'longPress' });
};
var touchMove = function (event, ownerInstance) {
var ins = event.instance;
var st = ownerInstance.getState();
var _ = st.dragBaseData;
var mTouch = event.changedTouches[0];
if (!mTouch) return;
if (!st.dragging) return;
// 如果不是同一个触发点则返回
if (st.sId !== mTouch.identifier) return;
// 计算X,Y轴位移, 单列时候X轴初始不做位移
var tranX = _.columns === 1 ? 0 : mTouch.pageX - (_.itemWidth / 2 + _.wrapLeft);
var tranY = mTouch.pageY - (_.itemHeight / 2 + _.wrapTop);
// 到顶到底自动滑动
if (mTouch.clientY > _.windowHeight - _.itemHeight - _.realBottomSize) {
// 当前触摸点pageY + item高度 - (屏幕高度 - 底部固定区域高度)
ownerInstance.callMethod('pageScroll', {
scrollTop: mTouch.pageY + _.itemHeight - (_.windowHeight - _.realBottomSize),
});
} else if (mTouch.clientY < _.itemHeight + _.realTopSize) {
// 当前触摸点pageY - item高度 - 顶部固定区域高度
ownerInstance.callMethod('pageScroll', {
scrollTop: mTouch.pageY - _.itemHeight - _.realTopSize,
});
}
// 设置当前激活元素偏移量
ins.setStyle({
transform: 'translate3d(' + tranX + 'px, ' + tranY + 'px, 0)',
});
var startKey = st.list[st.cur].sortKey;
var curX = Math.round(tranX / _.itemWidth);
var curY = Math.round(tranY / _.itemHeight);
var endKey = curX + _.columns * curY;
// 目标项是固定项则返回
var 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.itemsInstance.forEach(function (itemIns, index) {
var item = dragCollisionList[index];
if (index !== st.cur) {
itemIns.setStyle({
transform: 'translate3d(' + item.tranX + ',' + item.tranY + ', 0)',
});
}
});
ownerInstance.callMethod('dragVibrate', { vibrateType: 'touchMove' });
ownerInstance.callMethod('dragCollision', {
dragCollisionList: dragCollisionList,
startIndex: startIndex,
endIndex: endIndex,
});
triggerCustomEvent(dragCollisionList, 'change', ownerInstance);
};
var touchEnd = function (event, ownerInstance) {
var ins = event.instance;
var st = ownerInstance.getState();
if (!st.dragging) return;
triggerCustomEvent(st.list, 'sortend', ownerInstance);
ins.addClass(classPrefix + '__drag--tran');
ins.setStyle({
transform: 'translate3d(' + st.list[st.cur].tranX + ',' + st.list[st.cur].tranY + ', 0)',
});
st.preStartKey = -1;
st.dragging = false;
ownerInstance.callMethod('dragStatusChange', { dragging: false });
ownerInstance.callMethod('dragEnd', {
dragCollisionList: dragCollisionList,
startIndex: startIndex,
endIndex: endIndex,
});
st.cur = -1;
st.tranX = 0;
st.tranY = 0;
};
var baseDataObserver = function (newVal, oldVal, ownerInstance, ins) {
var st = ownerInstance.getState();
st.dragBaseData = newVal;
classPrefix = newVal.classPrefix;
};
var listObserver = function (newVal, oldVal, ownerInstance, ins) {
var st = ownerInstance.getState();
st.itemsInstance = ownerInstance.selectAllComponents('.' + classPrefix + '__drag-item');
st.list = newVal || [];
st.list.forEach(function (item, index) {
var itemIns = st.itemsInstance[index];
if (item && itemIns) {
itemIns.removeClass(classPrefix + '__drag--tran');
itemIns.setStyle({
transform: 'translate3d(' + item.tranX + ',' + item.tranY + ', 0)',
});
if (item.fixed) itemIns.addClass(classPrefix + '__drag--fixed');
}
});
dragCollisionList = [];
};
module.exports = {
longPress: longPress,
touchMove: touchMove,
touchEnd: touchEnd,
baseDataObserver: baseDataObserver,
listObserver: listObserver,
};

View File

@@ -0,0 +1,3 @@
import { TdUploadProps } from './type';
declare const props: TdUploadProps;
export default props;

View File

@@ -0,0 +1,59 @@
const props = {
addContent: {
type: String,
},
allowUploadDuplicateFile: {
type: Boolean,
value: false,
},
config: {
type: Object,
},
disabled: {
type: null,
value: undefined,
},
draggable: {
type: null,
},
files: {
type: Array,
value: null,
},
defaultFiles: {
type: Array,
},
gridConfig: {
type: Object,
},
gutter: {
type: Number,
value: 16,
},
imageProps: {
type: Object,
},
max: {
type: Number,
value: 0,
},
mediaType: {
type: Array,
value: ['image', 'video'],
},
requestMethod: {
type: null,
},
sizeLimit: {
type: null,
},
source: {
type: String,
value: 'media',
},
transition: {
type: Object,
value: { backTransition: true, duration: 300, timingFunction: 'ease' },
},
};
export default props;

View File

@@ -0,0 +1,109 @@
import { ImageProps } from '../image/index';
export interface TdUploadProps {
addContent?: {
type: StringConstructor;
value?: string;
};
allowUploadDuplicateFile?: {
type: BooleanConstructor;
value?: boolean;
};
config?: {
type: ObjectConstructor;
value?: UploadMpConfig;
};
disabled?: {
type: BooleanConstructor;
value?: boolean;
};
draggable?: {
type: null;
value?: boolean | {
vibrate?: boolean;
collisionVibrate?: boolean;
};
};
files?: {
type: ArrayConstructor;
value?: Array<UploadFile>;
};
defaultFiles?: {
type: ArrayConstructor;
value?: Array<UploadFile>;
};
gridConfig?: {
type: ObjectConstructor;
value?: {
column?: number;
width?: number;
height?: number;
};
};
gutter?: {
type: NumberConstructor;
value?: number;
};
imageProps?: {
type: ObjectConstructor;
value?: ImageProps;
};
max?: {
type: NumberConstructor;
value?: number;
};
mediaType?: {
type: ArrayConstructor;
value?: Array<MediaType>;
};
requestMethod?: {
type: undefined;
value?: null;
};
sizeLimit?: {
type: null;
value?: number | SizeLimitObj;
};
source?: {
type: StringConstructor;
value?: 'media' | 'messageFile';
};
transition?: {
type: ObjectConstructor;
value?: Transition;
};
}
export declare type UploadMpConfig = ImageConfig | VideoConfig;
export interface ImageConfig {
count?: number;
sizeType?: Array<SizeTypeValues>;
sourceType?: Array<SourceTypeValues>;
}
export declare type SizeTypeValues = 'original' | 'compressed';
export declare 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 declare type MediaType = 'image' | 'video';
export interface SizeLimitObj {
size: number;
unit: SizeUnit;
message?: string;
}
export declare type SizeUnitArray = ['B', 'KB', 'MB', 'GB'];
export declare type SizeUnit = SizeUnitArray[number];
export interface Transition {
backTransition?: boolean;
duration?: number;
timingFunction?: string;
}

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,123 @@
{
"key": "Upload",
"label": "上传",
"icon": "",
"properties": [
{
"key": "addContent",
"type": ["String", "TNode"],
"defaultValue": "",
"desc": "添加按钮内容。值为空,使用默认图标渲染;值为 slot 则表示使用插槽渲染;其他值无效。",
"label": ""
},
{
"key": "config",
"type": ["Object"],
"defaultValue": "",
"desc": "图片上传配置,视频上传配置,文件上传配置等,包含图片尺寸、图片来源、视频来源、视频拍摄最长时间等。更多细节查看小程序官网。[图片上传](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)",
"label": ""
},
{
"key": "deleteBtn",
"type": ["String", "TNode"],
"defaultValue": "",
"desc": "删除图标。值为空,使用默认图标渲染;值为 slot 则表示使用插槽渲染;其他值无效。",
"label": ""
},
{
"key": "fileListDisplay",
"type": ["TNode"],
"defaultValue": "",
"desc": "【开发中】用于完全自定义文件列表内容",
"label": ""
},
{
"key": "files",
"type": ["Array"],
"defaultValue": "",
"desc": "已上传文件列表",
"label": ""
},
{
"key": "gridConfig",
"type": ["Object"],
"defaultValue": "",
"desc": "upload组件每行上传图片列数以及图片的宽度和高度",
"label": ""
},
{
"key": "gutter",
"type": ["Number"],
"defaultValue": "16",
"desc": "预览窗格的 gutter 大小,单位 rpx",
"label": ""
},
{
"key": "imageProps",
"type": ["Object"],
"defaultValue": "",
"desc": "透传 Image 组件全部属性",
"label": ""
},
{
"key": "max",
"type": ["Number"],
"defaultValue": "0",
"desc": "用于控制文件上传数量,值为 0 则不限制",
"label": ""
},
{
"key": "mediaType",
"type": ["Array"],
"defaultValue": "['image', 'video']",
"desc": "支持上传的文件类型,图片或视频",
"label": ""
},
{
"key": "requestMethod",
"type": ["Function"],
"defaultValue": "",
"desc": "自定义上传方法",
"label": ""
},
{
"key": "sizeLimit",
"type": ["Number", "Object"],
"defaultValue": "",
"desc": "图片文件大小限制,单位 KB。可选单位有`'B' | 'KB' | 'MB' | 'GB'`。示例一:`1000`。示例二:`{ size: 2, unit: 'MB', message: '图片大小不超过 {sizeLimit} MB' }`",
"label": ""
}
],
"events": [
{
"key": "add",
"desc": "上传成功后触发,仅包含本次选择的照片;`context.url` 表示选定视频的临时文件路径 (本地路径)。`context.duration` 表示选定视频的时间长度。`context.size`选定视频的数据量大小。更多描述参考 wx.chooseMedia 小程序官网描述",
"label": ""
},
{
"key": "complete",
"desc": "上传成功或失败后触发",
"label": ""
},
{
"key": "fail",
"desc": "上传失败后触发",
"label": ""
},
{
"key": "remove",
"desc": "移除文件时触发",
"label": ""
},
{
"key": "selectChange",
"desc": "选择文件或图片之后,上传之前,触发该事件。<br />`params.value` 表示之前已经上传完成的文件列表。<br />`params.currentSelectedFiles` 表示本次上传选中的文件列表",
"label": ""
},
{
"key": "success",
"desc": "上传成功后触发,包含所有上传的文件;`context.url` 表示选定视频的临时文件路径 (本地路径)。`context.duration` 表示选定视频的时间长度。`context.size`选定视频的数据量大小。更多描述参考 wx.chooseMedia 小程序官网描述",
"label": ""
}
]
}

View File

@@ -0,0 +1,60 @@
import { SuperComponent } from '../common/src/index';
import { UploadFile } from './type';
export default class Upload extends SuperComponent {
externalClasses: string[];
options: {
multipleSlots: boolean;
};
data: {
classPrefix: string;
prefix: string;
current: boolean;
proofs: any[];
customFiles: UploadFile[];
customLimit: number;
column: number;
dragBaseData: {};
rows: number;
dragWrapStyle: string;
dragList: any[];
dragging: boolean;
dragLayout: boolean;
};
properties: import("./type").TdUploadProps;
controlledProps: {
key: string;
event: string;
}[];
observers: {
'files, max, draggable'(files: UploadFile, max: number): void;
gridConfig(): void;
};
lifetimes: {
ready(): void;
};
onProofTap(e: any): void;
handleLimit(customFiles: UploadFile[], max: number): void;
triggerSuccessEvent(files: any): void;
triggerFailEvent(err: any): void;
onFileClick(e: any): void;
getFileType(mediaType: string[], tempFilePath: string, fileType?: string): string;
getRandFileName(filePath: any): string;
onDelete(e: any): void;
deleteHandle(index: number): void;
updateGrid(): void;
initDragLayout(): void;
initDragList(): void;
initDragBaseData(): void;
methods: {
uploadFiles(files: UploadFile[]): Promise<unknown>;
startUpload(files: UploadFile[]): any;
onAddTap(): void;
chooseMedia(mediaType: any): void;
chooseMessageFile(mediaType: any): void;
afterSelect(files: any): void;
dragVibrate(e: any): void;
dragStatusChange(e: any): void;
dragEnd(e: any): void;
triggerDropEvent(files: any): void;
};
}

View File

@@ -0,0 +1,352 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
import { isObject, SuperComponent, wxComponent } from '../common/src/index';
import props from './props';
import config from '../common/config';
import { isOverSize } from '../common/utils';
const { prefix } = config;
const name = `${prefix}-upload`;
let Upload = class Upload extends SuperComponent {
constructor() {
super(...arguments);
this.externalClasses = [`${prefix}-class`];
this.options = {
multipleSlots: true,
};
this.data = {
classPrefix: name,
prefix,
current: false,
proofs: [],
customFiles: [],
customLimit: 0,
column: 4,
dragBaseData: {},
rows: 0,
dragWrapStyle: '',
dragList: [],
dragging: true,
dragLayout: false,
};
this.properties = props;
this.controlledProps = [
{
key: 'files',
event: 'success',
},
];
this.observers = {
'files, max, draggable'(files, max) {
this.handleLimit(files, max);
},
gridConfig() {
this.updateGrid();
},
};
this.lifetimes = {
ready() {
this.handleLimit(this.data.customFiles, this.data.max);
this.updateGrid();
},
};
this.methods = {
uploadFiles(files) {
return new Promise((resolve) => {
const task = this.data.requestMethod(files);
if (task instanceof Promise) {
return task;
}
resolve({});
});
},
startUpload(files) {
if (typeof this.data.requestMethod === 'function') {
return this.uploadFiles(files)
.then(() => {
files.forEach((file) => {
file.percent = 100;
});
this.triggerSuccessEvent(files);
})
.catch((err) => {
this.triggerFailEvent(err);
});
}
this.triggerSuccessEvent(files);
this.handleLimit(this.data.customFiles, this.data.max);
return Promise.resolve();
},
onAddTap() {
const { disabled, mediaType, source } = this.properties;
if (disabled)
return;
if (source === 'media') {
this.chooseMedia(mediaType);
}
else {
this.chooseMessageFile(mediaType);
}
},
chooseMedia(mediaType) {
const { config, sizeLimit, customLimit } = this.data;
wx.chooseMedia(Object.assign(Object.assign({ count: customLimit, mediaType }, config), { success: (res) => {
const files = [];
res.tempFiles.forEach((temp) => {
const { size, fileType, tempFilePath, width, height, duration, thumbTempFilePath } = temp, res = __rest(temp, ["size", "fileType", "tempFilePath", "width", "height", "duration", "thumbTempFilePath"]);
if (isOverSize(size, sizeLimit)) {
let title = `${fileType === 'image' ? '图片' : '视频'}大小超过限制`;
if (typeof sizeLimit !== 'number') {
title = sizeLimit.message.replace('{sizeLimit}', sizeLimit === null || sizeLimit === void 0 ? void 0 : sizeLimit.size);
}
wx.showToast({ icon: 'none', title });
return;
}
const name = this.getRandFileName(tempFilePath);
files.push(Object.assign({ name, type: this.getFileType(mediaType, tempFilePath, fileType), url: tempFilePath, size: size, width: width, height: height, duration: duration, thumb: thumbTempFilePath, percent: 0 }, res));
});
this.afterSelect(files);
}, fail: (err) => {
this.triggerFailEvent(err);
}, complete: (res) => {
this.triggerEvent('complete', res);
} }));
},
chooseMessageFile(mediaType) {
const { max, config, sizeLimit } = this.properties;
wx.chooseMessageFile(Object.assign(Object.assign({ count: max, type: Array.isArray(mediaType) ? 'all' : mediaType }, config), { success: (res) => {
const files = [];
res.tempFiles.forEach((temp) => {
const { size, type: fileType, path: tempFilePath } = temp, res = __rest(temp, ["size", "type", "path"]);
if (isOverSize(size, sizeLimit)) {
let title = `${fileType === 'image' ? '图片' : '视频'}大小超过限制`;
if (typeof sizeLimit !== 'number') {
title = sizeLimit.message.replace('{sizeLimit}', sizeLimit === null || sizeLimit === void 0 ? void 0 : sizeLimit.size);
}
wx.showToast({ icon: 'none', title });
return;
}
const name = this.getRandFileName(tempFilePath);
files.push(Object.assign({ name, type: this.getFileType(mediaType, tempFilePath, fileType), url: tempFilePath, size: size, percent: 0 }, res));
});
this.afterSelect(files);
}, fail: (err) => this.triggerFailEvent(err), complete: (res) => this.triggerEvent('complete', res) }));
},
afterSelect(files) {
this._trigger('select-change', {
files: [...this.data.customFiles],
currentSelectedFiles: [files],
});
this._trigger('add', { files });
this.startUpload(files);
},
dragVibrate(e) {
var _a;
const { vibrateType } = e;
const { draggable } = this.data;
const dragVibrate = (_a = draggable === null || draggable === void 0 ? void 0 : draggable.vibrate) !== null && _a !== void 0 ? _a : true;
const dragCollisionVibrate = draggable === null || draggable === void 0 ? void 0 : draggable.collisionVibrate;
if ((dragVibrate && vibrateType === 'longPress') || (dragCollisionVibrate && vibrateType === 'touchMove')) {
wx.vibrateShort({
type: 'light',
});
}
},
dragStatusChange(e) {
const { dragging } = e;
this.setData({ dragging });
},
dragEnd(e) {
const { dragCollisionList } = e;
let files = [];
if (dragCollisionList.length === 0) {
files = this.data.customFiles;
}
else {
files = dragCollisionList.reduce((list, item) => {
const { realKey, data, fixed } = item;
if (!fixed) {
list[realKey] = Object.assign({}, data);
}
return list;
}, []);
}
this.triggerDropEvent(files);
},
triggerDropEvent(files) {
const { transition } = this.properties;
if (transition.backTransition) {
const timer = setTimeout(() => {
this.triggerEvent('drop', { files });
clearTimeout(timer);
}, transition.duration);
}
else {
this.triggerEvent('drop', { files });
}
},
};
}
onProofTap(e) {
var _a;
this.onFileClick(e);
const { index } = e.currentTarget.dataset;
wx.previewImage({
urls: this.data.customFiles.filter((file) => file.percent !== -1).map((file) => file.url),
current: (_a = this.data.customFiles[index]) === null || _a === void 0 ? void 0 : _a.url,
});
}
handleLimit(customFiles, max) {
if (max === 0) {
max = 20;
}
this.setData({
customFiles: customFiles.length > max ? customFiles.slice(0, max) : customFiles,
customLimit: max - customFiles.length,
dragging: true,
});
this.initDragLayout();
}
triggerSuccessEvent(files) {
this._trigger('success', { files: [...this.data.customFiles, ...files] });
}
triggerFailEvent(err) {
this.triggerEvent('fail', err);
}
onFileClick(e) {
const { file } = e.currentTarget.dataset;
this.triggerEvent('click', { file });
}
getFileType(mediaType, tempFilePath, fileType) {
if (fileType)
return 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;
}
onDelete(e) {
const { index } = e.currentTarget.dataset;
this.deleteHandle(index);
}
deleteHandle(index) {
const { customFiles } = this.data;
const delFile = customFiles[index];
this.triggerEvent('remove', { index, file: delFile });
}
updateGrid() {
let { gridConfig = {} } = this.properties;
if (!isObject(gridConfig))
gridConfig = {};
const { column = 4, width = 160, height = 160 } = gridConfig;
this.setData({
gridItemStyle: `width:${width}rpx;height:${height}rpx`,
column: column,
});
}
initDragLayout() {
const { draggable, disabled } = this.properties;
if (!draggable || disabled)
return;
this.initDragList();
this.initDragBaseData();
}
initDragList() {
let i = 0;
const { column, customFiles, customLimit } = this.data;
const dragList = [];
customFiles.forEach((item, index) => {
dragList.push({
realKey: i,
sortKey: index,
tranX: `${(index % column) * 100}%`,
tranY: `${Math.floor(index / column) * 100}%`,
data: Object.assign({}, 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.data.rows = Math.ceil(dragList.length / column);
this.setData({
dragList,
});
}
initDragBaseData() {
const { classPrefix, rows, column, customFiles } = this.data;
if (customFiles.length === 0) {
this.setData({
dragBaseData: {},
dragWrapStyle: '',
dragLayout: false,
});
return;
}
const query = this.createSelectorQuery();
const selectorGridItem = `.${classPrefix} >>> .t-grid-item`;
const 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.setData({
dragBaseData,
dragWrapStyle,
dragLayout: true,
}, () => {
const timer = setTimeout(() => {
this.setData({ dragging: false });
clearTimeout(timer);
}, 0);
});
});
}
};
Upload = __decorate([
wxComponent()
], Upload);
export default Upload;

View File

@@ -0,0 +1,10 @@
{
"component": true,
"styleIsolation": "apply-shared",
"usingComponents": {
"t-grid": "../grid/grid",
"t-grid-item": "../grid-item/grid-item",
"t-icon": "../icon/icon",
"t-image": "../image/image"
}
}

View File

@@ -0,0 +1,227 @@
<wxs src="../common/utils.wxs" module="_" />
<wxs src="./upload.wxs" module="_this" />
<wxs src="./drag.wxs" module="handler" />
<view style="{{_._style([style, customStyle])}}" class="{{classPrefix}} class {{prefix}}-class">
<t-grid
gutter="{{gutter}}"
border="{{false}}"
align="center"
column="{{column}}"
style="{{draggable? 'overflow: visible' : ''}}"
>
<block wx:if="{{!dragLayout}}">
<!-- 图片/视频 -->
<t-grid-item
wx:for="{{customFiles}}"
wx:key="url"
wx:for-item="file"
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 || _this.getWrapperAriaRole(file)}}"
aria-label="{{ariaLabel || _this.getWrapperAriaLabel(file)}}"
>
<t-image
wx:if="{{file.type !== 'video'}}"
data-file="{{file}}"
bind:tap="onProofTap"
data-index="{{index}}"
t-class="{{classPrefix}}__thumbnail"
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}}"
showMenuByLongpress="{{imageProps && imageProps.showMenuByLongpress || false}}"
/>
<video
data-file="{{file}}"
bind:tap="onFileClick"
wx:if="{{file.type === 'video'}}"
class="{{classPrefix}}__thumbnail"
src="{{file.url}}"
poster="{{ file.thumb }}"
controls
autoplay="{{false}}"
objectFit="contain"
/>
<!-- 失败重试 -->
<view
data-index="{{index}}"
wx:if="{{file.status && file.status != 'done'}}"
class="{{classPrefix}}__progress-mask"
data-file="{{file}}"
bind:tap="onFileClick"
>
<block wx: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 wx:else name="{{file.status == 'reload' ? 'refresh' : 'close-circle'}}" size="48rpx" aria-hidden />
<view wx:if="{{file.status == 'reload' || file.status == 'failed'}}" class="{{classPrefix}}__progress-text">
{{file.status == 'reload' ? '重新上传' : '上传失败'}}
</view>
</view>
<!-- 删除 -->
<view
class="{{classPrefix}}__close-btn hotspot-expanded"
bindtap="onDelete"
data-index="{{index}}"
aria-role="button"
aria-label="删除"
>
<t-icon name="close" size="32rpx" color="#fff" />
</view>
</view>
</t-grid-item>
<!-- 添加 -->
<t-grid-item
wx:if="{{customLimit > 0}}"
t-class="{{classPrefix}}__grid"
t-class-content="{{classPrefix}}__grid-content"
bindclick="onAddTap"
aria-label="上传"
>
<view class="{{classPrefix}}__wrapper" style="{{gridItemStyle}}">
<slot name="add-content" />
<block wx:if="{{addContent}}">{{addContent}}</block>
<view wx:else class="{{classPrefix}}__add-icon {{disabled? classPrefix + '__add-icon--disabled' : '' }}">
<t-icon name="add" />
</view>
</view>
</t-grid-item>
</block>
<block wx:else>
<view
class="{{classPrefix}}__drag"
list="{{dragList}}"
style="{{dragWrapStyle}};"
dragBaseData="{{dragBaseData}}"
change:list="{{handler.listObserver}}"
change:dragBaseData="{{handler.baseDataObserver}}"
>
<view
class="{{classPrefix}}__drag-item"
wx:for="{{customFiles}}"
wx:key="url"
wx:for-item="file"
style="width: {{100 / column}}%; --td-upload-drag-transition-duration: {{transition.duration}}ms; --td-upload-drag-transition-timing-function: {{transition.timingFunction}}"
bind:longpress="{{handler.longPress}}"
catch:touchmove="{{dragging ? handler.touchMove : ''}}"
catch:touchend="{{dragging ? handler.touchEnd : ''}}"
data-index="{{index}}"
>
<!-- 图片/视频 -->
<t-grid-item
t-class="{{classPrefix}}__grid {{classPrefix}}__grid-file"
t-class-content="{{classPrefix}}__grid-content"
aria-role="presentation"
style="width: 100%"
>
<view
class="{{classPrefix}}__wrapper {{disabled? classPrefix + '__wrapper--disabled' : '' }}"
style="{{gridItemStyle}};"
aria-role="{{ariaRole || _this.getWrapperAriaRole(file)}}"
aria-label="{{ariaLabel || _this.getWrapperAriaLabel(file)}}"
>
<t-image
wx:if="{{file.type !== 'video'}}"
data-file="{{file}}"
bind:tap="onProofTap"
data-index="{{index}}"
t-class="{{classPrefix}}__thumbnail"
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}}"
showMenuByLongpress="{{imageProps && imageProps.showMenuByLongpress || false}}"
/>
<video
data-file="{{file}}"
bind:tap="onFileClick"
wx:if="{{file.type === 'video'}}"
class="{{classPrefix}}__thumbnail"
src="{{file.url}}"
poster="{{ file.thumb }}"
controls
autoplay="{{false}}"
objectFit="contain"
/>
<!-- 失败重试 -->
<view
data-index="{{index}}"
wx:if="{{file.status && file.status != 'done'}}"
class="{{classPrefix}}__progress-mask"
data-file="{{file}}"
bind:tap="onFileClick"
>
<block wx: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
wx:else
name="{{file.status == 'reload' ? 'refresh' : 'close-circle'}}"
size="48rpx"
aria-hidden
/>
<view
wx:if="{{file.status == 'reload' || file.status == 'failed'}}"
class="{{classPrefix}}__progress-text"
>
{{file.status == 'reload' ? '重新上传' : '上传失败'}}
</view>
</view>
<!-- 删除 -->
<view
class="{{classPrefix}}__close-btn hotspot-expanded"
bindtap="onDelete"
data-index="{{index}}"
data-url="{{file.url}}"
aria-role="button"
aria-label="删除"
>
<t-icon name="close" size="32rpx" color="#fff" />
</view>
</view>
</t-grid-item>
<!-- 添加 -->
</view>
<view class="{{classPrefix}}__drag-item" style="width: {{100 / column}}%" wx:if="{{customLimit > 0}}">
<t-grid-item
t-class="{{classPrefix}}__grid"
t-class-content="{{classPrefix}}__grid-content"
bindclick="onAddTap"
aria-label="上传"
style="width: 100%"
>
<view class="{{classPrefix}}__wrapper" style="{{gridItemStyle}}">
<slot name="add-content" />
<block wx:if="{{addContent}}">{{addContent}}</block>
<view wx: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>

View File

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

View File

@@ -0,0 +1,144 @@
.t-float-left {
float: left;
}
.t-float-right {
float: right;
}
@keyframes tdesign-fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.hotspot-expanded.relative {
position: relative;
}
.hotspot-expanded::after {
content: '';
display: block;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
transform: scale(1.5);
}
.t-upload {
position: relative;
}
.t-upload__grid-content {
padding: 0;
}
.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-font-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;
}
.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-size: 24rpx;
line-height: 40rpx;
margin-top: 8rpx;
}
.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);
}
}