@ecolovo/uniapp-components 2.0.4 → 2.0.6
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/badge/badge.vue +22 -11
- package/components/badge/types.d.ts +6 -0
- package/components/badge/types.js +4 -0
- package/components/badge/types.ts +10 -0
- package/components/button/button.vue +77 -48
- package/components/button/types.d.ts +20 -0
- package/components/button/types.js +4 -0
- package/components/button/types.ts +24 -0
- package/components/checkbox/checkbox.scss +1 -1
- package/components/checkbox/checkbox.vue +2 -0
- package/components/checkbox/types.d.ts +10 -0
- package/components/checkbox/types.js +4 -0
- package/components/checkbox/types.ts +16 -0
- package/components/checkbox-group/checkbox-group.vue +2 -0
- package/components/checkbox-group/types.d.ts +10 -0
- package/components/checkbox-group/types.js +8 -0
- package/components/checkbox-group/types.ts +20 -0
- package/components/checkbox-group2/checkbox-group2.vue +2 -0
- package/components/data-list/data-list.vue +13 -7
- package/components/date-picker/date-picker.vue +2 -0
- package/components/date-picker/types.d.ts +12 -0
- package/components/date-picker/types.js +4 -0
- package/components/date-picker/types.ts +18 -0
- package/components/dialog/dialog-content.vue +61 -46
- package/components/dialog/dialog.vue +55 -29
- package/components/dialog/types.d.ts +20 -0
- package/components/dialog/types.js +4 -0
- package/components/dialog/types.ts +24 -0
- package/components/dropdown/dropdown.vue +2 -0
- package/components/dropdown/types.d.ts +10 -0
- package/components/dropdown/types.js +4 -0
- package/components/dropdown/types.ts +16 -0
- package/components/empty/empty.vue +37 -20
- package/components/empty/types.d.ts +12 -0
- package/components/empty/types.js +9 -0
- package/components/empty/types.ts +21 -0
- package/components/flex/flex.vue +2 -0
- package/components/flex/types.d.ts +12 -0
- package/components/flex/types.js +4 -0
- package/components/flex/types.ts +22 -0
- package/components/form/form.vue +2 -0
- package/components/form/types.d.ts +6 -0
- package/components/form/types.js +4 -0
- package/components/form/types.ts +10 -0
- package/components/form-item/form-item.scss +6 -0
- package/components/form-item/form-item.vue +9 -3
- package/components/form-item/types.d.ts +6 -0
- package/components/form-item/types.js +4 -0
- package/components/form-item/types.ts +12 -0
- package/components/image/image.vue +28 -21
- package/components/image/types.d.ts +18 -0
- package/components/image/types.js +4 -0
- package/components/image/types.ts +23 -0
- package/components/index.d.ts +1 -0
- package/components/index.js +1 -0
- package/components/index.ts +1 -0
- package/components/input/input.scss +9 -0
- package/components/input/input.vue +11 -4
- package/components/input/password-input.vue +3 -1
- package/components/input/types.d.ts +20 -0
- package/components/input/types.js +8 -0
- package/components/input/types.ts +31 -0
- package/components/list-page/list-page.vue +21 -24
- package/components/list-page/types.d.ts +6 -0
- package/components/list-page/types.js +4 -0
- package/components/list-page/types.ts +12 -0
- package/components/load-more/load-more.scss +1 -0
- package/components/load-more/load-more.vue +66 -6
- package/components/load-more/types.d.ts +6 -0
- package/components/load-more/types.js +4 -0
- package/components/load-more/types.ts +12 -0
- package/components/loading/loading.vue +2 -0
- package/components/loading/types.d.ts +6 -0
- package/components/loading/types.js +4 -0
- package/components/loading/types.ts +12 -0
- package/components/message-box/message-box.d.ts +14 -16
- package/components/message-box/message-box.ts +0 -2
- package/components/message-box/message-box.vue +18 -43
- package/components/message-box/types.d.ts +16 -0
- package/components/message-box/types.js +4 -0
- package/components/message-box/types.ts +22 -0
- package/components/number-dialog/number-dialog.vue +5 -3
- package/components/number-dialog/types.d.ts +18 -0
- package/components/number-dialog/types.js +4 -0
- package/components/number-dialog/types.ts +24 -0
- package/components/number-input/number-input.scss +1 -1
- package/components/number-input/number-input.vue +6 -4
- package/components/number-input/types.d.ts +20 -0
- package/components/number-input/types.js +4 -0
- package/components/number-input/types.ts +26 -0
- package/components/numpad/numpad.vue +2 -0
- package/components/numpad/types.d.ts +10 -0
- package/components/numpad/types.js +4 -0
- package/components/numpad/types.ts +14 -0
- package/components/page/page-body.scss +1 -1
- package/components/page/page-footer.vue +2 -3
- package/components/page/page-header.vue +19 -9
- package/components/page/page.vue +45 -14
- package/components/page/types.d.ts +16 -0
- package/components/page/types.js +8 -0
- package/components/page/types.ts +27 -0
- package/components/picker/picker-popup.vue +10 -6
- package/components/picker/picker.vue +8 -0
- package/components/picker/types.d.ts +14 -0
- package/components/picker/types.js +4 -0
- package/components/picker/types.ts +18 -0
- package/components/popover/popover-content.vue +10 -1
- package/components/popover/popover-mask.vue +5 -5
- package/components/popover/popover.vue +34 -29
- package/components/popover/types.d.ts +10 -0
- package/components/popover/types.js +4 -0
- package/components/popover/types.ts +16 -0
- package/components/popup/popup-content.vue +61 -43
- package/components/popup/popup.vue +34 -28
- package/components/popup/types.d.ts +20 -0
- package/components/popup/types.js +4 -0
- package/components/popup/types.ts +24 -0
- package/components/price/price.scss +7 -3
- package/components/price/price.vue +6 -0
- package/components/price/types.d.ts +10 -0
- package/components/price/types.js +4 -0
- package/components/price/types.ts +14 -0
- package/components/search/search.vue +4 -2
- package/components/search/types.d.ts +14 -0
- package/components/search/types.js +4 -0
- package/components/search/types.ts +18 -0
- package/components/searching/searching.vue +2 -0
- package/components/searching/types.d.ts +8 -0
- package/components/searching/types.js +4 -0
- package/components/searching/types.ts +14 -0
- package/components/switch/switch.vue +5 -8
- package/components/switch/types.d.ts +8 -0
- package/components/switch/types.js +4 -0
- package/components/switch/types.ts +12 -0
- package/components/tab/tab.vue +2 -0
- package/components/tab/types.d.ts +8 -0
- package/components/tab/types.js +4 -0
- package/components/tab/types.ts +12 -0
- package/components/textarea/textarea.scss +10 -0
- package/components/textarea/textarea.vue +17 -9
- package/components/textarea/types.d.ts +12 -0
- package/components/textarea/types.js +4 -0
- package/components/textarea/types.ts +18 -0
- package/components/upload/composable.d.ts +24 -0
- package/components/upload/composable.js +173 -0
- package/components/upload/composable.ts +228 -0
- package/components/upload/media-upload.scss +12 -0
- package/components/upload/media-upload.vue +250 -0
- package/components/upload/types.d.ts +29 -0
- package/components/upload/types.js +4 -0
- package/components/upload/types.ts +38 -0
- package/composable/component-name.d.ts +8 -0
- package/composable/component-name.js +11 -4
- package/composable/component-name.ts +8 -0
- package/composable/dialog.d.ts +40 -0
- package/composable/dialog.js +96 -0
- package/composable/dialog.ts +113 -0
- package/composable/index.d.ts +5 -0
- package/composable/index.js +5 -0
- package/composable/index.ts +5 -0
- package/composable/message-box.d.ts +45 -28
- package/composable/message-box.js +68 -53
- package/composable/message-box.ts +101 -36
- package/composable/model.d.ts +8 -0
- package/composable/model.js +23 -0
- package/composable/model.ts +27 -0
- package/composable/page.d.ts +72 -0
- package/composable/page.js +25 -0
- package/composable/page.ts +34 -0
- package/configs/index.d.ts +24 -19
- package/configs/index.js +38 -35
- package/configs/index.ts +19 -0
- package/easyCom.d.ts +6 -6
- package/easyCom.js +19 -31
- package/easyCom.ts +4 -4
- package/globals.d.ts +93 -33
- package/globals.js +1 -0
- package/globals.ts +118 -0
- package/index.d.ts +3 -2
- package/index.js +2 -2
- package/index.ts +3 -2
- package/init.d.ts +11 -6
- package/init.js +84 -14
- package/init.ts +86 -1
- package/package.json +15 -12
- package/styles/var/component.scss +1 -0
- package/utils/date.d.ts +14 -19
- package/utils/date.js +77 -87
- package/utils/freezeDeep.js +7 -7
- package/utils/price.d.ts +15 -18
- package/utils/price.js +36 -37
- package/utils/upload.d.ts +1 -0
- package/utils/upload.js +8 -0
- package/utils/upload.ts +12 -0
- package/utils/windowInfo.d.ts +1 -9
- package/utils/windowInfo.js +10 -17
- package/utils/round.d.ts +0 -2
- package/utils/round.js +0 -13
- package/utils/round.ts +0 -14
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type CommonProps } from './types';
|
|
2
|
+
declare const useUpload: <Emit extends ((event: "update:modelValue", value: UniApp.UploadMediaFile[]) => void) & ((event: "change", value: UniApp.UploadMediaFile[]) => void)>(props: CommonProps, emit: Emit) => {
|
|
3
|
+
currentValue: import("vue").WritableComputedRef<UniNamespace.UploadMediaFile[], UniNamespace.UploadMediaFile[]>;
|
|
4
|
+
isReadonly: import("vue").ComputedRef<boolean>;
|
|
5
|
+
isUploading: import("vue").ComputedRef<boolean>;
|
|
6
|
+
restCount: import("vue").ComputedRef<number>;
|
|
7
|
+
previewList: import("vue").ComputedRef<string[]>;
|
|
8
|
+
validateFiles: (files: UniApp.UploadMediaFile[]) => {
|
|
9
|
+
uploadStatus: "ready";
|
|
10
|
+
duration?: number | undefined;
|
|
11
|
+
fileType?: "video" | "image" | undefined;
|
|
12
|
+
height?: number | undefined;
|
|
13
|
+
size?: number | undefined;
|
|
14
|
+
tempFilePath?: string | undefined;
|
|
15
|
+
thumbTempFilePath?: string | undefined;
|
|
16
|
+
width?: number | undefined;
|
|
17
|
+
name?: string;
|
|
18
|
+
type?: string;
|
|
19
|
+
url?: string;
|
|
20
|
+
}[] | undefined;
|
|
21
|
+
upload: () => Promise<(UniNamespace.UploadMediaFile | null)[]>;
|
|
22
|
+
deleteFile: (index: number) => void;
|
|
23
|
+
};
|
|
24
|
+
export { useUpload };
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { computed, inject, ref, watch } from 'vue';
|
|
2
|
+
import { floor } from '@ecolovo/utils';
|
|
3
|
+
import { toUploadMediaFile } from '../../utils/upload';
|
|
4
|
+
const ONE_KB = 1024;
|
|
5
|
+
const ONE_MB = ONE_KB * 1024;
|
|
6
|
+
const ONE_GB = ONE_MB * 1024;
|
|
7
|
+
/**
|
|
8
|
+
* 格式化文件尺寸
|
|
9
|
+
*
|
|
10
|
+
* @param {number} size 文件大小 B
|
|
11
|
+
*/
|
|
12
|
+
function fileSize(size) {
|
|
13
|
+
if (size < 0) {
|
|
14
|
+
return { size: -1, unit: 'B', formattedSize: '' };
|
|
15
|
+
}
|
|
16
|
+
const rtnObj = {};
|
|
17
|
+
if (size < 100) {
|
|
18
|
+
rtnObj.size = size;
|
|
19
|
+
rtnObj.unit = 'B';
|
|
20
|
+
}
|
|
21
|
+
else if (size < ONE_KB) {
|
|
22
|
+
rtnObj.size = floor(size / ONE_KB, -2);
|
|
23
|
+
rtnObj.unit = 'KB';
|
|
24
|
+
}
|
|
25
|
+
else if (size < ONE_MB) {
|
|
26
|
+
rtnObj.size = floor(size / ONE_KB);
|
|
27
|
+
rtnObj.unit = 'KB';
|
|
28
|
+
}
|
|
29
|
+
else if (size < ONE_GB) {
|
|
30
|
+
rtnObj.size = floor(size / ONE_MB, -1);
|
|
31
|
+
rtnObj.unit = 'MB';
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
rtnObj.size = floor(size / ONE_GB, -1);
|
|
35
|
+
rtnObj.unit = 'GB';
|
|
36
|
+
}
|
|
37
|
+
rtnObj.formattedSize = `${rtnObj.size} ${rtnObj.unit}`;
|
|
38
|
+
return rtnObj;
|
|
39
|
+
}
|
|
40
|
+
const useUpload = (props, emit) => {
|
|
41
|
+
/** 表单是否只读 */
|
|
42
|
+
const isFormReadonly = inject('ecolovo/form/readonly', computed(() => false));
|
|
43
|
+
const isReadonly = computed(() => {
|
|
44
|
+
return props.readonly || isFormReadonly.value;
|
|
45
|
+
});
|
|
46
|
+
/** 剩余可上传文件数 */
|
|
47
|
+
const restCount = computed(() => {
|
|
48
|
+
const max = props.max ?? 9;
|
|
49
|
+
if (max <= 0)
|
|
50
|
+
return 9;
|
|
51
|
+
return max - currentValue.value.length;
|
|
52
|
+
});
|
|
53
|
+
const innerValue = ref(props.modelValue || []);
|
|
54
|
+
watch(() => props.modelValue, (newValue, oldValue) => {
|
|
55
|
+
if (newValue || (oldValue && !newValue)) {
|
|
56
|
+
innerValue.value = newValue || [];
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
const currentValue = computed({
|
|
60
|
+
get: function () {
|
|
61
|
+
return toUploadMediaFile(innerValue.value);
|
|
62
|
+
},
|
|
63
|
+
set: function (val) {
|
|
64
|
+
innerValue.value = val;
|
|
65
|
+
emit('update:modelValue', val);
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
const previewList = computed(() => {
|
|
69
|
+
return currentValue.value.map((item) => item.tempFilePath || item.url || '');
|
|
70
|
+
});
|
|
71
|
+
/** 当前是否有文件还在上传 */
|
|
72
|
+
const isUploading = computed(() => {
|
|
73
|
+
return currentValue.value.some((item) => item.uploadStatus === 'uploading');
|
|
74
|
+
});
|
|
75
|
+
/** 校验待上传文件 */
|
|
76
|
+
const validateFiles = (files) => {
|
|
77
|
+
if (files.length === 0)
|
|
78
|
+
return;
|
|
79
|
+
const maxSize = props.maxSize ?? 0;
|
|
80
|
+
// 过滤大文件
|
|
81
|
+
const selectedFiles = files.filter((file) => (file.size || 0) <= maxSize);
|
|
82
|
+
if (selectedFiles.length < files.length) {
|
|
83
|
+
uni.showToast({
|
|
84
|
+
icon: 'none',
|
|
85
|
+
title: `文件大小不能超过${fileSize(maxSize).formattedSize}`,
|
|
86
|
+
duration: 2000,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (selectedFiles.length === 0)
|
|
90
|
+
return;
|
|
91
|
+
// 校验数量
|
|
92
|
+
if (selectedFiles.length > restCount.value) {
|
|
93
|
+
uni.showToast({
|
|
94
|
+
icon: 'none',
|
|
95
|
+
title: `最多只能上传${props.max}个文件`,
|
|
96
|
+
duration: 2000,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const validFiles = selectedFiles
|
|
100
|
+
.slice(0, restCount.value)
|
|
101
|
+
.map((file) => ({ ...file, uploadStatus: 'ready' }));
|
|
102
|
+
currentValue.value = currentValue.value.concat(validFiles);
|
|
103
|
+
return validFiles;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* 上传文件
|
|
107
|
+
*/
|
|
108
|
+
const upload = () => {
|
|
109
|
+
return Promise.all(currentValue.value.map(async (file, _index, arr) => {
|
|
110
|
+
if (file.uploadStatus !== 'ready')
|
|
111
|
+
return file;
|
|
112
|
+
try {
|
|
113
|
+
file.uploadStatus = 'uploading';
|
|
114
|
+
const res = await props.uploadHandler?.(file);
|
|
115
|
+
const index = arr.findIndex((item) => item.tempFilePath === file.tempFilePath);
|
|
116
|
+
if (index < 0) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const tempCurrentValue = [...currentValue.value];
|
|
120
|
+
tempCurrentValue.splice(index, 1, res && res.url
|
|
121
|
+
? {
|
|
122
|
+
...file,
|
|
123
|
+
name: res.name,
|
|
124
|
+
url: res.url,
|
|
125
|
+
type: file.type,
|
|
126
|
+
uploadStatus: 'success',
|
|
127
|
+
}
|
|
128
|
+
: {
|
|
129
|
+
...file,
|
|
130
|
+
uploadStatus: 'error',
|
|
131
|
+
});
|
|
132
|
+
currentValue.value = tempCurrentValue;
|
|
133
|
+
return currentValue.value[index];
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
const index = currentValue.value.findIndex((item) => item.tempFilePath === file.tempFilePath);
|
|
137
|
+
if (index < 0) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
const tempCurrentValue = [...currentValue.value];
|
|
141
|
+
tempCurrentValue.splice(index, 1, {
|
|
142
|
+
...file,
|
|
143
|
+
uploadStatus: 'error',
|
|
144
|
+
});
|
|
145
|
+
currentValue.value = tempCurrentValue;
|
|
146
|
+
return currentValue.value[index];
|
|
147
|
+
}
|
|
148
|
+
}));
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* 删除文件
|
|
152
|
+
*
|
|
153
|
+
* @param index 文件索引
|
|
154
|
+
*/
|
|
155
|
+
const deleteFile = (index) => {
|
|
156
|
+
if (index >= 0 && index < currentValue.value.length) {
|
|
157
|
+
const tempCurrentValue = [...currentValue.value];
|
|
158
|
+
tempCurrentValue.splice(index, 1);
|
|
159
|
+
currentValue.value = tempCurrentValue;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
return {
|
|
163
|
+
currentValue,
|
|
164
|
+
isReadonly,
|
|
165
|
+
isUploading,
|
|
166
|
+
restCount,
|
|
167
|
+
previewList,
|
|
168
|
+
validateFiles,
|
|
169
|
+
upload,
|
|
170
|
+
deleteFile,
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
export { useUpload };
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { computed, inject, ref, watch } from 'vue';
|
|
2
|
+
import { floor } from '@ecolovo/utils';
|
|
3
|
+
import { type CommonProps } from './types';
|
|
4
|
+
import { toUploadMediaFile } from '../../utils/upload';
|
|
5
|
+
|
|
6
|
+
const ONE_KB = 1024;
|
|
7
|
+
const ONE_MB = ONE_KB * 1024;
|
|
8
|
+
const ONE_GB = ONE_MB * 1024;
|
|
9
|
+
|
|
10
|
+
interface FileSizeInfo {
|
|
11
|
+
size: number;
|
|
12
|
+
unit: 'B' | 'KB' | 'MB' | 'GB';
|
|
13
|
+
formattedSize: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 格式化文件尺寸
|
|
18
|
+
*
|
|
19
|
+
* @param {number} size 文件大小 B
|
|
20
|
+
*/
|
|
21
|
+
function fileSize(size: number): FileSizeInfo {
|
|
22
|
+
if (size < 0) {
|
|
23
|
+
return { size: -1, unit: 'B', formattedSize: '' };
|
|
24
|
+
}
|
|
25
|
+
const rtnObj = {} as FileSizeInfo;
|
|
26
|
+
if (size < 100) {
|
|
27
|
+
rtnObj.size = size;
|
|
28
|
+
rtnObj.unit = 'B';
|
|
29
|
+
} else if (size < ONE_KB) {
|
|
30
|
+
rtnObj.size = floor(size / ONE_KB, -2);
|
|
31
|
+
rtnObj.unit = 'KB';
|
|
32
|
+
} else if (size < ONE_MB) {
|
|
33
|
+
rtnObj.size = floor(size / ONE_KB);
|
|
34
|
+
rtnObj.unit = 'KB';
|
|
35
|
+
} else if (size < ONE_GB) {
|
|
36
|
+
rtnObj.size = floor(size / ONE_MB, -1);
|
|
37
|
+
rtnObj.unit = 'MB';
|
|
38
|
+
} else {
|
|
39
|
+
rtnObj.size = floor(size / ONE_GB, -1);
|
|
40
|
+
rtnObj.unit = 'GB';
|
|
41
|
+
}
|
|
42
|
+
rtnObj.formattedSize = `${rtnObj.size} ${rtnObj.unit}`;
|
|
43
|
+
return rtnObj;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const useUpload = <
|
|
47
|
+
Emit extends ((
|
|
48
|
+
event: 'update:modelValue',
|
|
49
|
+
value: UniApp.UploadMediaFile[]
|
|
50
|
+
) => void) &
|
|
51
|
+
((event: 'change', value: UniApp.UploadMediaFile[]) => void)
|
|
52
|
+
>(
|
|
53
|
+
props: CommonProps,
|
|
54
|
+
emit: Emit
|
|
55
|
+
) => {
|
|
56
|
+
/** 表单是否只读 */
|
|
57
|
+
const isFormReadonly = inject(
|
|
58
|
+
'ecolovo/form/readonly',
|
|
59
|
+
computed(() => false)
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const isReadonly = computed(() => {
|
|
63
|
+
return props.readonly || isFormReadonly.value;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/** 剩余可上传文件数 */
|
|
67
|
+
const restCount = computed(() => {
|
|
68
|
+
const max = props.max ?? 9;
|
|
69
|
+
if (max <= 0) return 9;
|
|
70
|
+
return max - currentValue.value.length;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const innerValue = ref<UniApp.UploadMediaFile[] | string[]>(
|
|
74
|
+
props.modelValue || []
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
watch(
|
|
78
|
+
() => props.modelValue,
|
|
79
|
+
(newValue, oldValue) => {
|
|
80
|
+
if (newValue || (oldValue && !newValue)) {
|
|
81
|
+
innerValue.value = newValue || [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const currentValue = computed<UniApp.UploadMediaFile[]>({
|
|
87
|
+
get: function () {
|
|
88
|
+
return toUploadMediaFile(innerValue.value);
|
|
89
|
+
},
|
|
90
|
+
set: function (val) {
|
|
91
|
+
innerValue.value = val;
|
|
92
|
+
emit('update:modelValue', val);
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const previewList = computed(() => {
|
|
97
|
+
return currentValue.value.map(
|
|
98
|
+
(item) => item.tempFilePath || item.url || ''
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
/** 当前是否有文件还在上传 */
|
|
103
|
+
const isUploading = computed(() => {
|
|
104
|
+
return currentValue.value.some(
|
|
105
|
+
(item) => item.uploadStatus === 'uploading'
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/** 校验待上传文件 */
|
|
110
|
+
const validateFiles = (files: UniApp.UploadMediaFile[]) => {
|
|
111
|
+
if (files.length === 0) return;
|
|
112
|
+
|
|
113
|
+
const maxSize = props.maxSize ?? 0;
|
|
114
|
+
|
|
115
|
+
// 过滤大文件
|
|
116
|
+
const selectedFiles = files.filter(
|
|
117
|
+
(file) => (file.size || 0) <= maxSize
|
|
118
|
+
);
|
|
119
|
+
if (selectedFiles.length < files.length) {
|
|
120
|
+
uni.showToast({
|
|
121
|
+
icon: 'none',
|
|
122
|
+
title: `文件大小不能超过${fileSize(maxSize).formattedSize}`,
|
|
123
|
+
duration: 2000,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (selectedFiles.length === 0) return;
|
|
127
|
+
|
|
128
|
+
// 校验数量
|
|
129
|
+
if (selectedFiles.length > restCount.value) {
|
|
130
|
+
uni.showToast({
|
|
131
|
+
icon: 'none',
|
|
132
|
+
title: `最多只能上传${props.max}个文件`,
|
|
133
|
+
duration: 2000,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const validFiles = selectedFiles
|
|
138
|
+
.slice(0, restCount.value)
|
|
139
|
+
.map((file) => ({ ...file, uploadStatus: 'ready' as const }));
|
|
140
|
+
currentValue.value = currentValue.value.concat(validFiles);
|
|
141
|
+
|
|
142
|
+
return validFiles;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 上传文件
|
|
147
|
+
*/
|
|
148
|
+
const upload = () => {
|
|
149
|
+
return Promise.all(
|
|
150
|
+
currentValue.value.map(async (file, _index, arr) => {
|
|
151
|
+
if (file.uploadStatus !== 'ready') return file;
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
file.uploadStatus = 'uploading';
|
|
155
|
+
const res = await props.uploadHandler?.(file);
|
|
156
|
+
|
|
157
|
+
const index = arr.findIndex(
|
|
158
|
+
(item) => item.tempFilePath === file.tempFilePath
|
|
159
|
+
);
|
|
160
|
+
if (index < 0) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const tempCurrentValue = [...currentValue.value];
|
|
165
|
+
tempCurrentValue.splice(
|
|
166
|
+
index,
|
|
167
|
+
1,
|
|
168
|
+
res && res.url
|
|
169
|
+
? {
|
|
170
|
+
...file,
|
|
171
|
+
name: res.name,
|
|
172
|
+
url: res.url,
|
|
173
|
+
type: file.type,
|
|
174
|
+
uploadStatus: 'success' as const,
|
|
175
|
+
}
|
|
176
|
+
: {
|
|
177
|
+
...file,
|
|
178
|
+
uploadStatus: 'error' as const,
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
currentValue.value = tempCurrentValue;
|
|
182
|
+
return currentValue.value[index];
|
|
183
|
+
} catch {
|
|
184
|
+
const index = currentValue.value.findIndex(
|
|
185
|
+
(item) => item.tempFilePath === file.tempFilePath
|
|
186
|
+
);
|
|
187
|
+
if (index < 0) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const tempCurrentValue = [...currentValue.value];
|
|
192
|
+
tempCurrentValue.splice(index, 1, {
|
|
193
|
+
...file,
|
|
194
|
+
uploadStatus: 'error',
|
|
195
|
+
});
|
|
196
|
+
currentValue.value = tempCurrentValue;
|
|
197
|
+
return currentValue.value[index];
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 删除文件
|
|
205
|
+
*
|
|
206
|
+
* @param index 文件索引
|
|
207
|
+
*/
|
|
208
|
+
const deleteFile = (index: number) => {
|
|
209
|
+
if (index >= 0 && index < currentValue.value.length) {
|
|
210
|
+
const tempCurrentValue = [...currentValue.value];
|
|
211
|
+
tempCurrentValue.splice(index, 1);
|
|
212
|
+
currentValue.value = tempCurrentValue;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
currentValue,
|
|
218
|
+
isReadonly,
|
|
219
|
+
isUploading,
|
|
220
|
+
restCount,
|
|
221
|
+
previewList,
|
|
222
|
+
validateFiles,
|
|
223
|
+
upload,
|
|
224
|
+
deleteFile,
|
|
225
|
+
};
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
export { useUpload };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
@use "sass:map";
|
|
2
|
+
@use "../../styles/utils" as *;
|
|
3
|
+
|
|
4
|
+
// 可配置的变量
|
|
5
|
+
$media-upload: () !default;
|
|
6
|
+
$media-upload-default: (
|
|
7
|
+
gap: 12rpx,
|
|
8
|
+
delete-icon-size: 36rpx,
|
|
9
|
+
delete-icon-color: red,
|
|
10
|
+
border-radius: 16rpx,
|
|
11
|
+
);
|
|
12
|
+
$media-upload: map.merge($media-upload-default, $media-upload);
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<view :class="[...classNames, $style['Uploader']]" :style="inlineStyle">
|
|
3
|
+
<slot name="file-list" :delete-file="deleteFile">
|
|
4
|
+
<!-- 已上传文件列表 -->
|
|
5
|
+
<aspect-cpn
|
|
6
|
+
v-for="(item, index) in currentValue"
|
|
7
|
+
:key="index"
|
|
8
|
+
:class="$style['Uploader--item']"
|
|
9
|
+
:style="styleWidth"
|
|
10
|
+
>
|
|
11
|
+
<!-- TODO: 视频文件支持 -->
|
|
12
|
+
<!-- <video
|
|
13
|
+
v-if="item.fileType === 'video'"
|
|
14
|
+
:muted="true"
|
|
15
|
+
:src="item.fileUrl"
|
|
16
|
+
:controls="false"
|
|
17
|
+
:show-progress="false"
|
|
18
|
+
:show-fullscreen-btn="false"
|
|
19
|
+
:show-play-btn="false"
|
|
20
|
+
:show-center-play-btn="false"
|
|
21
|
+
:show-loading="false"
|
|
22
|
+
:enable-progress-gesture="false"
|
|
23
|
+
object-fit="fill"
|
|
24
|
+
:class="[$style['Uploader--image'], $style['Uploader--video']]"
|
|
25
|
+
></video> -->
|
|
26
|
+
<slot :image="item" :index="index" :list="previewList">
|
|
27
|
+
<image-cpn
|
|
28
|
+
:src="item.tempFilePath || item.url"
|
|
29
|
+
:preview="previewList"
|
|
30
|
+
:class="$style['Uploader--image']"
|
|
31
|
+
mode="aspectFill"
|
|
32
|
+
/>
|
|
33
|
+
</slot>
|
|
34
|
+
<loading-cpn
|
|
35
|
+
v-if="!isReadonly && item.uploadStatus === 'uploading'"
|
|
36
|
+
:class="$style['Uploader--upload-loading']"
|
|
37
|
+
/>
|
|
38
|
+
<view
|
|
39
|
+
v-if="!isReadonly && item.uploadStatus === 'error'"
|
|
40
|
+
:class="$style['Uploader--upload-error']"
|
|
41
|
+
>
|
|
42
|
+
上传失败
|
|
43
|
+
</view>
|
|
44
|
+
<template
|
|
45
|
+
v-if="!isReadonly && item.uploadStatus !== 'uploading'"
|
|
46
|
+
>
|
|
47
|
+
<slot
|
|
48
|
+
name="delete-icon"
|
|
49
|
+
:index="index"
|
|
50
|
+
:delete-file="deleteFile"
|
|
51
|
+
>
|
|
52
|
+
<icon-cpn
|
|
53
|
+
icon="close-filled"
|
|
54
|
+
:class="$style['Uploader--delete-icon']"
|
|
55
|
+
@click.stop="deleteFile(index)"
|
|
56
|
+
>
|
|
57
|
+
</icon-cpn>
|
|
58
|
+
</slot>
|
|
59
|
+
</template>
|
|
60
|
+
</aspect-cpn>
|
|
61
|
+
</slot>
|
|
62
|
+
<!-- 上传按钮 -->
|
|
63
|
+
<aspect-cpn
|
|
64
|
+
v-if="!isReadonly && restCount > 0"
|
|
65
|
+
:class="$style['Uploader--item']"
|
|
66
|
+
:style="styleWidth"
|
|
67
|
+
@click="selectFile"
|
|
68
|
+
>
|
|
69
|
+
<slot name="adder">
|
|
70
|
+
<view :class="$style['Uploader--button']">
|
|
71
|
+
<view>上传</view>
|
|
72
|
+
<view>图片</view>
|
|
73
|
+
</view>
|
|
74
|
+
</slot>
|
|
75
|
+
</aspect-cpn>
|
|
76
|
+
</view>
|
|
77
|
+
</template>
|
|
78
|
+
|
|
79
|
+
<script lang="ts" setup>
|
|
80
|
+
import { computed } from 'vue';
|
|
81
|
+
import useClassNames from '../../composable/component-name';
|
|
82
|
+
import AspectCpn from '../aspect/aspect.vue';
|
|
83
|
+
import IconCpn from '../icon/icon.vue';
|
|
84
|
+
import ImageCpn from '../image/image.vue';
|
|
85
|
+
import LoadingCpn from '../loading/animations/loading-animation4.vue';
|
|
86
|
+
import { useUpload } from './composable.js';
|
|
87
|
+
import { type CommonProps, mediaUploadProps } from './types.js';
|
|
88
|
+
|
|
89
|
+
defineOptions({
|
|
90
|
+
options: {
|
|
91
|
+
virtualHost: true,
|
|
92
|
+
styleIsolation: 'shared',
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const classNames = useClassNames('media-upload');
|
|
97
|
+
|
|
98
|
+
type _Props = CommonProps & {
|
|
99
|
+
/** 媒体类型,可选值:'image' | 'video' | 'mix' */
|
|
100
|
+
type?: 'image' | 'video' | 'mix';
|
|
101
|
+
/** 媒体来源,可选值:'album' | 'camera' | 'album&camera' */
|
|
102
|
+
source?: 'album' | 'camera' | 'album&camera';
|
|
103
|
+
/** 列数 */
|
|
104
|
+
columns?: string | number;
|
|
105
|
+
/** 显示宽度 */
|
|
106
|
+
width?: string | number;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const props = withDefaults(defineProps<_Props>(), {
|
|
110
|
+
modelValue: () => [],
|
|
111
|
+
readonly: false,
|
|
112
|
+
max: 9,
|
|
113
|
+
maxSize: 1024 * 1024 * 10, // 10MB
|
|
114
|
+
autoUpload: true,
|
|
115
|
+
uploadHandler: undefined,
|
|
116
|
+
type: 'image',
|
|
117
|
+
source: 'album',
|
|
118
|
+
columns: 4,
|
|
119
|
+
width: undefined,
|
|
120
|
+
...mediaUploadProps,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const emit = defineEmits<{
|
|
124
|
+
(e: 'update:modelValue', value: UniApp.UploadMediaFile[]): void;
|
|
125
|
+
(e: 'change', value: UniApp.UploadMediaFile[]): void;
|
|
126
|
+
}>();
|
|
127
|
+
|
|
128
|
+
const {
|
|
129
|
+
currentValue,
|
|
130
|
+
isReadonly,
|
|
131
|
+
restCount,
|
|
132
|
+
previewList,
|
|
133
|
+
deleteFile,
|
|
134
|
+
validateFiles,
|
|
135
|
+
upload,
|
|
136
|
+
} = useUpload(props, emit);
|
|
137
|
+
|
|
138
|
+
const styleWidth = computed(() => {
|
|
139
|
+
if (!props.width) return '';
|
|
140
|
+
const width =
|
|
141
|
+
typeof props.width === 'number' ? `${props.width}rpx` : props.width;
|
|
142
|
+
return `width: ${width};`;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const inlineStyle = computed(() => `--columns: ${props.columns}`);
|
|
146
|
+
|
|
147
|
+
const selectFile = () => {
|
|
148
|
+
uni.chooseMedia({
|
|
149
|
+
count: restCount.value,
|
|
150
|
+
mediaType: [props.type],
|
|
151
|
+
sourceType:
|
|
152
|
+
props.source === 'album&camera'
|
|
153
|
+
? ['album', 'camera']
|
|
154
|
+
: [props.source],
|
|
155
|
+
success: (res) => {
|
|
156
|
+
const files = validateFiles(res.tempFiles);
|
|
157
|
+
if (props.autoUpload && files) {
|
|
158
|
+
upload();
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
</script>
|
|
164
|
+
|
|
165
|
+
<style lang="scss" module>
|
|
166
|
+
@use '../../styles/utils' as *;
|
|
167
|
+
@use './media-upload' as *;
|
|
168
|
+
|
|
169
|
+
// 定义外部变量
|
|
170
|
+
%external-vars {
|
|
171
|
+
@include UTILS__DEFINE_COMPONENT_VARS('media-upload', $media-upload);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
$gap: UTILS__LOCAL_VAR(gap);
|
|
175
|
+
|
|
176
|
+
.Uploader {
|
|
177
|
+
@extend %external-vars;
|
|
178
|
+
width: 100%;
|
|
179
|
+
display: flex;
|
|
180
|
+
flex-wrap: wrap;
|
|
181
|
+
gap: $gap;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.Uploader--item {
|
|
185
|
+
position: relative;
|
|
186
|
+
width: calc((100% + #{$gap}) / var(--columns) - #{$gap});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.Uploader--image {
|
|
190
|
+
width: 100%;
|
|
191
|
+
height: 100%;
|
|
192
|
+
border-radius: UTILS__LOCAL_VAR(border-radius);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.Uploader--upload-loading {
|
|
196
|
+
pointer-events: none;
|
|
197
|
+
position: absolute;
|
|
198
|
+
top: 50%;
|
|
199
|
+
left: 50%;
|
|
200
|
+
font-size: 2em;
|
|
201
|
+
color: #fff;
|
|
202
|
+
transform: translate(-50%, -50%);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.Uploader--upload-error {
|
|
206
|
+
pointer-events: none;
|
|
207
|
+
position: absolute;
|
|
208
|
+
top: 50%;
|
|
209
|
+
left: 50%;
|
|
210
|
+
width: 2em;
|
|
211
|
+
height: 2em;
|
|
212
|
+
box-sizing: content-box;
|
|
213
|
+
transform: translate(-50%, -50%);
|
|
214
|
+
padding: 1.25em;
|
|
215
|
+
border-radius: 50%;
|
|
216
|
+
background-color: #00000080;
|
|
217
|
+
color: #fff;
|
|
218
|
+
font-size: 22rpx;
|
|
219
|
+
display: flex;
|
|
220
|
+
justify-content: center;
|
|
221
|
+
align-items: center;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.Uploader--button {
|
|
225
|
+
box-sizing: border-box;
|
|
226
|
+
width: 100%;
|
|
227
|
+
height: 100%;
|
|
228
|
+
border-radius: UTILS__LOCAL_VAR(border-radius);
|
|
229
|
+
border: 1rpx solid UTILS__GLOBAL_VAR(theme, primary);
|
|
230
|
+
color: UTILS__GLOBAL_VAR(theme, primary);
|
|
231
|
+
display: flex;
|
|
232
|
+
flex-direction: column;
|
|
233
|
+
justify-content: center;
|
|
234
|
+
align-items: center;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.Uploader--delete-icon {
|
|
238
|
+
position: absolute;
|
|
239
|
+
top: 0;
|
|
240
|
+
right: 0;
|
|
241
|
+
transform: translate(25%, -25%);
|
|
242
|
+
z-index: 1;
|
|
243
|
+
color: UTILS__LOCAL_VAR(delete-icon-color);
|
|
244
|
+
@include UTILS__SET_VAR(
|
|
245
|
+
icon,
|
|
246
|
+
font-size,
|
|
247
|
+
UTILS__LOCAL_VAR(delete-icon-size)
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
</style>
|