@ecolovo/uniapp-components 1.0.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/components/aspect/aspect.vue +43 -0
- package/components/badge/badge.vue +88 -0
- package/components/button/button.vue +409 -0
- package/components/checkbox/checkbox.vue +183 -0
- package/components/checkbox-group/checkbox-group.vue +101 -0
- package/components/checkbox-group2/checkbox-group2.vue +138 -0
- package/components/checkbox-group2/checkbox.vue +103 -0
- package/components/data-list/data-list.vue +130 -0
- package/components/date-picker/date-picker-view.vue +181 -0
- package/components/date-picker/date-picker.vue +237 -0
- package/components/datetime-picker/datetime-picker-view.vue +209 -0
- package/components/datetime-picker/datetime-picker.vue +235 -0
- package/components/dialog/dialog-content.vue +162 -0
- package/components/dialog/dialog.vue +166 -0
- package/components/dropdown/dropdown.vue +145 -0
- package/components/empty/empty.vue +102 -0
- package/components/flex/flex.vue +76 -0
- package/components/form/form.vue +43 -0
- package/components/form/mixin.js +13 -0
- package/components/form-item/form-item.vue +141 -0
- package/components/icon/icon.vue +114 -0
- package/components/image/image.vue +61 -0
- package/components/input/input.vue +154 -0
- package/components/input/password-input.vue +95 -0
- package/components/list-page/list-page.js +10 -0
- package/components/list-page/list-page.vue +150 -0
- package/components/load-more/load-more.vue +97 -0
- package/components/loading/animations/loading-animation1.vue +92 -0
- package/components/loading/animations/loading-animation2.vue +28 -0
- package/components/loading/animations/loading-animation3.vue +81 -0
- package/components/loading/animations/loading-animation4.vue +74 -0
- package/components/loading/loading.vue +91 -0
- package/components/message-box/message-box.vue +60 -0
- package/components/number-dialog/number-dialog.vue +360 -0
- package/components/number-input/number-input.vue +352 -0
- package/components/numpad/numpad.vue +224 -0
- package/components/page/page-body.vue +227 -0
- package/components/page/page-footer.vue +75 -0
- package/components/page/page-header.vue +222 -0
- package/components/page/page.vue +128 -0
- package/components/picker/picker-popup.vue +295 -0
- package/components/picker/picker.vue +151 -0
- package/components/popover/popover-content.vue +255 -0
- package/components/popover/popover-mask.vue +113 -0
- package/components/popover/popover.vue +84 -0
- package/components/popup/popup-content.vue +162 -0
- package/components/popup/popup.vue +168 -0
- package/components/price/price.vue +104 -0
- package/components/search/search.vue +220 -0
- package/components/searching/searching.vue +86 -0
- package/components/selector/selector.vue +86 -0
- package/components/switch/switch.vue +180 -0
- package/components/tab/tab.vue +235 -0
- package/components/table/table.vue +112 -0
- package/components/textarea/textarea.vue +192 -0
- package/components/time-picker/time-picker-view.vue +127 -0
- package/components/time-picker/time-picker.vue +140 -0
- package/components/uploader/download.js +81 -0
- package/components/uploader/file-uploader.vue +52 -0
- package/components/uploader/files.js +225 -0
- package/components/uploader/media-uploader.vue +133 -0
- package/components/uploader/mixin.js +106 -0
- package/components/uploader/uni.js +38 -0
- package/components/uploader/upload-button.vue +29 -0
- package/components/uploader/uploader copy.vue +547 -0
- package/components/uploader/uploader.vue +117 -0
- package/configs/index.js +41 -0
- package/easyCom.js +25 -0
- package/init.js +17 -0
- package/mixins/componentName.js +32 -0
- package/mixins/listPage.js +1 -0
- package/mixins/windowInfo.js +1 -0
- package/package.json +12 -0
- package/resources/iconfont/uniapp_ui_icon/demo.css +539 -0
- package/resources/iconfont/uniapp_ui_icon/demo_index.html +555 -0
- package/resources/iconfont/uniapp_ui_icon/iconfont.css +78 -0
- package/resources/iconfont/uniapp_ui_icon/iconfont.js +1 -0
- package/resources/iconfont/uniapp_ui_icon/iconfont.json +121 -0
- package/resources/iconfont/uniapp_ui_icon/iconfont.woff2 +0 -0
- package/resources/image/empty.svg +13 -0
- package/styles/global.scss +24 -0
- package/styles/index.scss +2 -0
- package/styles/utils/css-var.scss +87 -0
- package/styles/utils/ellipsis.scss +26 -0
- package/styles/utils/global-var.scss +11 -0
- package/styles/utils/index.scss +5 -0
- package/styles/utils/selector.scss +5 -0
- package/styles/utils/z-index.scss +10 -0
- package/styles/var/component.scss +242 -0
- package/styles/var/index.scss +2 -0
- package/styles/var/theme.scss +40 -0
- package/utils/date.js +151 -0
- package/utils/floor.js +20 -0
- package/utils/freezeDeep.js +19 -0
- package/utils/price.js +32 -0
- package/utils/round.js +22 -0
- package/utils/windowInfo.js +77 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
export function randomInt(max = 100, min = 0, padStart = false) {
|
|
2
|
+
const v = Math.floor(Math.random() * max + min);
|
|
3
|
+
if (!padStart) return v;
|
|
4
|
+
const n = (max - 1).toString().length;
|
|
5
|
+
return v.toString().padStart(n, '0');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const FILE_TYPE_IMAGE = 'image';
|
|
9
|
+
const FILE_TYPE_VIDEO = 'video';
|
|
10
|
+
const FILE_TYPE_FILE = 'file';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 文件
|
|
14
|
+
* @typedef {object} ChosenFile
|
|
15
|
+
* @property {string} name 文件名
|
|
16
|
+
* @property {number} size 文件大小
|
|
17
|
+
* @property {string} path 路径
|
|
18
|
+
* @property {string} type 类型
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 文件类型字符串是否有效
|
|
23
|
+
* @param {string} type 文件类型字符串 'image' 、 'video' 、 'file' 的组合或 'all'
|
|
24
|
+
* @returns {boolean} 是否有效
|
|
25
|
+
*/
|
|
26
|
+
function typeStringValidator(type) {
|
|
27
|
+
if (type === 'all') return true;
|
|
28
|
+
const arr = typeStringToArr(type);
|
|
29
|
+
if (arr.length === 0) return false;
|
|
30
|
+
return arr.every((item) => [FILE_TYPE_IMAGE, FILE_TYPE_VIDEO, FILE_TYPE_FILE].includes(item));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 将文件类型字符串转换为文件类型数组
|
|
35
|
+
* @param {string} type 文件类型字符串 'image' 、 'video' 、 'file' 的组合或 'all'
|
|
36
|
+
* @param {string} divider 分隔符
|
|
37
|
+
* @returns {string[]} 文件类型数组
|
|
38
|
+
*/
|
|
39
|
+
function typeStringToArr(type, divider = ',') {
|
|
40
|
+
if (Array.isArray(type)) return type;
|
|
41
|
+
if (type === 'all') return [FILE_TYPE_IMAGE, FILE_TYPE_VIDEO, FILE_TYPE_FILE];
|
|
42
|
+
return [...new Set(type.split(divider))];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 将文件类型数组转换为 {@link uni.chooseMessageFile} 的 `type` 参数
|
|
47
|
+
* @param {('image' | 'video' | 'file')[]} arr 文件类型,可选 'image' 、 'video' 、 'file' 组成的数组
|
|
48
|
+
* @returns {'image' | 'video' | 'file' | 'all'}
|
|
49
|
+
*/
|
|
50
|
+
function typeArrToChooseMessageFileType(arr) {
|
|
51
|
+
if (!arr || !Array.isArray(arr)) throw 'arr is invalid';
|
|
52
|
+
|
|
53
|
+
const image = arr.includes(FILE_TYPE_IMAGE);
|
|
54
|
+
const video = arr.includes(FILE_TYPE_VIDEO);
|
|
55
|
+
const file = arr.includes(FILE_TYPE_FILE);
|
|
56
|
+
if (image && video && file) return 'all';
|
|
57
|
+
|
|
58
|
+
if (file) return FILE_TYPE_FILE;
|
|
59
|
+
if (image) return FILE_TYPE_IMAGE;
|
|
60
|
+
if (video) return FILE_TYPE_VIDEO;
|
|
61
|
+
throw 'arr is invalid';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 将文件类型数组转换为 {@link uni.chooseMessageFile} 的 `type` 参数
|
|
66
|
+
* @param {('image' | 'video' | 'file')[] | undefined} arr 文件类型,可选 'image' 、 'video' 组成的数组
|
|
67
|
+
* @returns {('image' | 'video')[]}
|
|
68
|
+
*/
|
|
69
|
+
function typeArrToChooseMediaType(arr) {
|
|
70
|
+
if (!arr || !Array.isArray(arr)) throw 'arr is invalid';
|
|
71
|
+
|
|
72
|
+
const image = arr.includes(FILE_TYPE_IMAGE);
|
|
73
|
+
const video = arr.includes(FILE_TYPE_VIDEO);
|
|
74
|
+
if (image && video) return [FILE_TYPE_IMAGE, FILE_TYPE_VIDEO];
|
|
75
|
+
|
|
76
|
+
if (image) return [FILE_TYPE_IMAGE];
|
|
77
|
+
if (video) return [FILE_TYPE_VIDEO];
|
|
78
|
+
throw 'arr is invalid';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getFileExt(filename) {
|
|
82
|
+
return filename.split('.').pop();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isImage(filename) {
|
|
86
|
+
return ['jpg', 'jpeg', 'png'].includes(getFileExt(filename));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isVideo(filename) {
|
|
90
|
+
return ['mp4', 'mov', 'mkv', 'wmv', 'avi', 'flv'].includes(getFileExt(filename));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @typedef {object} chooseFileResult
|
|
95
|
+
* @property {ChosenFile[]} files 有效的文件列表
|
|
96
|
+
* @property {ChosenFile[]} invalidFiles 无效的文件列表
|
|
97
|
+
*/
|
|
98
|
+
/**
|
|
99
|
+
* 过滤文件
|
|
100
|
+
* @param {ChosenFile[]} files 选择的文件列表
|
|
101
|
+
* @param {ChosenFile[]} extensionList 限制的文件类型
|
|
102
|
+
* @param {string} key 读取文件名的 key
|
|
103
|
+
* @returns {chooseFileResult} 过滤后的结果
|
|
104
|
+
*/
|
|
105
|
+
function filterFiles(files, extensionList, key = 'name') {
|
|
106
|
+
const invalidFiles = [];
|
|
107
|
+
if (extensionList && extensionList.length > 0) {
|
|
108
|
+
files = files.filter((item) => {
|
|
109
|
+
const ext = getFileExt(item[key]);
|
|
110
|
+
if (extensionList.includes(ext)) {
|
|
111
|
+
return item;
|
|
112
|
+
}
|
|
113
|
+
invalidFiles.push(item);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return { files, invalidFiles };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @typedef {object} chooseFileParams
|
|
121
|
+
* @property {number} max 最大上传数量,不能超过 9
|
|
122
|
+
* @property {('image' | 'video' | 'file')[] | undefined} type 文件类型,可选 'image' 、 'video' 、 'file' 组成的数组
|
|
123
|
+
* @property {string[]} selectExtension 选择文件时限制文件类型,不带 ".",仅 type 包含 file 时有效
|
|
124
|
+
* @property {string[]} allowedExtension 选中文件后文件类型限制,不带 "."
|
|
125
|
+
* @property {boolean} fromChat 是否从聊天文件中选择图片和视频
|
|
126
|
+
*/
|
|
127
|
+
/** 选择文件
|
|
128
|
+
* @param {chooseFileParams} param0
|
|
129
|
+
* @return {Promise<chooseFileResult>} 选择的文件列表
|
|
130
|
+
*/
|
|
131
|
+
function chooseFile({ max = 1, type, selectExtension, allowedExtension, fromChat = false } = {}) {
|
|
132
|
+
/* #ifdef MP-WEIXIN */
|
|
133
|
+
|
|
134
|
+
if (type.includes(FILE_TYPE_FILE) || fromChat) {
|
|
135
|
+
type = typeArrToChooseMessageFileType(type);
|
|
136
|
+
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
uni.chooseMessageFile({
|
|
139
|
+
count: Math.max(1, Math.min(max, 100)),
|
|
140
|
+
type: type,
|
|
141
|
+
extension: selectExtension,
|
|
142
|
+
success(res) {
|
|
143
|
+
resolve(filterFiles(res.tempFiles, allowedExtension));
|
|
144
|
+
},
|
|
145
|
+
fail(err) {
|
|
146
|
+
reject(err);
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/* #endif */
|
|
152
|
+
type = typeArrToChooseMediaType(type);
|
|
153
|
+
|
|
154
|
+
return new Promise((resolve, reject) => {
|
|
155
|
+
uni.chooseMedia({
|
|
156
|
+
count: Math.max(1, Math.min(max, 20)),
|
|
157
|
+
mediaType: type,
|
|
158
|
+
sizeType: ['original', 'compressed'],
|
|
159
|
+
sourceType: ['album', 'camera'],
|
|
160
|
+
success(res) {
|
|
161
|
+
resolve(
|
|
162
|
+
filterFiles(
|
|
163
|
+
res.tempFiles.map((item) => ({
|
|
164
|
+
name:
|
|
165
|
+
(item.fileType === FILE_TYPE_IMAGE ? '图片' : '视频') +
|
|
166
|
+
'_' +
|
|
167
|
+
Date.now() +
|
|
168
|
+
randomInt(10000, 0, true) +
|
|
169
|
+
'.' +
|
|
170
|
+
getFileExt(item.tempFilePath),
|
|
171
|
+
size: item.size,
|
|
172
|
+
path: item.tempFilePath,
|
|
173
|
+
type: item.fileType,
|
|
174
|
+
})),
|
|
175
|
+
allowedExtension
|
|
176
|
+
)
|
|
177
|
+
);
|
|
178
|
+
},
|
|
179
|
+
fail(err) {
|
|
180
|
+
reject(err);
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 保存文件
|
|
188
|
+
* @param {string} path 文件临时路径
|
|
189
|
+
* @returns 保存后的路径
|
|
190
|
+
*/
|
|
191
|
+
function saveFile(path, name) {
|
|
192
|
+
return new Promise((resolve, reject) => {
|
|
193
|
+
/* #ifdef MP-WEIXIN */
|
|
194
|
+
uni.getFileSystemManager().saveFile({
|
|
195
|
+
tempFilePath: path,
|
|
196
|
+
filePath: `${wx.env.USER_DATA_PATH}/${name}`,
|
|
197
|
+
success: function (res) {
|
|
198
|
+
resolve(res.savedFilePath);
|
|
199
|
+
},
|
|
200
|
+
fail: reject,
|
|
201
|
+
});
|
|
202
|
+
/* #endif */
|
|
203
|
+
/* #ifndef MP-WEIXIN */
|
|
204
|
+
uni.saveFile({
|
|
205
|
+
tempFilePath: path,
|
|
206
|
+
success: function (res) {
|
|
207
|
+
resolve(res.savedFilePath);
|
|
208
|
+
},
|
|
209
|
+
fail: reject,
|
|
210
|
+
});
|
|
211
|
+
/* #endif */
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export {
|
|
216
|
+
FILE_TYPE_IMAGE,
|
|
217
|
+
FILE_TYPE_VIDEO,
|
|
218
|
+
FILE_TYPE_FILE,
|
|
219
|
+
typeStringToArr,
|
|
220
|
+
typeStringValidator,
|
|
221
|
+
chooseFile,
|
|
222
|
+
isImage,
|
|
223
|
+
isVideo,
|
|
224
|
+
saveFile,
|
|
225
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<view :class="componentClassNames" :style="{ '--columns': columns }">
|
|
3
|
+
<!-- 已上传文件列表 -->
|
|
4
|
+
<aspect-cpn v-for="(item, idx) in assetsList" :key="idx" :class="$style['Uploader--item']" :style="styleWidth">
|
|
5
|
+
<!-- TODO: 视频文件支持 -->
|
|
6
|
+
<!-- <video
|
|
7
|
+
v-if="item.fileType === 'video'"
|
|
8
|
+
:muted="true"
|
|
9
|
+
:src="item.fileUrl"
|
|
10
|
+
:controls="false"
|
|
11
|
+
:show-progress="false"
|
|
12
|
+
:show-fullscreen-btn="false"
|
|
13
|
+
:show-play-btn="false"
|
|
14
|
+
:show-center-play-btn="false"
|
|
15
|
+
:show-loading="false"
|
|
16
|
+
:enable-progress-gesture="false"
|
|
17
|
+
object-fit="fill"
|
|
18
|
+
:class="[$style['Uploader--image'], $style['Uploader--video']]"
|
|
19
|
+
></video> -->
|
|
20
|
+
<image-cpn
|
|
21
|
+
:src="item.tempFilePath || item.url"
|
|
22
|
+
:preview-list="previewList"
|
|
23
|
+
:class="$style['Uploader--image']"
|
|
24
|
+
:style="styleSize"
|
|
25
|
+
mode="aspectFill"
|
|
26
|
+
/>
|
|
27
|
+
<view
|
|
28
|
+
v-if="item.uploadStatus !== 'uploading'"
|
|
29
|
+
:class="$style['Uploader--icon-delete']"
|
|
30
|
+
@click.stop="deleteFile(idx)"
|
|
31
|
+
>
|
|
32
|
+
</view>
|
|
33
|
+
</aspect-cpn>
|
|
34
|
+
<!-- 上传按钮 -->
|
|
35
|
+
<aspect-cpn :class="$style['Uploader--item']" :style="styleWidth">
|
|
36
|
+
<button-cpn plain :class="$style['Uploader--button']" @click="selectFile">
|
|
37
|
+
<view style="line-height: 1.3em">
|
|
38
|
+
<view>上传</view>
|
|
39
|
+
<view>图片</view>
|
|
40
|
+
</view>
|
|
41
|
+
</button-cpn>
|
|
42
|
+
</aspect-cpn>
|
|
43
|
+
</view>
|
|
44
|
+
</template>
|
|
45
|
+
|
|
46
|
+
<script>
|
|
47
|
+
import componentName from '../../mixins/componentName';
|
|
48
|
+
import AspectCpn from '../aspect/aspect.vue';
|
|
49
|
+
import ButtonCpn from '../button/button.vue';
|
|
50
|
+
import ImageCpn from '../image/image.vue';
|
|
51
|
+
import UploadMixin from './mixin';
|
|
52
|
+
|
|
53
|
+
export default {
|
|
54
|
+
name: 'AppUploader',
|
|
55
|
+
options: {
|
|
56
|
+
virtualHost: true,
|
|
57
|
+
styleIsolation: 'shared',
|
|
58
|
+
},
|
|
59
|
+
components: { AspectCpn, ButtonCpn, ImageCpn },
|
|
60
|
+
mixins: [componentName, UploadMixin],
|
|
61
|
+
props: {
|
|
62
|
+
/** 显示宽度 */
|
|
63
|
+
columns: {
|
|
64
|
+
type: [String, Number],
|
|
65
|
+
default: 4,
|
|
66
|
+
},
|
|
67
|
+
/** 显示宽度 */
|
|
68
|
+
width: {
|
|
69
|
+
type: [String, Number],
|
|
70
|
+
default: '',
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
computed: {
|
|
74
|
+
styleWidth() {
|
|
75
|
+
if (!this.width) return '';
|
|
76
|
+
const width = typeof this.width === 'number' ? `${this.width}rpx` : this.width;
|
|
77
|
+
return `width: ${width};`;
|
|
78
|
+
},
|
|
79
|
+
/** 预览列表 */
|
|
80
|
+
previewList() {
|
|
81
|
+
return this.assetsList.map((item) => item.tempFilePath || item.url);
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
methods: {
|
|
85
|
+
selectFile() {
|
|
86
|
+
uni.chooseMedia({
|
|
87
|
+
count: this.restCount,
|
|
88
|
+
mediaType: ['image', 'video'],
|
|
89
|
+
sourceType: ['album', 'camera'],
|
|
90
|
+
success: (res) => {
|
|
91
|
+
this.validateFiles(res.tempFiles);
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
</script>
|
|
98
|
+
|
|
99
|
+
<style lang="scss" module>
|
|
100
|
+
.Uploader {
|
|
101
|
+
width: 100%;
|
|
102
|
+
display: flex;
|
|
103
|
+
flex-wrap: wrap;
|
|
104
|
+
gap: 12rpx;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.Uploader--item {
|
|
108
|
+
width: calc(100% / var(--columns) - 12rpx * (var(--columns) - 1) / var(--columns));
|
|
109
|
+
}
|
|
110
|
+
.Uploader--image {
|
|
111
|
+
border-radius: 16rpx;
|
|
112
|
+
}
|
|
113
|
+
.Uploader--button {
|
|
114
|
+
height: 100%;
|
|
115
|
+
border-color: #ccc;
|
|
116
|
+
border-radius: 16rpx;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.Uploader--icon-delete {
|
|
120
|
+
position: absolute;
|
|
121
|
+
top: 0;
|
|
122
|
+
right: 0;
|
|
123
|
+
z-index: 1;
|
|
124
|
+
transform-origin: 75%, 25%;
|
|
125
|
+
width: 36rpx;
|
|
126
|
+
height: 36rpx;
|
|
127
|
+
background: no-repeat top / 100% 45% linear-gradient(90deg, red 45%, transparent 45%, transparent 55%, red 55%),
|
|
128
|
+
no-repeat bottom / 100% 45% linear-gradient(90deg, red 45%, transparent 45%, transparent 55%, red 55%),
|
|
129
|
+
radial-gradient(transparent 50%, red 50%, red 100%);
|
|
130
|
+
transform: translate(25%, -25%) rotate(45deg);
|
|
131
|
+
border-radius: 50%;
|
|
132
|
+
}
|
|
133
|
+
</style>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
props: {
|
|
3
|
+
/** 文件列表 */
|
|
4
|
+
value: {
|
|
5
|
+
type: Array,
|
|
6
|
+
default: () => [],
|
|
7
|
+
},
|
|
8
|
+
max: {
|
|
9
|
+
type: Number,
|
|
10
|
+
default: 9,
|
|
11
|
+
},
|
|
12
|
+
maxSize: {
|
|
13
|
+
type: Number,
|
|
14
|
+
default: 1024 * 1024 * 10, // 10MB
|
|
15
|
+
},
|
|
16
|
+
uploadHandler: {
|
|
17
|
+
type: Function,
|
|
18
|
+
default: null,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
data() {
|
|
22
|
+
return {
|
|
23
|
+
innerValue: [],
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
computed: {
|
|
27
|
+
currentValue: {
|
|
28
|
+
get: function () {
|
|
29
|
+
return this.value === null ? this.innerValue : this.value;
|
|
30
|
+
},
|
|
31
|
+
set: function (value) {
|
|
32
|
+
this.$emit('input', value);
|
|
33
|
+
this.innerValue = value;
|
|
34
|
+
this.$emit('change', value);
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
restCount() {
|
|
38
|
+
if (this.max <= 0) return 9;
|
|
39
|
+
return this.max - this.currentValue.length;
|
|
40
|
+
},
|
|
41
|
+
assetsList() {
|
|
42
|
+
return this.currentValue.map((item) =>
|
|
43
|
+
typeof item === 'string' ? { url: item, uploadStatus: 'success' } : { uploadStatus: 'success', ...item }
|
|
44
|
+
);
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
methods: {
|
|
48
|
+
/** 校验待上传文件 */
|
|
49
|
+
async validateFiles(files) {
|
|
50
|
+
if (files.length === 0) return;
|
|
51
|
+
|
|
52
|
+
// 过滤大文件
|
|
53
|
+
const validFiles = files.filter((file) => file.size <= this.maxSize);
|
|
54
|
+
if (validFiles.length < files.length) {
|
|
55
|
+
uni.showToast({ icon: 'none', title: '已过滤超出大小限制的文件', duration: 2000 });
|
|
56
|
+
}
|
|
57
|
+
if (validFiles.length === 0) return;
|
|
58
|
+
|
|
59
|
+
// 校验数量
|
|
60
|
+
if (validFiles.length > this.restCount) {
|
|
61
|
+
uni.showToast({ icon: 'none', title: `最多只能上传${this.max}个文件`, duration: 2000 });
|
|
62
|
+
}
|
|
63
|
+
this.currentValue = this.currentValue.concat(
|
|
64
|
+
validFiles.slice(0, this.restCount).map((file) => ({ ...file, uploadStatus: 'ready' }))
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
/** 上传文件 */
|
|
68
|
+
upload(files) {
|
|
69
|
+
files.map(async (file) => {
|
|
70
|
+
try {
|
|
71
|
+
const res = await this.uploadHandler(file);
|
|
72
|
+
const index = this.currentValue.findIndex((item) => item.tempFilePath === file.tempFilePath);
|
|
73
|
+
if (index < 0) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const currentValue = [...this.currentValue];
|
|
77
|
+
currentValue.splice(index, 1, {
|
|
78
|
+
fileId: res.fileId,
|
|
79
|
+
fileName: res.fileName,
|
|
80
|
+
filePreviewUrl: res.filePreviewUrl,
|
|
81
|
+
fileUrl: res.fileUrl,
|
|
82
|
+
fileType: file.type,
|
|
83
|
+
});
|
|
84
|
+
this.currentValue = currentValue;
|
|
85
|
+
return this.currentValue[index];
|
|
86
|
+
} catch {
|
|
87
|
+
const index = this.currentValue.findIndex((item) => item.tempFilePath === file.tempFilePath);
|
|
88
|
+
if (index < 0) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const currentValue = [...this.currentValue];
|
|
92
|
+
currentValue.splice(index, 1, { ...file, uploadStatus: 'error' });
|
|
93
|
+
this.currentValue = currentValue;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
/** 删除文件 */
|
|
98
|
+
deleteFile(index) {
|
|
99
|
+
if (index >= 0 && index < this.currentValue.length) {
|
|
100
|
+
const currentValue = [...this.currentValue];
|
|
101
|
+
currentValue.splice(index, 1);
|
|
102
|
+
this.currentValue = currentValue;
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { download } from './download';
|
|
2
|
+
|
|
3
|
+
function canOpenDocument(filename) {
|
|
4
|
+
return ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'pdf'].includes(filename.split('.').pop());
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 新开页面打开文档,支持格式:doc, docx, xls, xlsx, ppt, pptx, pdf
|
|
9
|
+
* @param {string} url 文件 url
|
|
10
|
+
* @param {string} name 文件名
|
|
11
|
+
*/
|
|
12
|
+
export function openDocument(url, name) {
|
|
13
|
+
return download(url, name)
|
|
14
|
+
.catch(() => {
|
|
15
|
+
return Promise.reject('文件下载失败');
|
|
16
|
+
})
|
|
17
|
+
.then((res) => {
|
|
18
|
+
var path = res.filePath;
|
|
19
|
+
if (!canOpenDocument(path)) {
|
|
20
|
+
return Promise.reject('该文件类型不支持预览');
|
|
21
|
+
}
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
uni.openDocument({
|
|
24
|
+
filePath: path,
|
|
25
|
+
showMenu: true,
|
|
26
|
+
success: resolve,
|
|
27
|
+
fail: reject,
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 复制文本 */
|
|
34
|
+
export function copy(text) {
|
|
35
|
+
uni.setClipboardData({
|
|
36
|
+
data: text,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<button-cpn plain :class="$style['Uploader--card-uploader']" @click="$emit('select')">
|
|
3
|
+
<slot>上传</slot>
|
|
4
|
+
</button-cpn>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script>
|
|
8
|
+
import componentName from '../../mixins/componentName';
|
|
9
|
+
import ButtonCpn from '../button/button.vue';
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
name: 'AppUploadButton',
|
|
13
|
+
options: {
|
|
14
|
+
virtualHost: true,
|
|
15
|
+
styleIsolation: 'shared',
|
|
16
|
+
},
|
|
17
|
+
components: { ButtonCpn },
|
|
18
|
+
mixins: [componentName],
|
|
19
|
+
props: {
|
|
20
|
+
/** 文件列表 */
|
|
21
|
+
value: {
|
|
22
|
+
type: Array,
|
|
23
|
+
default: () => [],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
computed: {},
|
|
27
|
+
methods: {},
|
|
28
|
+
};
|
|
29
|
+
</script>
|