uview-pro 0.5.18 → 0.6.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +25 -0
- package/components/u-divider/u-divider.vue +3 -2
- package/components/u-upload/types.ts +78 -9
- package/components/u-upload/u-upload.vue +1168 -230
- package/locale/lang/en-US.ts +8 -2
- package/locale/lang/zh-CN.ts +8 -2
- package/package.json +1 -1
- package/types/global.d.ts +25 -0
|
@@ -1,59 +1,205 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<view
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
2
|
+
<view
|
|
3
|
+
v-if="!disabled"
|
|
4
|
+
class="u-upload"
|
|
5
|
+
:class="[customClass, { 'u-upload--list': props.mode === 'list' }]"
|
|
6
|
+
:style="$u.toStyle(customStyle)"
|
|
7
|
+
>
|
|
8
|
+
<!-- 列表模式 -->
|
|
9
|
+
<template v-if="props.mode === 'list'">
|
|
10
|
+
<view v-if="showUploadList" class="u-upload-list">
|
|
11
|
+
<view
|
|
12
|
+
v-for="(item, index) in lists"
|
|
13
|
+
class="u-upload-list__item"
|
|
14
|
+
:key="index"
|
|
15
|
+
:class="{ 'u-upload-list__item--error': item.error }"
|
|
16
|
+
>
|
|
17
|
+
<!-- 左侧:图片缩略图或文件图标 -->
|
|
18
|
+
<view class="u-upload-list__left" @tap.stop="handlePreview(item, index)">
|
|
19
|
+
<!-- 图片类型:显示缩略图 -->
|
|
20
|
+
<template v-if="isImageFile(item)">
|
|
21
|
+
<image class="u-upload-list__thumb" :src="item.url || item.path" mode="aspectFill" />
|
|
22
|
+
</template>
|
|
23
|
+
<!-- 非图片类型:显示文件图标 -->
|
|
24
|
+
<template v-else>
|
|
25
|
+
<view class="u-upload-list__icon" :style="{ background: getFileIcon(item).bgColor }">
|
|
26
|
+
<u-icon :name="getFileIcon(item).name" size="32" color="var(--u-white-color)"></u-icon>
|
|
27
|
+
</view>
|
|
28
|
+
</template>
|
|
29
|
+
</view>
|
|
30
|
+
|
|
31
|
+
<!-- 中间:文件名和大小 -->
|
|
32
|
+
<view class="u-upload-list__center" @tap.stop="handlePreview(item, index)">
|
|
33
|
+
<text
|
|
34
|
+
v-if="showFileName && item.name"
|
|
35
|
+
class="u-upload-list__name"
|
|
36
|
+
:class="{ 'u-upload-list__name--error': item.error }"
|
|
37
|
+
>
|
|
38
|
+
{{ getFileName(item) }}
|
|
39
|
+
</text>
|
|
40
|
+
<text v-if="showFileSize && item.size" class="u-upload-list__size">
|
|
41
|
+
{{ formatFileSize(item.size) }}
|
|
42
|
+
</text>
|
|
43
|
+
<!-- 进度条 -->
|
|
44
|
+
<view
|
|
45
|
+
v-if="showProgress && item.progress > 0 && item.progress < 100 && !item.error"
|
|
46
|
+
class="u-upload-list__progress"
|
|
47
|
+
>
|
|
48
|
+
<u-line-progress
|
|
49
|
+
height="4"
|
|
50
|
+
:show-percent="false"
|
|
51
|
+
:percent="item.progress"
|
|
52
|
+
></u-line-progress>
|
|
53
|
+
</view>
|
|
54
|
+
<view
|
|
55
|
+
v-if="item.error"
|
|
56
|
+
class="u-upload-list__retry"
|
|
57
|
+
hover-class="u-upload-list__retry--hover"
|
|
58
|
+
hover-stay-time="150"
|
|
59
|
+
@tap.stop="retry(index)"
|
|
60
|
+
>
|
|
61
|
+
<u-icon name="reload" size="24" color="var(--u-type-error)"></u-icon>
|
|
62
|
+
<text class="u-upload-list__retry-text">{{ t('uUpload.retry') }}</text>
|
|
63
|
+
</view>
|
|
64
|
+
</view>
|
|
65
|
+
|
|
66
|
+
<!-- 右侧:删除按钮 -->
|
|
67
|
+
<view
|
|
68
|
+
v-if="deletable"
|
|
69
|
+
class="u-upload-list__right"
|
|
70
|
+
@tap.stop="deleteItem(index)"
|
|
71
|
+
:style="{
|
|
72
|
+
backgroundColor: delBgColor
|
|
73
|
+
}"
|
|
74
|
+
>
|
|
75
|
+
<u-icon :name="delIcon" size="20" :color="delColor"></u-icon>
|
|
76
|
+
</view>
|
|
77
|
+
</view>
|
|
22
78
|
</view>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
></
|
|
79
|
+
|
|
80
|
+
<!-- 列表模式:自定义文件列表插槽 -->
|
|
81
|
+
<slot name="file" :file="lists"></slot>
|
|
82
|
+
|
|
83
|
+
<!-- 列表模式:添加按钮 -->
|
|
84
|
+
<view v-if="Number(maxCount) > lists.length" class="u-upload-list__add" @tap="selectFile">
|
|
85
|
+
<slot name="addBtn"></slot>
|
|
86
|
+
<view
|
|
87
|
+
v-if="!customBtn"
|
|
88
|
+
class="u-upload-list__add-btn"
|
|
89
|
+
hover-class="u-upload-list__add-btn--hover"
|
|
90
|
+
hover-stay-time="150"
|
|
91
|
+
>
|
|
92
|
+
<u-icon name="plus" size="32" :color="$u.color.primary"></u-icon>
|
|
93
|
+
<text class="u-upload-list__add-text">{{ uploadBtnText }}</text>
|
|
94
|
+
</view>
|
|
30
95
|
</view>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
96
|
+
</template>
|
|
97
|
+
|
|
98
|
+
<!-- 网格模式(默认) -->
|
|
99
|
+
<template v-else>
|
|
100
|
+
<template v-if="showUploadList">
|
|
101
|
+
<view
|
|
102
|
+
class="u-upload-grid__item u-upload-grid__preview"
|
|
103
|
+
v-for="(item, index) in lists"
|
|
104
|
+
:key="index"
|
|
105
|
+
:style="{
|
|
106
|
+
width: $u.addUnit(width),
|
|
107
|
+
height: $u.addUnit(height)
|
|
108
|
+
}"
|
|
109
|
+
>
|
|
110
|
+
<view
|
|
111
|
+
v-if="deletable"
|
|
112
|
+
class="u-upload-grid__delete"
|
|
113
|
+
@tap.stop="deleteItem(index)"
|
|
114
|
+
:style="{
|
|
115
|
+
backgroundColor: delBgColor
|
|
116
|
+
}"
|
|
117
|
+
>
|
|
118
|
+
<u-icon :name="delIcon" size="20" :color="delColor"></u-icon>
|
|
119
|
+
</view>
|
|
120
|
+
<view class="u-upload-grid__progress">
|
|
121
|
+
<u-line-progress
|
|
122
|
+
v-if="showProgress && item.progress > 0 && item.progress !== 100 && !item.error"
|
|
123
|
+
:show-percent="false"
|
|
124
|
+
height="16"
|
|
125
|
+
:percent="item.progress"
|
|
126
|
+
></u-line-progress>
|
|
127
|
+
</view>
|
|
128
|
+
<view @tap.stop="retry(index)" v-if="item.error" class="u-upload-grid__error">{{
|
|
129
|
+
t('uUpload.retry')
|
|
130
|
+
}}</view>
|
|
131
|
+
|
|
132
|
+
<!-- 图片类型预览 -->
|
|
133
|
+
<template v-if="isImageFile(item)">
|
|
134
|
+
<image
|
|
135
|
+
@tap.stop="handlePreview(item, index)"
|
|
136
|
+
class="u-upload-grid__image"
|
|
137
|
+
:src="item.url || item.path"
|
|
138
|
+
:mode="imageMode"
|
|
139
|
+
></image>
|
|
140
|
+
</template>
|
|
141
|
+
|
|
142
|
+
<!-- 视频类型预览 -->
|
|
143
|
+
<template v-else-if="isVideoFile(item)">
|
|
144
|
+
<view class="u-upload-grid__file" @tap.stop="doPreviewFile(item, index)">
|
|
145
|
+
<view class="u-upload-grid__file-icon" :style="{ background: getFileIcon(item).bgColor }">
|
|
146
|
+
<u-icon :name="getFileIcon(item).name" size="48" color="var(--u-white-color)"></u-icon>
|
|
147
|
+
</view>
|
|
148
|
+
<view class="u-upload-grid__file-info" v-if="showFileName || showFileSize">
|
|
149
|
+
<text
|
|
150
|
+
v-if="showFileName && item.name"
|
|
151
|
+
class="u-upload-grid__file-name"
|
|
152
|
+
:style="{ color: item.error ? 'var(--u-type-error)' : 'var(--u-content-color)' }"
|
|
153
|
+
>
|
|
154
|
+
{{ getFileName(item) }}
|
|
155
|
+
</text>
|
|
156
|
+
<text v-if="showFileSize && item.size" class="u-upload-grid__file-size">{{
|
|
157
|
+
formatFileSize(item.size)
|
|
158
|
+
}}</text>
|
|
159
|
+
</view>
|
|
160
|
+
</view>
|
|
161
|
+
</template>
|
|
162
|
+
|
|
163
|
+
<!-- 其他文件类型预览 -->
|
|
164
|
+
<template v-else>
|
|
165
|
+
<view class="u-upload-grid__file" @tap.stop="doPreviewFile(item, index)">
|
|
166
|
+
<view class="u-upload-grid__file-icon" :style="{ background: getFileIcon(item).bgColor }">
|
|
167
|
+
<u-icon :name="getFileIcon(item).name" size="48" color="var(--u-white-color)"></u-icon>
|
|
168
|
+
</view>
|
|
169
|
+
<view class="u-upload-grid__file-info" v-if="showFileName || showFileSize">
|
|
170
|
+
<text
|
|
171
|
+
v-if="showFileName && item.name"
|
|
172
|
+
class="u-upload-grid__file-name"
|
|
173
|
+
:style="{ color: item.error ? 'var(--u-type-error)' : 'var(--u-content-color)' }"
|
|
174
|
+
>
|
|
175
|
+
{{ getFileName(item) }}
|
|
176
|
+
</text>
|
|
177
|
+
<text v-if="showFileSize && item.size" class="u-upload-grid__file-size">
|
|
178
|
+
{{ formatFileSize(item.size) }}
|
|
179
|
+
</text>
|
|
180
|
+
</view>
|
|
181
|
+
</view>
|
|
182
|
+
</template>
|
|
183
|
+
</view>
|
|
184
|
+
</template>
|
|
185
|
+
<slot name="file" :file="lists"></slot>
|
|
186
|
+
<view style="display: inline-block" @tap="selectFile" v-if="Number(maxCount) > lists.length">
|
|
187
|
+
<slot name="addBtn"></slot>
|
|
188
|
+
<view
|
|
189
|
+
v-if="!customBtn"
|
|
190
|
+
class="u-upload-grid__item u-upload-grid__add"
|
|
191
|
+
hover-class="u-upload-grid__add--hover"
|
|
192
|
+
hover-stay-time="150"
|
|
193
|
+
:style="{
|
|
194
|
+
width: $u.addUnit(width),
|
|
195
|
+
height: $u.addUnit(height)
|
|
196
|
+
}"
|
|
197
|
+
>
|
|
198
|
+
<u-icon name="plus" class="u-upload-grid__add-icon" size="40"></u-icon>
|
|
199
|
+
<view class="u-upload-grid__add-text">{{ uploadBtnText }}</view>
|
|
200
|
+
</view>
|
|
55
201
|
</view>
|
|
56
|
-
</
|
|
202
|
+
</template>
|
|
57
203
|
</view>
|
|
58
204
|
</template>
|
|
59
205
|
|
|
@@ -71,50 +217,71 @@ export default {
|
|
|
71
217
|
</script>
|
|
72
218
|
|
|
73
219
|
<script setup lang="ts">
|
|
74
|
-
import { ref, watch } from 'vue';
|
|
220
|
+
import { ref, watch, computed } from 'vue';
|
|
75
221
|
import { $u, useLocale } from '../..';
|
|
76
222
|
import { UploadProps } from './types';
|
|
223
|
+
import type { UploadFileItem } from '../../types/global';
|
|
77
224
|
|
|
78
225
|
/**
|
|
79
|
-
* upload
|
|
80
|
-
* @description
|
|
226
|
+
* upload 文件上传
|
|
227
|
+
* @description 该组件用于上传文件场景,支持图片、视频、文档等多种类型,支持网格和列表两种展示模式
|
|
81
228
|
* @tutorial https://uviewpro.cn/zh/components/upload.html
|
|
229
|
+
*
|
|
82
230
|
* @property {String} action 服务器上传地址
|
|
83
|
-
* @property {String
|
|
84
|
-
* @property {
|
|
85
|
-
* @property {
|
|
86
|
-
* @property {
|
|
87
|
-
* @property {
|
|
88
|
-
* @property {
|
|
89
|
-
* @property {
|
|
231
|
+
* @property {String} accept 接受上传的文件类型,可选值:image|video|file|media|all,默认image
|
|
232
|
+
* @property {String} mode 上传组件的展示模式,可选值:grid|list,默认grid
|
|
233
|
+
* @property {String Number} max-count 最大选择文件的数量(默认52)
|
|
234
|
+
* @property {String Number} max-size 选择单个文件的最大大小,单位B(byte),默认不限制(默认Number.MAX_VALUE)
|
|
235
|
+
* @property {Boolean} multiple 是否开启文件多选(默认true)
|
|
236
|
+
* @property {Boolean} disabled 是否禁用组件(默认false)
|
|
237
|
+
* @property {Boolean} auto-upload 选择完文件是否自动上传(默认true)
|
|
238
|
+
* @property {Boolean} deletable 是否显示删除文件的按钮(默认true)
|
|
239
|
+
* @property {Boolean} show-confirm 删除文件前是否显示确认弹窗(默认true)
|
|
240
|
+
* @property {Boolean} show-tips 特殊情况下是否自动提示toast(默认true)
|
|
241
|
+
* @property {Boolean} show-progress 是否显示上传进度条(默认true)
|
|
242
|
+
* @property {Boolean} show-upload-list 是否显示组件内部的文件预览列表(默认true)
|
|
243
|
+
* @property {Boolean} show-file-name 是否显示文件名(默认true)
|
|
244
|
+
* @property {Boolean} show-file-size 是否显示文件大小(默认false)
|
|
245
|
+
* @property {Boolean} preview-full-image 是否可以通过uni.previewImage预览已选择的图片(默认true)
|
|
246
|
+
* @property {Boolean} preview-file 是否可预览文件(非图片类型)(默认true)
|
|
247
|
+
* @property {Boolean} custom-btn 是否自定义选择文件的按钮,设置为true可使用addBtn插槽(默认false)
|
|
248
|
+
* @property {String} upload-text 选择文件按钮的提示文字(默认根据accept自动显示)
|
|
249
|
+
* @property {String} image-mode 预览图片的显示模式,可选值为uni的image的mode属性值(默认aspectFill)
|
|
250
|
+
* @property {String} del-icon 右上角删除图标名称,只能为uView内置图标(默认'close')
|
|
251
|
+
* @property {String} del-bg-color 右上角删除按钮的背景颜色(默认'var(--u-type-error)')
|
|
252
|
+
* @property {String} del-color 右上角删除按钮图标的颜色(默认'var(--u-white-color)')
|
|
90
253
|
* @property {String | Number} index 在各个回调事件中的最后一个参数返回,用于区别是哪一个组件的事件
|
|
91
|
-
* @property {
|
|
92
|
-
* @property {Object} header 上传携带的头信息,对象形式
|
|
254
|
+
* @property {Object} header 上传携带的请求头信息,对象形式
|
|
93
255
|
* @property {Object} form-data 上传额外携带的参数
|
|
94
|
-
* @property {String} name 上传文件的字段名,供后端获取使用(默认file)
|
|
256
|
+
* @property {String} name 上传文件的字段名,供后端获取使用(默认'file')
|
|
95
257
|
* @property {Array<String>} size-type original 原图,compressed 压缩图,默认二者都有(默认['original', 'compressed'])
|
|
96
|
-
* @property {Array<String>} source-type
|
|
97
|
-
* @property {
|
|
98
|
-
* @property {
|
|
99
|
-
* @property {
|
|
100
|
-
* @property {
|
|
101
|
-
* @property {
|
|
102
|
-
* @property {
|
|
103
|
-
* @property {
|
|
104
|
-
* @property {
|
|
105
|
-
* @property {
|
|
106
|
-
* @
|
|
107
|
-
*
|
|
108
|
-
* @event {Function} on-
|
|
109
|
-
* @event {Function} on-
|
|
110
|
-
* @event {Function} on-
|
|
111
|
-
* @event {Function} on-error 图片上传失败时触发
|
|
112
|
-
* @event {Function} on-progress 图片上传过程中的进度变化过程触发
|
|
113
|
-
* @event {Function} on-uploaded 所有图片上传完毕触发
|
|
114
|
-
* @event {Function} on-choose-complete 每次选择图片后触发,只是让外部可以得知每次选择后,内部的文件列表
|
|
258
|
+
* @property {Array<String>} source-type 选择文件的来源,album-从相册选图,camera-使用相机,默认二者都有(默认['album', 'camera'])
|
|
259
|
+
* @property {Array<String>} limit-type 限制允许上传的文件后缀,如['png', 'jpg']。优先级高于accept内置的格式限制
|
|
260
|
+
* @property {Array<String>} extension 选择文件时的扩展名过滤,仅在H5和微信小程序accept='file'时有效,如['.pdf', '.docx']
|
|
261
|
+
* @property {Object} file-icon-map 文件类型图标映射配置,用于自定义不同文件类型的图标和颜色
|
|
262
|
+
* @property {Boolean} compressed 选择视频时是否压缩(默认true)
|
|
263
|
+
* @property {Number} max-duration 选择视频时拍摄最长时长,单位秒(默认60)
|
|
264
|
+
* @property {String} camera 选择视频时摄像头方向,可选值:front|back(默认'back')
|
|
265
|
+
* @property {Array<Object>} file-list 默认显示的文件列表,数组元素为对象,必须提供url属性
|
|
266
|
+
* @property {Function} before-upload 上传前钩子,返回false或Promise.reject则跳过当前文件上传
|
|
267
|
+
* @property {Function} before-remove 删除前钩子,返回false或Promise.reject则阻止删除
|
|
268
|
+
* @property {Boolean} to-json 如果上传后的返回值为json字符串,是否自动转为json格式(默认true)
|
|
269
|
+
*
|
|
270
|
+
* @event {Function} on-oversize 文件大小超出max-size限制时触发
|
|
271
|
+
* @event {Function} on-exceed 文件数量超出max-count限制时触发
|
|
272
|
+
* @event {Function} on-choose-complete 每次选择文件后触发,返回当前文件列表
|
|
115
273
|
* @event {Function} on-choose-fail 文件选择失败时触发
|
|
274
|
+
* @event {Function} on-uploaded 所有文件上传完毕时触发
|
|
275
|
+
* @event {Function} on-success 单个文件上传成功时触发
|
|
276
|
+
* @event {Function} on-error 单个文件上传失败时触发
|
|
277
|
+
* @event {Function} on-change 单个文件上传状态改变时触发(无论成功或失败)
|
|
278
|
+
* @event {Function} on-progress 文件上传过程中的进度变化时触发
|
|
279
|
+
* @event {Function} on-remove 移除文件时触发
|
|
280
|
+
* @event {Function} on-preview 预览文件时触发
|
|
116
281
|
* @event {Function} on-list-change 文件列表发生变化时触发
|
|
117
|
-
*
|
|
282
|
+
*
|
|
283
|
+
* @example <u-upload :action="action" :file-list="fileList" accept="image"></u-upload>
|
|
284
|
+
* @example <u-upload :action="action" accept="file" mode="list" :show-file-name="true"></u-upload>
|
|
118
285
|
*/
|
|
119
286
|
|
|
120
287
|
const props = defineProps(UploadProps);
|
|
@@ -133,37 +300,280 @@ const emit = defineEmits([
|
|
|
133
300
|
'on-change',
|
|
134
301
|
'on-progress',
|
|
135
302
|
'on-remove',
|
|
136
|
-
'on-preview'
|
|
303
|
+
'on-preview',
|
|
304
|
+
'update:modelValue'
|
|
137
305
|
]);
|
|
138
306
|
|
|
139
|
-
const lists = ref<
|
|
307
|
+
const lists = ref<UploadFileItem[]>([]);
|
|
140
308
|
const uploading = ref(false);
|
|
141
309
|
|
|
142
|
-
//
|
|
310
|
+
// 默认文件图标映射
|
|
311
|
+
const defaultFileIconMap: Record<string, { name: string; bgColor: string }> = {
|
|
312
|
+
image: { name: 'photo', bgColor: 'var(--u-type-primary)' },
|
|
313
|
+
video: { name: 'play-circle', bgColor: 'var(--u-type-error)' },
|
|
314
|
+
pdf: { name: 'file-text', bgColor: 'var(--u-type-error)' },
|
|
315
|
+
word: { name: 'file-text', bgColor: 'var(--u-type-primary)' },
|
|
316
|
+
excel: { name: 'file-text', bgColor: 'var(--u-type-success)' },
|
|
317
|
+
ppt: { name: 'file-text', bgColor: 'var(--u-type-warning)' },
|
|
318
|
+
zip: { name: 'folder', bgColor: 'var(--u-tips-color)' },
|
|
319
|
+
audio: { name: 'volume-up', bgColor: 'var(--u-type-info)' },
|
|
320
|
+
file: { name: 'file-text', bgColor: 'var(--u-tips-color)' }
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// 文件扩展名到类型的映射
|
|
324
|
+
const extTypeMap: Record<string, string> = {
|
|
325
|
+
// 图片
|
|
326
|
+
png: 'image',
|
|
327
|
+
jpg: 'image',
|
|
328
|
+
jpeg: 'image',
|
|
329
|
+
gif: 'image',
|
|
330
|
+
bmp: 'image',
|
|
331
|
+
webp: 'image',
|
|
332
|
+
svg: 'image',
|
|
333
|
+
// 视频
|
|
334
|
+
mp4: 'video',
|
|
335
|
+
avi: 'video',
|
|
336
|
+
mov: 'video',
|
|
337
|
+
wmv: 'video',
|
|
338
|
+
flv: 'video',
|
|
339
|
+
mkv: 'video',
|
|
340
|
+
rmvb: 'video',
|
|
341
|
+
'3gp': 'video',
|
|
342
|
+
m3u8: 'video',
|
|
343
|
+
// PDF
|
|
344
|
+
pdf: 'pdf',
|
|
345
|
+
// Word
|
|
346
|
+
doc: 'word',
|
|
347
|
+
docx: 'word',
|
|
348
|
+
// Excel
|
|
349
|
+
xls: 'excel',
|
|
350
|
+
xlsx: 'excel',
|
|
351
|
+
csv: 'excel',
|
|
352
|
+
// PPT
|
|
353
|
+
ppt: 'ppt',
|
|
354
|
+
pptx: 'ppt',
|
|
355
|
+
// 压缩包
|
|
356
|
+
zip: 'zip',
|
|
357
|
+
rar: 'zip',
|
|
358
|
+
'7z': 'zip',
|
|
359
|
+
tar: 'zip',
|
|
360
|
+
gz: 'zip',
|
|
361
|
+
// 音频
|
|
362
|
+
mp3: 'audio',
|
|
363
|
+
wav: 'audio',
|
|
364
|
+
wma: 'audio',
|
|
365
|
+
ogg: 'audio',
|
|
366
|
+
aac: 'audio',
|
|
367
|
+
flac: 'audio'
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* 根据 accept 类型获取上传按钮文字
|
|
372
|
+
*/
|
|
373
|
+
const uploadBtnText = computed(() => {
|
|
374
|
+
// 如果用户自定义了 uploadText,优先使用
|
|
375
|
+
if (props.uploadText !== t('uUpload.uploadText')) {
|
|
376
|
+
return props.uploadText;
|
|
377
|
+
}
|
|
378
|
+
// 根据 accept 类型返回对应的国际化文本
|
|
379
|
+
switch (props.accept) {
|
|
380
|
+
case 'image':
|
|
381
|
+
return t('uUpload.uploadImage');
|
|
382
|
+
case 'video':
|
|
383
|
+
return t('uUpload.uploadVideo');
|
|
384
|
+
case 'file':
|
|
385
|
+
return t('uUpload.uploadFile');
|
|
386
|
+
case 'media':
|
|
387
|
+
return t('uUpload.uploadMedia');
|
|
388
|
+
case 'all':
|
|
389
|
+
default:
|
|
390
|
+
return t('uUpload.uploadFile');
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* 同步外部列表到内部 lists
|
|
396
|
+
* @param val 外部传入的文件列表
|
|
397
|
+
* @param isModelValue 是否来自 modelValue
|
|
398
|
+
*/
|
|
399
|
+
function syncToInternal(val: UploadFileItem[], isModelValue: boolean = false) {
|
|
400
|
+
// 只有当不是由内部变化触发时才同步(避免循环更新)
|
|
401
|
+
const externalList = isModelValue ? props.modelValue : props.fileList;
|
|
402
|
+
const externalSet = new Set(externalList.map((item: UploadFileItem) => item.url || item.path).filter(Boolean));
|
|
403
|
+
const internalSet = new Set(lists.value.map(item => item.url || item.path).filter(Boolean));
|
|
404
|
+
|
|
405
|
+
// 检查是否需要同步(简单比较长度和关键属性)
|
|
406
|
+
const needSync =
|
|
407
|
+
externalList.length !== lists.value.length ||
|
|
408
|
+
!externalList.every(
|
|
409
|
+
(item: UploadFileItem) => externalSet.has(item.url || item.path) && internalSet.has(item.url || item.path)
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
if (!needSync) return;
|
|
413
|
+
|
|
414
|
+
// 清空并重新填充(保持数据一致性)
|
|
415
|
+
lists.value = val.map((value: UploadFileItem) => {
|
|
416
|
+
const fileType = value.fileType || detectFileType(value);
|
|
417
|
+
return {
|
|
418
|
+
...value,
|
|
419
|
+
url: value.url,
|
|
420
|
+
path: value.path,
|
|
421
|
+
name: value.name || getFileNameFromPath(value.url || value.path || ''),
|
|
422
|
+
size: value.size || 0,
|
|
423
|
+
fileType,
|
|
424
|
+
error: value.error || false,
|
|
425
|
+
progress: value.progress ?? 100
|
|
426
|
+
};
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// 监听 modelValue 变化,自动同步内部 lists(v-model 模式,优先级更高)
|
|
431
|
+
watch(
|
|
432
|
+
() => props.modelValue,
|
|
433
|
+
val => {
|
|
434
|
+
if (val && val.length > 0) {
|
|
435
|
+
syncToInternal(val, true);
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
{ immediate: true, deep: true }
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
// 监听 fileList 变化,自动同步内部 lists(向后兼容模式,仅在 modelValue 为空时生效)
|
|
143
442
|
watch(
|
|
144
443
|
() => props.fileList,
|
|
145
444
|
val => {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
// 数组的some方法意思是,只要数组元素有任意一个元素条件符合,就返回true,而另一个数组的every方法的意思是数组所有元素都符合条件才返回true
|
|
150
|
-
let tmp = lists.value.some(val2 => val2.url == value.url);
|
|
151
|
-
// 如果内部没有这个图片(tmp为false),则添加到内部
|
|
152
|
-
!tmp && lists.value.push({ ...value, url: value.url, error: false, progress: 100 });
|
|
153
|
-
});
|
|
445
|
+
// 如果 modelValue 有值,优先使用 modelValue,忽略 fileList
|
|
446
|
+
if (props.modelValue && props.modelValue.length > 0) return;
|
|
447
|
+
syncToInternal(val, false);
|
|
154
448
|
},
|
|
155
449
|
{ immediate: true, deep: true }
|
|
156
450
|
);
|
|
157
451
|
|
|
158
|
-
// 监听 lists
|
|
452
|
+
// 监听 lists 变化,自动触发事件
|
|
159
453
|
watch(
|
|
160
454
|
lists,
|
|
161
455
|
n => {
|
|
456
|
+
// 触发 v-model 更新
|
|
457
|
+
emit('update:modelValue', n);
|
|
458
|
+
// 触发 on-list-change 事件(向后兼容)
|
|
162
459
|
emit('on-list-change', n, props.index);
|
|
163
460
|
},
|
|
164
461
|
{ deep: true }
|
|
165
462
|
);
|
|
166
463
|
|
|
464
|
+
/**
|
|
465
|
+
* 从路径中获取文件名
|
|
466
|
+
*/
|
|
467
|
+
function getFileNameFromPath(path: string): string {
|
|
468
|
+
if (!path) return '';
|
|
469
|
+
const arr = path.split('/');
|
|
470
|
+
return arr[arr.length - 1] || '';
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* 根据扩展名检测文件类型
|
|
475
|
+
*/
|
|
476
|
+
function getFileTypeByExt(ext: string): 'image' | 'video' | 'file' {
|
|
477
|
+
const lowerExt = ext.toLowerCase();
|
|
478
|
+
if (['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg'].includes(lowerExt)) {
|
|
479
|
+
return 'image';
|
|
480
|
+
}
|
|
481
|
+
if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'rmvb', '3gp', 'm3u8'].includes(lowerExt)) {
|
|
482
|
+
return 'video';
|
|
483
|
+
}
|
|
484
|
+
return 'file';
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* 检测文件类型
|
|
489
|
+
*/
|
|
490
|
+
function detectFileType(file: UploadFileItem): 'image' | 'video' | 'file' {
|
|
491
|
+
return getFileTypeByExt(getFileExt(file));
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* 获取文件扩展名
|
|
496
|
+
*/
|
|
497
|
+
function getFileExt(file: UploadFileItem): string {
|
|
498
|
+
const path = file.url || file.path || file.name || '';
|
|
499
|
+
if (!path) return '';
|
|
500
|
+
const reg = /\.([^.]+)$/;
|
|
501
|
+
const match = path.match(reg);
|
|
502
|
+
return match ? match[1].toLowerCase() : '';
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* 判断是否为图片文件
|
|
507
|
+
*/
|
|
508
|
+
function isImageFile(file: UploadFileItem): boolean {
|
|
509
|
+
if (file.fileType) return file.fileType === 'image';
|
|
510
|
+
const ext = getFileExt(file);
|
|
511
|
+
return ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg', 'image'].includes(ext);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* 判断是否为视频文件
|
|
516
|
+
*/
|
|
517
|
+
function isVideoFile(file: UploadFileItem): boolean {
|
|
518
|
+
if (file.fileType) return file.fileType === 'video';
|
|
519
|
+
const ext = getFileExt(file);
|
|
520
|
+
return ['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'rmvb', '3gp', 'm3u8'].includes(ext);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* 获取文件图标配置
|
|
525
|
+
*/
|
|
526
|
+
function getFileIcon(file: UploadFileItem): { name: string; bgColor: string } {
|
|
527
|
+
const ext = getFileExt(file);
|
|
528
|
+
const type = extTypeMap[ext] || 'file';
|
|
529
|
+
|
|
530
|
+
// 优先使用用户自定义配置
|
|
531
|
+
if (props.fileIconMap && props.fileIconMap[type]) {
|
|
532
|
+
const customIcon = props.fileIconMap[type];
|
|
533
|
+
return {
|
|
534
|
+
name: customIcon.name,
|
|
535
|
+
bgColor: customIcon.color || defaultFileIconMap[type]?.bgColor || 'var(--u-tips-color)'
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return defaultFileIconMap[type] || defaultFileIconMap.file;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* 获取文件名(处理过长的情况)
|
|
544
|
+
*/
|
|
545
|
+
function getFileName(file: UploadFileItem): string {
|
|
546
|
+
const name = file.name || getFileNameFromPath(file.url || file.path || '');
|
|
547
|
+
if (!name) return '未知文件';
|
|
548
|
+
// 如果文件名过长,截断显示
|
|
549
|
+
if (name.length > 15) {
|
|
550
|
+
const ext = getFileExt(file);
|
|
551
|
+
return name.substring(0, 12) + '...' + (ext ? '.' + ext : '');
|
|
552
|
+
}
|
|
553
|
+
return name;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* 格式化文件大小(简化版,避免 Math.log 计算)
|
|
558
|
+
*/
|
|
559
|
+
function formatFileSize(size: number): string {
|
|
560
|
+
if (size === 0) return '0 B';
|
|
561
|
+
|
|
562
|
+
const KB = 1024;
|
|
563
|
+
const MB = KB * 1024;
|
|
564
|
+
const GB = MB * 1024;
|
|
565
|
+
|
|
566
|
+
if (size < KB) {
|
|
567
|
+
return size + ' B';
|
|
568
|
+
} else if (size < MB) {
|
|
569
|
+
return (size / KB).toFixed(2) + ' KB';
|
|
570
|
+
} else if (size < GB) {
|
|
571
|
+
return (size / MB).toFixed(2) + ' MB';
|
|
572
|
+
} else {
|
|
573
|
+
return (size / GB).toFixed(2) + ' GB';
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
167
577
|
/**
|
|
168
578
|
* 清除列表
|
|
169
579
|
*/
|
|
@@ -179,49 +589,238 @@ function reUpload() {
|
|
|
179
589
|
}
|
|
180
590
|
|
|
181
591
|
/**
|
|
182
|
-
*
|
|
592
|
+
* 选择文件
|
|
183
593
|
*/
|
|
184
594
|
function selectFile() {
|
|
185
595
|
if (props.disabled) return;
|
|
596
|
+
|
|
597
|
+
// 根据 accept 类型选择不同的文件选择方式
|
|
598
|
+
switch (props.accept) {
|
|
599
|
+
case 'image':
|
|
600
|
+
chooseImage();
|
|
601
|
+
break;
|
|
602
|
+
case 'video':
|
|
603
|
+
chooseVideo();
|
|
604
|
+
break;
|
|
605
|
+
case 'media':
|
|
606
|
+
chooseMedia();
|
|
607
|
+
break;
|
|
608
|
+
case 'file':
|
|
609
|
+
case 'all':
|
|
610
|
+
chooseFile();
|
|
611
|
+
break;
|
|
612
|
+
default:
|
|
613
|
+
chooseImage();
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* 选择图片
|
|
619
|
+
*/
|
|
620
|
+
function chooseImage() {
|
|
186
621
|
const newMaxCount = Number(props.maxCount) - lists.value.length;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
622
|
+
|
|
623
|
+
uni.chooseImage({
|
|
624
|
+
count: props.multiple ? (newMaxCount > 9 ? 9 : newMaxCount) : 1,
|
|
625
|
+
sourceType: props.sourceType,
|
|
626
|
+
sizeType: props.sizeType as string[],
|
|
627
|
+
success: (res: any) => {
|
|
628
|
+
handleFilesSelected(res.tempFiles, 'image');
|
|
629
|
+
},
|
|
630
|
+
fail: (error: any) => {
|
|
631
|
+
emit('on-choose-fail', error);
|
|
632
|
+
}
|
|
197
633
|
});
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
})
|
|
222
|
-
.catch((error: any) => {
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* 选择视频
|
|
638
|
+
*/
|
|
639
|
+
function chooseVideo() {
|
|
640
|
+
uni.chooseVideo({
|
|
641
|
+
sourceType: props.sourceType,
|
|
642
|
+
compressed: props.compressed,
|
|
643
|
+
maxDuration: props.maxDuration,
|
|
644
|
+
camera: props.camera as 'front' | 'back',
|
|
645
|
+
success: (res: any) => {
|
|
646
|
+
const file = {
|
|
647
|
+
path: res.tempFilePath,
|
|
648
|
+
name: res.name || getFileNameFromPath(res.tempFilePath),
|
|
649
|
+
size: res.size || 0,
|
|
650
|
+
width: res.width,
|
|
651
|
+
height: res.height,
|
|
652
|
+
duration: res.duration
|
|
653
|
+
};
|
|
654
|
+
handleFilesSelected([file], 'video');
|
|
655
|
+
},
|
|
656
|
+
fail: (error: any) => {
|
|
223
657
|
emit('on-choose-fail', error);
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* 选择媒体文件(图片+视频)
|
|
664
|
+
*/
|
|
665
|
+
function chooseMedia() {
|
|
666
|
+
// #ifdef MP-WEIXIN || MP-TOUTIAO || APP
|
|
667
|
+
const newMaxCount = Number(props.maxCount) - lists.value.length;
|
|
668
|
+
uni.chooseMedia({
|
|
669
|
+
count: props.multiple ? (newMaxCount > 9 ? 9 : newMaxCount) : 1,
|
|
670
|
+
mediaType: ['image', 'video'],
|
|
671
|
+
sourceType: props.sourceType,
|
|
672
|
+
maxDuration: props.maxDuration,
|
|
673
|
+
camera: props.camera as 'front' | 'back',
|
|
674
|
+
success: (res: any) => {
|
|
675
|
+
const files = res.tempFiles.map((file: any) => ({
|
|
676
|
+
path: file.tempFilePath,
|
|
677
|
+
name: file.name || getFileNameFromPath(file.tempFilePath),
|
|
678
|
+
size: file.size || 0,
|
|
679
|
+
thumb: file.thumbTempFilePath,
|
|
680
|
+
duration: file.duration,
|
|
681
|
+
width: file.width,
|
|
682
|
+
height: file.height
|
|
683
|
+
}));
|
|
684
|
+
handleFilesSelected(files, 'media');
|
|
685
|
+
},
|
|
686
|
+
fail: (error: any) => {
|
|
687
|
+
emit('on-choose-fail', error);
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
// #endif
|
|
691
|
+
|
|
692
|
+
// #ifndef MP-WEIXIN || MP-TOUTIAO || APP
|
|
693
|
+
// 非支持平台回退到选择图片
|
|
694
|
+
chooseImage();
|
|
695
|
+
// #endif
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* 选择文件
|
|
700
|
+
*/
|
|
701
|
+
function chooseFile() {
|
|
702
|
+
// #ifdef H5 || MP-WEIXIN
|
|
703
|
+
const newMaxCount = Number(props.maxCount) - lists.value.length;
|
|
704
|
+
|
|
705
|
+
// #ifdef H5
|
|
706
|
+
// H5 使用 chooseFile
|
|
707
|
+
if (typeof uni.chooseFile === 'function') {
|
|
708
|
+
uni.chooseFile({
|
|
709
|
+
count: props.multiple ? (newMaxCount > 100 ? 100 : newMaxCount) : 1,
|
|
710
|
+
type: 'all',
|
|
711
|
+
extension: props.extension.length > 0 ? props.extension : undefined,
|
|
712
|
+
success: (res: any) => {
|
|
713
|
+
handleFilesSelected(res.tempFiles, 'file');
|
|
714
|
+
},
|
|
715
|
+
fail: (error: any) => {
|
|
716
|
+
emit('on-choose-fail', error);
|
|
717
|
+
}
|
|
224
718
|
});
|
|
719
|
+
}
|
|
720
|
+
// #endif
|
|
721
|
+
|
|
722
|
+
// #ifdef MP-WEIXIN
|
|
723
|
+
// 微信小程序使用 chooseMessageFile
|
|
724
|
+
if (typeof uni.chooseMessageFile === 'function') {
|
|
725
|
+
uni.chooseMessageFile({
|
|
726
|
+
count: props.multiple ? (newMaxCount > 100 ? 100 : newMaxCount) : 1,
|
|
727
|
+
type: 'all',
|
|
728
|
+
extension: props.extension.length > 0 ? props.extension : undefined,
|
|
729
|
+
success: (res: any) => {
|
|
730
|
+
const files = res.tempFiles.map((file: any) => ({
|
|
731
|
+
path: file.path,
|
|
732
|
+
name: file.name,
|
|
733
|
+
size: file.size,
|
|
734
|
+
type: file.type
|
|
735
|
+
}));
|
|
736
|
+
handleFilesSelected(files, 'file');
|
|
737
|
+
},
|
|
738
|
+
fail: (error: any) => {
|
|
739
|
+
emit('on-choose-fail', error);
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
// #endif
|
|
744
|
+
// #endif
|
|
745
|
+
|
|
746
|
+
// #ifndef H5 || MP-WEIXIN
|
|
747
|
+
// 其他平台暂不支持文件选择,提示用户
|
|
748
|
+
showToast(t('uUpload.fileNotSupported') || '当前平台暂不支持文件选择');
|
|
749
|
+
// #endif
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* 处理选中的文件
|
|
754
|
+
*/
|
|
755
|
+
function handleFilesSelected(files: any[], sourceType: 'image' | 'video' | 'media' | 'file') {
|
|
756
|
+
let listOldLength = lists.value.length;
|
|
757
|
+
|
|
758
|
+
files.forEach((val: any, index: number) => {
|
|
759
|
+
// 检查文件后缀是否允许
|
|
760
|
+
if (!checkFileExt(val)) return;
|
|
761
|
+
|
|
762
|
+
// 如果是非多选,index大于等于1或者超出最大限制数量时,不处理
|
|
763
|
+
if (!props.multiple && index >= 1) return;
|
|
764
|
+
|
|
765
|
+
if (val.size > Number(props.maxSize)) {
|
|
766
|
+
emit('on-oversize', val, lists.value, props.index);
|
|
767
|
+
showToast(t('uUpload.overSize'));
|
|
768
|
+
} else {
|
|
769
|
+
if (Number(props.maxCount) <= lists.value.length) {
|
|
770
|
+
emit('on-exceed', val, lists.value, props.index);
|
|
771
|
+
showToast(t('uUpload.overMaxCount'));
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// 根据 accept 和 sourceType 确定 fileType
|
|
776
|
+
let fileType: 'image' | 'video' | 'file';
|
|
777
|
+
if (props.accept === 'image') {
|
|
778
|
+
fileType = 'image';
|
|
779
|
+
} else if (props.accept === 'video') {
|
|
780
|
+
fileType = 'video';
|
|
781
|
+
} else if (props.accept === 'file') {
|
|
782
|
+
fileType = 'file';
|
|
783
|
+
} else {
|
|
784
|
+
// media 或 all 时,根据路径或 sourceType 检测
|
|
785
|
+
fileType = detectFileTypeFromPath(val.path || val.name || '');
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
lists.value.push({
|
|
789
|
+
url: val.path,
|
|
790
|
+
path: val.path,
|
|
791
|
+
name: val.name || getFileNameFromPath(val.path || ''),
|
|
792
|
+
size: val.size || 0,
|
|
793
|
+
progress: 0,
|
|
794
|
+
error: false,
|
|
795
|
+
file: val,
|
|
796
|
+
fileType,
|
|
797
|
+
thumb: val.thumb || '',
|
|
798
|
+
width: val.width,
|
|
799
|
+
height: val.height,
|
|
800
|
+
duration: val.duration,
|
|
801
|
+
type: val.type
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
// 每次文件选择完,抛出一个事件,并将当前内部选择的文件数组抛出去
|
|
807
|
+
emit('on-choose-complete', lists.value, props.index);
|
|
808
|
+
if (props.autoUpload) uploadFile(listOldLength);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* 从路径中获取文件扩展名
|
|
813
|
+
*/
|
|
814
|
+
function getExtFromPath(path: string): string {
|
|
815
|
+
if (!path) return '';
|
|
816
|
+
return path.split('.').pop() || '';
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* 根据路径检测文件类型
|
|
821
|
+
*/
|
|
822
|
+
function detectFileTypeFromPath(path: string): 'image' | 'video' | 'file' {
|
|
823
|
+
return getFileTypeByExt(getExtFromPath(path));
|
|
225
824
|
}
|
|
226
825
|
|
|
227
826
|
/**
|
|
@@ -241,7 +840,7 @@ function upload() {
|
|
|
241
840
|
}
|
|
242
841
|
|
|
243
842
|
/**
|
|
244
|
-
*
|
|
843
|
+
* 对失败的文件重新上传
|
|
245
844
|
*/
|
|
246
845
|
function retry(index: number) {
|
|
247
846
|
lists.value[index].progress = 0;
|
|
@@ -254,31 +853,27 @@ function retry(index: number) {
|
|
|
254
853
|
}
|
|
255
854
|
|
|
256
855
|
/**
|
|
257
|
-
*
|
|
856
|
+
* 上传文件
|
|
258
857
|
*/
|
|
259
858
|
async function uploadFile(index = 0): Promise<void> {
|
|
260
859
|
if (props.disabled) return;
|
|
261
860
|
if (uploading.value) return;
|
|
861
|
+
|
|
262
862
|
// 全部上传完成
|
|
263
863
|
if (index >= lists.value.length) {
|
|
264
864
|
emit('on-uploaded', lists.value, props.index);
|
|
265
865
|
return;
|
|
266
866
|
}
|
|
867
|
+
|
|
267
868
|
// 检查是否是已上传或者正在上传中
|
|
268
|
-
if (lists.value[index].progress
|
|
269
|
-
if (props.autoUpload
|
|
869
|
+
if (lists.value[index].progress === 100) {
|
|
870
|
+
if (props.autoUpload === false) uploadFile(index + 1);
|
|
270
871
|
return;
|
|
271
872
|
}
|
|
873
|
+
|
|
272
874
|
// 执行before-upload钩子
|
|
273
875
|
if (props.beforeUpload && typeof props.beforeUpload === 'function') {
|
|
274
|
-
// 执行回调,同时传入索引和文件列表当作参数
|
|
275
|
-
// 在微信,支付宝等环境(H5正常),会导致父组件定义的customBack()函数体中的this变成子组件的this
|
|
276
|
-
// 通过bind()方法,绑定父组件的this,让this.customBack()的this为父组件的上下文
|
|
277
|
-
// 因为upload组件可能会被嵌套在其他组件内,比如u-form,这时this.$parent其实为u-form的this,
|
|
278
|
-
// 非页面的this,所以这里需要往上历遍,一直寻找到最顶端的$parent,这里用了this.$u.$parent.call(this)
|
|
279
|
-
// 明白意思即可,无需纠结this.$u.$parent.call(this)的细节
|
|
280
876
|
let beforeResponse = props.beforeUpload(index, lists.value);
|
|
281
|
-
// 判断是否返回了promise
|
|
282
877
|
if (
|
|
283
878
|
typeof beforeResponse === 'object' &&
|
|
284
879
|
beforeResponse !== null &&
|
|
@@ -289,32 +884,32 @@ async function uploadFile(index = 0): Promise<void> {
|
|
|
289
884
|
// promise返回成功,不进行动作,继续上传
|
|
290
885
|
})
|
|
291
886
|
.catch(() => {
|
|
292
|
-
// 进入catch
|
|
887
|
+
// 进入catch回调的话,继续下一个
|
|
293
888
|
return uploadFile(index + 1);
|
|
294
889
|
});
|
|
295
890
|
} else if (beforeResponse === false) {
|
|
296
|
-
// 如果返回false,继续下一张图片的上传
|
|
297
891
|
return uploadFile(index + 1);
|
|
298
|
-
} else {
|
|
299
|
-
// 此处为返回"true"的情形,这里不写代码,就跳过此处,继续执行当前的上传逻辑
|
|
300
892
|
}
|
|
301
893
|
}
|
|
894
|
+
|
|
302
895
|
// 检查上传地址
|
|
303
896
|
if (!props.action) {
|
|
304
897
|
showToast(t('uUpload.noAction'), true);
|
|
305
898
|
return;
|
|
306
899
|
}
|
|
900
|
+
|
|
307
901
|
lists.value[index].error = false;
|
|
308
902
|
uploading.value = true;
|
|
903
|
+
|
|
309
904
|
// 创建上传对象
|
|
310
|
-
const
|
|
905
|
+
const uploadTask = uni.uploadFile({
|
|
311
906
|
url: props.action,
|
|
312
|
-
filePath: lists.value[index].url,
|
|
907
|
+
filePath: lists.value[index].url || lists.value[index].path || '',
|
|
313
908
|
name: props.name,
|
|
314
909
|
formData: props.formData,
|
|
315
910
|
header: props.header,
|
|
316
911
|
// #ifdef MP-ALIPAY
|
|
317
|
-
fileType: 'image',
|
|
912
|
+
fileType: isImageFile(lists.value[index]) ? 'image' : 'video',
|
|
318
913
|
// #endif
|
|
319
914
|
success: (res: any) => {
|
|
320
915
|
// 判断是否json字符串,将其转为json格式
|
|
@@ -333,13 +928,19 @@ async function uploadFile(index = 0): Promise<void> {
|
|
|
333
928
|
uploadError(index, e);
|
|
334
929
|
},
|
|
335
930
|
complete: (res: any) => {
|
|
931
|
+
// 上传完成后清除任务引用
|
|
932
|
+
lists.value[index].uploadTask = undefined;
|
|
336
933
|
uni.hideLoading();
|
|
337
934
|
uploading.value = false;
|
|
338
935
|
uploadFile(index + 1);
|
|
339
936
|
emit('on-change', res, index, lists.value, props.index);
|
|
340
937
|
}
|
|
341
938
|
});
|
|
342
|
-
|
|
939
|
+
|
|
940
|
+
// 保存上传任务引用,用于后续取消上传
|
|
941
|
+
lists.value[index].uploadTask = uploadTask as unknown as UniApp.UploadTask;
|
|
942
|
+
|
|
943
|
+
uploadTask.onProgressUpdate((res: any) => {
|
|
343
944
|
if (res.progress > 0) {
|
|
344
945
|
lists.value[index].progress = res.progress;
|
|
345
946
|
emit('on-progress', res, index, lists.value, props.index);
|
|
@@ -359,59 +960,64 @@ function uploadError(index: number, err: any) {
|
|
|
359
960
|
}
|
|
360
961
|
|
|
361
962
|
/**
|
|
362
|
-
*
|
|
963
|
+
* 删除一个文件
|
|
363
964
|
*/
|
|
364
965
|
function deleteItem(index: number) {
|
|
365
|
-
uni.showModal
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
// 此处钩子执行 原理同before-remove参数,见上方注释
|
|
376
|
-
let beforeResponse = props.beforeRemove(index, lists.value);
|
|
377
|
-
// 判断是否返回了promise
|
|
378
|
-
if (
|
|
379
|
-
typeof beforeResponse === 'object' &&
|
|
380
|
-
beforeResponse !== null &&
|
|
381
|
-
typeof (beforeResponse as Promise<any>).then === 'function'
|
|
382
|
-
) {
|
|
383
|
-
await (beforeResponse as Promise<any>)
|
|
384
|
-
.then(() => {
|
|
385
|
-
// promise返回成功,不进行动作,继续上传
|
|
386
|
-
handlerDeleteItem(index);
|
|
387
|
-
})
|
|
388
|
-
.catch(() => {
|
|
389
|
-
// 如果进入promise的reject,终止删除操作
|
|
390
|
-
showToast(t('uUpload.terminatedRemove'));
|
|
391
|
-
});
|
|
392
|
-
} else if (beforeResponse === false) {
|
|
393
|
-
// 返回false,终止删除
|
|
394
|
-
showToast(t('uUpload.terminatedRemove'));
|
|
395
|
-
} else {
|
|
396
|
-
// 如果返回true,执行删除操作
|
|
397
|
-
handlerDeleteItem(index);
|
|
398
|
-
}
|
|
399
|
-
} else {
|
|
400
|
-
// 如果不存在before-remove钩子,
|
|
401
|
-
handlerDeleteItem(index);
|
|
966
|
+
// 如果需要确认弹窗,显示 uni.showModal
|
|
967
|
+
if (props.showConfirm) {
|
|
968
|
+
uni.showModal({
|
|
969
|
+
title: t('uUpload.modalTitle'),
|
|
970
|
+
content: t('uUpload.deleteConfirm'),
|
|
971
|
+
cancelText: t('uUpload.modalCancelText'),
|
|
972
|
+
confirmText: t('uUpload.modalConfirmText'),
|
|
973
|
+
success: async (res: any) => {
|
|
974
|
+
if (res.confirm) {
|
|
975
|
+
await executeBeforeRemove(index);
|
|
402
976
|
}
|
|
403
977
|
}
|
|
978
|
+
});
|
|
979
|
+
} else {
|
|
980
|
+
// 不需要确认弹窗,直接执行删除
|
|
981
|
+
executeBeforeRemove(index);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* 执行删除前的钩子并删除文件
|
|
987
|
+
*/
|
|
988
|
+
async function executeBeforeRemove(index: number) {
|
|
989
|
+
// 执行before-remove钩子
|
|
990
|
+
if (props.beforeRemove && typeof props.beforeRemove === 'function') {
|
|
991
|
+
let beforeResponse = props.beforeRemove(index, lists.value);
|
|
992
|
+
if (
|
|
993
|
+
typeof beforeResponse === 'object' &&
|
|
994
|
+
beforeResponse !== null &&
|
|
995
|
+
typeof (beforeResponse as Promise<any>).then === 'function'
|
|
996
|
+
) {
|
|
997
|
+
await (beforeResponse as Promise<any>)
|
|
998
|
+
.then(() => {
|
|
999
|
+
handlerDeleteItem(index);
|
|
1000
|
+
})
|
|
1001
|
+
.catch(() => {
|
|
1002
|
+
showToast(t('uUpload.terminatedRemove'));
|
|
1003
|
+
});
|
|
1004
|
+
} else if (beforeResponse === false) {
|
|
1005
|
+
showToast(t('uUpload.terminatedRemove'));
|
|
1006
|
+
} else {
|
|
1007
|
+
handlerDeleteItem(index);
|
|
404
1008
|
}
|
|
405
|
-
}
|
|
1009
|
+
} else {
|
|
1010
|
+
handlerDeleteItem(index);
|
|
1011
|
+
}
|
|
406
1012
|
}
|
|
407
1013
|
|
|
408
1014
|
/**
|
|
409
|
-
*
|
|
1015
|
+
* 执行移除文件的动作
|
|
410
1016
|
*/
|
|
411
1017
|
function handlerDeleteItem(index: number) {
|
|
412
|
-
//
|
|
1018
|
+
// 如果文件正在上传中,终止上传任务
|
|
413
1019
|
if (lists.value[index].progress < 100 && lists.value[index].progress > 0) {
|
|
414
|
-
typeof lists.value[index].uploadTask != 'undefined' && lists.value[index].uploadTask
|
|
1020
|
+
typeof lists.value[index].uploadTask != 'undefined' && lists.value[index].uploadTask?.abort?.();
|
|
415
1021
|
}
|
|
416
1022
|
lists.value.splice(index, 1);
|
|
417
1023
|
emit('on-remove', index, lists.value, props.index);
|
|
@@ -419,65 +1025,169 @@ function handlerDeleteItem(index: number) {
|
|
|
419
1025
|
}
|
|
420
1026
|
|
|
421
1027
|
/**
|
|
422
|
-
* 用户通过ref
|
|
1028
|
+
* 用户通过ref手动的形式,移除一个文件
|
|
423
1029
|
*/
|
|
424
1030
|
function remove(index: number) {
|
|
425
1031
|
// 判断索引的合法范围
|
|
426
1032
|
if (index >= 0 && index < lists.value.length) {
|
|
427
1033
|
lists.value.splice(index, 1);
|
|
428
|
-
emit('on-list-change', lists.value, props.index);
|
|
429
1034
|
}
|
|
430
1035
|
}
|
|
431
1036
|
|
|
1037
|
+
/**
|
|
1038
|
+
* 预览/打开文件
|
|
1039
|
+
*/
|
|
1040
|
+
function doPreviewFile(item: string | UploadFileItem, index: number) {
|
|
1041
|
+
// 处理字符串类型的文件
|
|
1042
|
+
if (typeof item === 'string') {
|
|
1043
|
+
item = { url: item };
|
|
1044
|
+
}
|
|
1045
|
+
// 图片文件预览
|
|
1046
|
+
if (isImageFile(item)) {
|
|
1047
|
+
doPreviewImage(item.url || item.path || '', index);
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
// 视频文件不支持预览
|
|
1051
|
+
if (isVideoFile(item)) {
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
// 尝试打开文档
|
|
1055
|
+
openDocument(item);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
432
1058
|
/**
|
|
433
1059
|
* 预览图片
|
|
434
1060
|
*/
|
|
435
1061
|
function doPreviewImage(url: string, index: number) {
|
|
436
|
-
// 判断是否允许预览
|
|
437
|
-
if (!props.previewFullImage) return;
|
|
438
1062
|
// 获取所有图片的url
|
|
439
|
-
const images = lists.value.map(item => item.url || item.path);
|
|
1063
|
+
const images = lists.value.filter(item => isImageFile(item)).map(item => item.url || item.path || '');
|
|
440
1064
|
uni.previewImage({
|
|
441
1065
|
urls: images,
|
|
442
1066
|
current: url,
|
|
443
1067
|
success: () => {
|
|
444
|
-
|
|
1068
|
+
console.log('预览图片成功');
|
|
445
1069
|
},
|
|
446
1070
|
fail: () => {
|
|
447
|
-
|
|
1071
|
+
showToast(t('uUpload.previewFailed'));
|
|
448
1072
|
}
|
|
449
1073
|
});
|
|
450
1074
|
}
|
|
451
1075
|
|
|
452
1076
|
/**
|
|
453
|
-
*
|
|
1077
|
+
* 打开文档
|
|
454
1078
|
*/
|
|
455
|
-
function
|
|
456
|
-
// 检查是否在允许的后缀中
|
|
457
|
-
let noArrowExt = false;
|
|
458
|
-
// 获取后缀名
|
|
459
|
-
let fileExt = '';
|
|
460
|
-
const reg = /.+\./;
|
|
461
|
-
// 如果是H5,需要从name中判断
|
|
1079
|
+
function openDocument(item: UploadFileItem) {
|
|
462
1080
|
// #ifdef H5
|
|
463
|
-
// H5
|
|
464
|
-
|
|
1081
|
+
// H5 环境直接在新标签页打开
|
|
1082
|
+
if (item.url) {
|
|
1083
|
+
window.open(item.url, '_blank');
|
|
1084
|
+
}
|
|
465
1085
|
// #endif
|
|
466
1086
|
|
|
467
|
-
// 非H5环境下从path中获取后缀
|
|
468
1087
|
// #ifndef H5
|
|
469
|
-
|
|
1088
|
+
// 其他环境尝试使用 uni.openDocument
|
|
1089
|
+
if (item.path || item.url) {
|
|
1090
|
+
uni.openDocument({
|
|
1091
|
+
filePath: item.path || item.url || '',
|
|
1092
|
+
showMenu: true,
|
|
1093
|
+
success: () => {
|
|
1094
|
+
console.log('打开文档成功');
|
|
1095
|
+
},
|
|
1096
|
+
fail: err => {
|
|
1097
|
+
console.error('打开文档失败', err);
|
|
1098
|
+
showToast(t('uUpload.openFailed') || '无法打开此文件');
|
|
1099
|
+
}
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
470
1102
|
// #endif
|
|
471
|
-
// 使用数组的some方法,只要符合limitType中的一个,就返回true
|
|
472
|
-
noArrowExt = (props.limitType as string[]).some((ext: string) => {
|
|
473
|
-
// 转为小写
|
|
474
|
-
return ext.toLowerCase() === fileExt;
|
|
475
|
-
});
|
|
476
|
-
if (!noArrowExt) showToast(t('uUpload.notAllowedExt', { ext: fileExt }));
|
|
477
|
-
return noArrowExt;
|
|
478
1103
|
}
|
|
479
1104
|
|
|
480
|
-
|
|
1105
|
+
/**
|
|
1106
|
+
* 统一处理预览
|
|
1107
|
+
*/
|
|
1108
|
+
function handlePreview(item: UploadFileItem, index: number) {
|
|
1109
|
+
emit('on-preview', item.url || item.path, lists.value, props.index);
|
|
1110
|
+
// 判断是否允许预览
|
|
1111
|
+
if (!props.previewFile) return;
|
|
1112
|
+
// 图片文件预览
|
|
1113
|
+
if (isImageFile(item) && props.previewFullImage) {
|
|
1114
|
+
doPreviewImage(item.url || item.path || '', index);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
// 文件预览
|
|
1118
|
+
doPreviewFile(item, index);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* 从文件名或路径中提取扩展名(简化版,避免正则表达式)
|
|
1123
|
+
*/
|
|
1124
|
+
function extractExt(file: { name?: string; path?: string }): string {
|
|
1125
|
+
let source: string;
|
|
1126
|
+
// #ifdef H5
|
|
1127
|
+
source = file.name || file.path || '';
|
|
1128
|
+
// #endif
|
|
1129
|
+
// #ifndef H5
|
|
1130
|
+
source = file.path || file.name || '';
|
|
1131
|
+
// #endif
|
|
1132
|
+
|
|
1133
|
+
if (!source) return '';
|
|
1134
|
+
const lastDotIndex = source.lastIndexOf('.');
|
|
1135
|
+
return lastDotIndex > -1 ? source.slice(lastDotIndex + 1).toLowerCase() : '';
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* 判断文件后缀是否允许
|
|
1140
|
+
*/
|
|
1141
|
+
function checkFileExt(file: { name?: string; path?: string }): boolean {
|
|
1142
|
+
const fileExt = extractExt(file);
|
|
1143
|
+
|
|
1144
|
+
// 根据 accept 类型确定允许的后缀
|
|
1145
|
+
let allowedExts: string[] = [];
|
|
1146
|
+
if (props.limitType && props.limitType.length > 0) {
|
|
1147
|
+
allowedExts = props.limitType;
|
|
1148
|
+
} else if (props.accept === 'image') {
|
|
1149
|
+
allowedExts = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'svg', 'image'];
|
|
1150
|
+
} else if (props.accept === 'video') {
|
|
1151
|
+
allowedExts = ['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'rmvb', '3gp', 'm3u8'];
|
|
1152
|
+
} else if (props.accept === 'file') {
|
|
1153
|
+
// file 类型使用用户设置的 extension 或允许所有文件
|
|
1154
|
+
allowedExts = props.extension.length > 0 ? (props.extension as string[]) : [];
|
|
1155
|
+
} else if (props.accept === 'media') {
|
|
1156
|
+
// media 类型,合并图片和视频格式
|
|
1157
|
+
allowedExts = [
|
|
1158
|
+
'png',
|
|
1159
|
+
'jpg',
|
|
1160
|
+
'jpeg',
|
|
1161
|
+
'gif',
|
|
1162
|
+
'webp',
|
|
1163
|
+
'bmp',
|
|
1164
|
+
'svg',
|
|
1165
|
+
'mp4',
|
|
1166
|
+
'avi',
|
|
1167
|
+
'mov',
|
|
1168
|
+
'wmv',
|
|
1169
|
+
'flv',
|
|
1170
|
+
'mkv',
|
|
1171
|
+
'rmvb',
|
|
1172
|
+
'3gp',
|
|
1173
|
+
'm3u8'
|
|
1174
|
+
];
|
|
1175
|
+
} else {
|
|
1176
|
+
// all 类型,允许所有文件
|
|
1177
|
+
return true;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// 如果没有限制类型(空数组),允许所有
|
|
1181
|
+
if (allowedExts.length === 0) return true;
|
|
1182
|
+
|
|
1183
|
+
// 使用数组的some方法,只要符合allowedExts中的一个,就返回true
|
|
1184
|
+
const isValid = allowedExts.some(ext => ext.toLowerCase() === fileExt);
|
|
1185
|
+
|
|
1186
|
+
if (!isValid) showToast(t('uUpload.notAllowedExt', { ext: fileExt || '未知' }));
|
|
1187
|
+
return isValid;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
defineExpose({ clear, reUpload, selectFile, upload, retry, remove, doPreviewImage, doPreviewFile, lists });
|
|
481
1191
|
</script>
|
|
482
1192
|
|
|
483
1193
|
<style lang="scss" scoped>
|
|
@@ -489,7 +1199,8 @@ defineExpose({ clear, reUpload, selectFile, upload, retry, remove, doPreviewImag
|
|
|
489
1199
|
align-items: center;
|
|
490
1200
|
}
|
|
491
1201
|
|
|
492
|
-
|
|
1202
|
+
// ==================== Grid 模式样式 ====================
|
|
1203
|
+
.u-upload-grid__item {
|
|
493
1204
|
width: 200rpx;
|
|
494
1205
|
height: 200rpx;
|
|
495
1206
|
overflow: hidden;
|
|
@@ -504,33 +1215,33 @@ defineExpose({ clear, reUpload, selectFile, upload, retry, remove, doPreviewImag
|
|
|
504
1215
|
justify-content: center;
|
|
505
1216
|
}
|
|
506
1217
|
|
|
507
|
-
.u-
|
|
1218
|
+
.u-upload-grid__preview {
|
|
508
1219
|
border: 1px solid var(--u-border-color);
|
|
509
1220
|
}
|
|
510
1221
|
|
|
511
|
-
.u-
|
|
1222
|
+
.u-upload-grid__add {
|
|
512
1223
|
flex-direction: column;
|
|
513
1224
|
color: $u-content-color;
|
|
514
1225
|
font-size: 26rpx;
|
|
515
1226
|
}
|
|
516
1227
|
|
|
517
|
-
.u-
|
|
1228
|
+
.u-upload-grid__add-text {
|
|
518
1229
|
margin-top: 20rpx;
|
|
519
1230
|
line-height: 40rpx;
|
|
520
1231
|
}
|
|
521
1232
|
|
|
522
|
-
.u-
|
|
1233
|
+
.u-upload-grid__add--hover {
|
|
523
1234
|
background-color: var(--u-bg-gray-light);
|
|
524
1235
|
}
|
|
525
1236
|
|
|
526
|
-
.u-
|
|
1237
|
+
.u-upload-grid__image {
|
|
527
1238
|
display: block;
|
|
528
1239
|
width: 100%;
|
|
529
1240
|
height: 100%;
|
|
530
1241
|
border-radius: 10rpx;
|
|
531
1242
|
}
|
|
532
1243
|
|
|
533
|
-
.u-
|
|
1244
|
+
.u-upload-grid__delete {
|
|
534
1245
|
position: absolute;
|
|
535
1246
|
top: 10rpx;
|
|
536
1247
|
right: 10rpx;
|
|
@@ -544,7 +1255,7 @@ defineExpose({ clear, reUpload, selectFile, upload, retry, remove, doPreviewImag
|
|
|
544
1255
|
justify-content: center;
|
|
545
1256
|
}
|
|
546
1257
|
|
|
547
|
-
.u-upload-
|
|
1258
|
+
.u-upload-grid__progress {
|
|
548
1259
|
position: absolute;
|
|
549
1260
|
bottom: 10rpx;
|
|
550
1261
|
left: 8rpx;
|
|
@@ -553,7 +1264,7 @@ defineExpose({ clear, reUpload, selectFile, upload, retry, remove, doPreviewImag
|
|
|
553
1264
|
width: auto;
|
|
554
1265
|
}
|
|
555
1266
|
|
|
556
|
-
.u-
|
|
1267
|
+
.u-upload-grid__error {
|
|
557
1268
|
color: var(--u-white-color);
|
|
558
1269
|
background-color: $u-type-error;
|
|
559
1270
|
font-size: 20rpx;
|
|
@@ -566,4 +1277,231 @@ defineExpose({ clear, reUpload, selectFile, upload, retry, remove, doPreviewImag
|
|
|
566
1277
|
z-index: 9;
|
|
567
1278
|
line-height: 1;
|
|
568
1279
|
}
|
|
1280
|
+
|
|
1281
|
+
// Grid 模式:文件预览样式
|
|
1282
|
+
.u-upload-grid__file {
|
|
1283
|
+
width: 100%;
|
|
1284
|
+
height: 100%;
|
|
1285
|
+
display: flex;
|
|
1286
|
+
flex-direction: column;
|
|
1287
|
+
align-items: center;
|
|
1288
|
+
justify-content: center;
|
|
1289
|
+
padding: 16rpx;
|
|
1290
|
+
box-sizing: border-box;
|
|
1291
|
+
position: relative;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
.u-upload-grid__file-icon {
|
|
1295
|
+
width: 80rpx;
|
|
1296
|
+
height: 80rpx;
|
|
1297
|
+
border-radius: 16rpx;
|
|
1298
|
+
display: flex;
|
|
1299
|
+
align-items: center;
|
|
1300
|
+
justify-content: center;
|
|
1301
|
+
margin-bottom: 12rpx;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
.u-upload-grid__file-play {
|
|
1305
|
+
position: absolute;
|
|
1306
|
+
top: 50%;
|
|
1307
|
+
left: 50%;
|
|
1308
|
+
transform: translate(-50%, -50%);
|
|
1309
|
+
margin-top: -20rpx;
|
|
1310
|
+
opacity: 0.9;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
.u-upload-grid__file-info {
|
|
1314
|
+
display: flex;
|
|
1315
|
+
flex-direction: column;
|
|
1316
|
+
align-items: center;
|
|
1317
|
+
width: 100%;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
.u-upload-grid__file-name {
|
|
1321
|
+
font-size: 22rpx;
|
|
1322
|
+
line-height: 1.4;
|
|
1323
|
+
text-align: center;
|
|
1324
|
+
max-width: 100%;
|
|
1325
|
+
overflow: hidden;
|
|
1326
|
+
text-overflow: ellipsis;
|
|
1327
|
+
white-space: nowrap;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
.u-upload-grid__file-size {
|
|
1331
|
+
font-size: 20rpx;
|
|
1332
|
+
color: var(--u-tips-color);
|
|
1333
|
+
margin-top: 4rpx;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// ==================== 列表模式样式 ====================
|
|
1337
|
+
.u-upload--list {
|
|
1338
|
+
flex-direction: column;
|
|
1339
|
+
align-items: stretch;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// 列表模式容器
|
|
1343
|
+
.u-upload-list {
|
|
1344
|
+
display: flex;
|
|
1345
|
+
flex-direction: column;
|
|
1346
|
+
width: 100%;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// 列表项
|
|
1350
|
+
.u-upload-list__item {
|
|
1351
|
+
display: flex;
|
|
1352
|
+
align-items: center;
|
|
1353
|
+
padding: 20rpx;
|
|
1354
|
+
background: var(--u-bg-white);
|
|
1355
|
+
border-radius: 12rpx;
|
|
1356
|
+
margin-bottom: 16rpx;
|
|
1357
|
+
border: 1rpx solid var(--u-border-color);
|
|
1358
|
+
position: relative;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
.u-upload-list__item--error {
|
|
1362
|
+
border-color: var(--u-type-error);
|
|
1363
|
+
position: relative;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
.u-upload-list__item--error::before {
|
|
1367
|
+
content: '';
|
|
1368
|
+
position: absolute;
|
|
1369
|
+
inset: 0;
|
|
1370
|
+
background-color: var(--u-type-error);
|
|
1371
|
+
opacity: 0.05;
|
|
1372
|
+
border-radius: inherit;
|
|
1373
|
+
pointer-events: none;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
.u-upload-list__item:last-child {
|
|
1377
|
+
margin-bottom: 0;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// 左侧:缩略图/图标
|
|
1381
|
+
.u-upload-list__left {
|
|
1382
|
+
flex-shrink: 0;
|
|
1383
|
+
margin-right: 20rpx;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
.u-upload-list__thumb {
|
|
1387
|
+
width: 80rpx;
|
|
1388
|
+
height: 80rpx;
|
|
1389
|
+
border-radius: 8rpx;
|
|
1390
|
+
background: var(--u-bg-gray-light);
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
.u-upload-list__icon {
|
|
1394
|
+
width: 80rpx;
|
|
1395
|
+
height: 80rpx;
|
|
1396
|
+
border-radius: 8rpx;
|
|
1397
|
+
display: flex;
|
|
1398
|
+
align-items: center;
|
|
1399
|
+
justify-content: center;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// 中间:文件名信息
|
|
1403
|
+
.u-upload-list__center {
|
|
1404
|
+
flex: 1;
|
|
1405
|
+
display: flex;
|
|
1406
|
+
flex-direction: column;
|
|
1407
|
+
min-width: 0;
|
|
1408
|
+
overflow: hidden;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
.u-upload-list__name {
|
|
1412
|
+
font-size: 28rpx;
|
|
1413
|
+
color: var(--u-content-color);
|
|
1414
|
+
line-height: 1.4;
|
|
1415
|
+
overflow: hidden;
|
|
1416
|
+
text-overflow: ellipsis;
|
|
1417
|
+
white-space: nowrap;
|
|
1418
|
+
text-align: left;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
.u-upload-list__name--error {
|
|
1422
|
+
color: var(--u-type-error);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
.u-upload-list__size {
|
|
1426
|
+
font-size: 24rpx;
|
|
1427
|
+
color: var(--u-tips-color);
|
|
1428
|
+
margin-top: 8rpx;
|
|
1429
|
+
text-align: left;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
.u-upload-list__progress {
|
|
1433
|
+
margin-top: 12rpx;
|
|
1434
|
+
width: 100%;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// 重试按钮
|
|
1438
|
+
.u-upload-list__retry {
|
|
1439
|
+
display: flex;
|
|
1440
|
+
align-items: center;
|
|
1441
|
+
margin-top: 8rpx;
|
|
1442
|
+
padding: 8rpx 16rpx;
|
|
1443
|
+
border-radius: 8rpx;
|
|
1444
|
+
align-self: flex-start;
|
|
1445
|
+
position: relative;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
.u-upload-list__retry::before {
|
|
1449
|
+
content: '';
|
|
1450
|
+
position: absolute;
|
|
1451
|
+
inset: 0;
|
|
1452
|
+
background-color: var(--u-type-error);
|
|
1453
|
+
opacity: 0.1;
|
|
1454
|
+
border-radius: inherit;
|
|
1455
|
+
pointer-events: none;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
.u-upload-list__retry--hover::before {
|
|
1459
|
+
opacity: 0.2;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
.u-upload-list__retry-text {
|
|
1463
|
+
font-size: 24rpx;
|
|
1464
|
+
color: var(--u-type-error);
|
|
1465
|
+
margin-left: 8rpx;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// 右侧:删除按钮
|
|
1469
|
+
.u-upload-list__right {
|
|
1470
|
+
flex-shrink: 0;
|
|
1471
|
+
margin-left: 20rpx;
|
|
1472
|
+
background-color: $u-type-error;
|
|
1473
|
+
border-radius: 100rpx;
|
|
1474
|
+
width: 44rpx;
|
|
1475
|
+
height: 44rpx;
|
|
1476
|
+
@include vue-flex;
|
|
1477
|
+
align-items: center;
|
|
1478
|
+
justify-content: center;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// 列表模式添加按钮
|
|
1482
|
+
.u-upload-list__add {
|
|
1483
|
+
margin-top: 16rpx;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
.u-upload-list__add-btn {
|
|
1487
|
+
display: flex;
|
|
1488
|
+
align-items: center;
|
|
1489
|
+
justify-content: center;
|
|
1490
|
+
padding: 24rpx;
|
|
1491
|
+
background: var(--u-bg-white);
|
|
1492
|
+
border: 2rpx dashed var(--u-border-color);
|
|
1493
|
+
border-radius: 12rpx;
|
|
1494
|
+
transition: all 0.3s;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
.u-upload-list__add-btn--hover {
|
|
1498
|
+
background: var(--u-bg-gray-light);
|
|
1499
|
+
border-color: var(--u-type-primary);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
.u-upload-list__add-text {
|
|
1503
|
+
margin-left: 16rpx;
|
|
1504
|
+
font-size: 28rpx;
|
|
1505
|
+
color: var(--u-type-primary);
|
|
1506
|
+
}
|
|
569
1507
|
</style>
|