@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.
Files changed (199) hide show
  1. package/components/badge/badge.vue +22 -11
  2. package/components/badge/types.d.ts +6 -0
  3. package/components/badge/types.js +4 -0
  4. package/components/badge/types.ts +10 -0
  5. package/components/button/button.vue +77 -48
  6. package/components/button/types.d.ts +20 -0
  7. package/components/button/types.js +4 -0
  8. package/components/button/types.ts +24 -0
  9. package/components/checkbox/checkbox.scss +1 -1
  10. package/components/checkbox/checkbox.vue +2 -0
  11. package/components/checkbox/types.d.ts +10 -0
  12. package/components/checkbox/types.js +4 -0
  13. package/components/checkbox/types.ts +16 -0
  14. package/components/checkbox-group/checkbox-group.vue +2 -0
  15. package/components/checkbox-group/types.d.ts +10 -0
  16. package/components/checkbox-group/types.js +8 -0
  17. package/components/checkbox-group/types.ts +20 -0
  18. package/components/checkbox-group2/checkbox-group2.vue +2 -0
  19. package/components/data-list/data-list.vue +13 -7
  20. package/components/date-picker/date-picker.vue +2 -0
  21. package/components/date-picker/types.d.ts +12 -0
  22. package/components/date-picker/types.js +4 -0
  23. package/components/date-picker/types.ts +18 -0
  24. package/components/dialog/dialog-content.vue +61 -46
  25. package/components/dialog/dialog.vue +55 -29
  26. package/components/dialog/types.d.ts +20 -0
  27. package/components/dialog/types.js +4 -0
  28. package/components/dialog/types.ts +24 -0
  29. package/components/dropdown/dropdown.vue +2 -0
  30. package/components/dropdown/types.d.ts +10 -0
  31. package/components/dropdown/types.js +4 -0
  32. package/components/dropdown/types.ts +16 -0
  33. package/components/empty/empty.vue +37 -20
  34. package/components/empty/types.d.ts +12 -0
  35. package/components/empty/types.js +9 -0
  36. package/components/empty/types.ts +21 -0
  37. package/components/flex/flex.vue +2 -0
  38. package/components/flex/types.d.ts +12 -0
  39. package/components/flex/types.js +4 -0
  40. package/components/flex/types.ts +22 -0
  41. package/components/form/form.vue +2 -0
  42. package/components/form/types.d.ts +6 -0
  43. package/components/form/types.js +4 -0
  44. package/components/form/types.ts +10 -0
  45. package/components/form-item/form-item.scss +6 -0
  46. package/components/form-item/form-item.vue +9 -3
  47. package/components/form-item/types.d.ts +6 -0
  48. package/components/form-item/types.js +4 -0
  49. package/components/form-item/types.ts +12 -0
  50. package/components/image/image.vue +28 -21
  51. package/components/image/types.d.ts +18 -0
  52. package/components/image/types.js +4 -0
  53. package/components/image/types.ts +23 -0
  54. package/components/index.d.ts +1 -0
  55. package/components/index.js +1 -0
  56. package/components/index.ts +1 -0
  57. package/components/input/input.scss +9 -0
  58. package/components/input/input.vue +11 -4
  59. package/components/input/password-input.vue +3 -1
  60. package/components/input/types.d.ts +20 -0
  61. package/components/input/types.js +8 -0
  62. package/components/input/types.ts +31 -0
  63. package/components/list-page/list-page.vue +21 -24
  64. package/components/list-page/types.d.ts +6 -0
  65. package/components/list-page/types.js +4 -0
  66. package/components/list-page/types.ts +12 -0
  67. package/components/load-more/load-more.scss +1 -0
  68. package/components/load-more/load-more.vue +66 -6
  69. package/components/load-more/types.d.ts +6 -0
  70. package/components/load-more/types.js +4 -0
  71. package/components/load-more/types.ts +12 -0
  72. package/components/loading/loading.vue +2 -0
  73. package/components/loading/types.d.ts +6 -0
  74. package/components/loading/types.js +4 -0
  75. package/components/loading/types.ts +12 -0
  76. package/components/message-box/message-box.d.ts +14 -16
  77. package/components/message-box/message-box.ts +0 -2
  78. package/components/message-box/message-box.vue +18 -43
  79. package/components/message-box/types.d.ts +16 -0
  80. package/components/message-box/types.js +4 -0
  81. package/components/message-box/types.ts +22 -0
  82. package/components/number-dialog/number-dialog.vue +5 -3
  83. package/components/number-dialog/types.d.ts +18 -0
  84. package/components/number-dialog/types.js +4 -0
  85. package/components/number-dialog/types.ts +24 -0
  86. package/components/number-input/number-input.scss +1 -1
  87. package/components/number-input/number-input.vue +6 -4
  88. package/components/number-input/types.d.ts +20 -0
  89. package/components/number-input/types.js +4 -0
  90. package/components/number-input/types.ts +26 -0
  91. package/components/numpad/numpad.vue +2 -0
  92. package/components/numpad/types.d.ts +10 -0
  93. package/components/numpad/types.js +4 -0
  94. package/components/numpad/types.ts +14 -0
  95. package/components/page/page-body.scss +1 -1
  96. package/components/page/page-footer.vue +2 -3
  97. package/components/page/page-header.vue +19 -9
  98. package/components/page/page.vue +45 -14
  99. package/components/page/types.d.ts +16 -0
  100. package/components/page/types.js +8 -0
  101. package/components/page/types.ts +27 -0
  102. package/components/picker/picker-popup.vue +10 -6
  103. package/components/picker/picker.vue +8 -0
  104. package/components/picker/types.d.ts +14 -0
  105. package/components/picker/types.js +4 -0
  106. package/components/picker/types.ts +18 -0
  107. package/components/popover/popover-content.vue +10 -1
  108. package/components/popover/popover-mask.vue +5 -5
  109. package/components/popover/popover.vue +34 -29
  110. package/components/popover/types.d.ts +10 -0
  111. package/components/popover/types.js +4 -0
  112. package/components/popover/types.ts +16 -0
  113. package/components/popup/popup-content.vue +61 -43
  114. package/components/popup/popup.vue +34 -28
  115. package/components/popup/types.d.ts +20 -0
  116. package/components/popup/types.js +4 -0
  117. package/components/popup/types.ts +24 -0
  118. package/components/price/price.scss +7 -3
  119. package/components/price/price.vue +6 -0
  120. package/components/price/types.d.ts +10 -0
  121. package/components/price/types.js +4 -0
  122. package/components/price/types.ts +14 -0
  123. package/components/search/search.vue +4 -2
  124. package/components/search/types.d.ts +14 -0
  125. package/components/search/types.js +4 -0
  126. package/components/search/types.ts +18 -0
  127. package/components/searching/searching.vue +2 -0
  128. package/components/searching/types.d.ts +8 -0
  129. package/components/searching/types.js +4 -0
  130. package/components/searching/types.ts +14 -0
  131. package/components/switch/switch.vue +5 -8
  132. package/components/switch/types.d.ts +8 -0
  133. package/components/switch/types.js +4 -0
  134. package/components/switch/types.ts +12 -0
  135. package/components/tab/tab.vue +2 -0
  136. package/components/tab/types.d.ts +8 -0
  137. package/components/tab/types.js +4 -0
  138. package/components/tab/types.ts +12 -0
  139. package/components/textarea/textarea.scss +10 -0
  140. package/components/textarea/textarea.vue +17 -9
  141. package/components/textarea/types.d.ts +12 -0
  142. package/components/textarea/types.js +4 -0
  143. package/components/textarea/types.ts +18 -0
  144. package/components/upload/composable.d.ts +24 -0
  145. package/components/upload/composable.js +173 -0
  146. package/components/upload/composable.ts +228 -0
  147. package/components/upload/media-upload.scss +12 -0
  148. package/components/upload/media-upload.vue +250 -0
  149. package/components/upload/types.d.ts +29 -0
  150. package/components/upload/types.js +4 -0
  151. package/components/upload/types.ts +38 -0
  152. package/composable/component-name.d.ts +8 -0
  153. package/composable/component-name.js +11 -4
  154. package/composable/component-name.ts +8 -0
  155. package/composable/dialog.d.ts +40 -0
  156. package/composable/dialog.js +96 -0
  157. package/composable/dialog.ts +113 -0
  158. package/composable/index.d.ts +5 -0
  159. package/composable/index.js +5 -0
  160. package/composable/index.ts +5 -0
  161. package/composable/message-box.d.ts +45 -28
  162. package/composable/message-box.js +68 -53
  163. package/composable/message-box.ts +101 -36
  164. package/composable/model.d.ts +8 -0
  165. package/composable/model.js +23 -0
  166. package/composable/model.ts +27 -0
  167. package/composable/page.d.ts +72 -0
  168. package/composable/page.js +25 -0
  169. package/composable/page.ts +34 -0
  170. package/configs/index.d.ts +24 -19
  171. package/configs/index.js +38 -35
  172. package/configs/index.ts +19 -0
  173. package/easyCom.d.ts +6 -6
  174. package/easyCom.js +19 -31
  175. package/easyCom.ts +4 -4
  176. package/globals.d.ts +93 -33
  177. package/globals.js +1 -0
  178. package/globals.ts +118 -0
  179. package/index.d.ts +3 -2
  180. package/index.js +2 -2
  181. package/index.ts +3 -2
  182. package/init.d.ts +11 -6
  183. package/init.js +84 -14
  184. package/init.ts +86 -1
  185. package/package.json +15 -12
  186. package/styles/var/component.scss +1 -0
  187. package/utils/date.d.ts +14 -19
  188. package/utils/date.js +77 -87
  189. package/utils/freezeDeep.js +7 -7
  190. package/utils/price.d.ts +15 -18
  191. package/utils/price.js +36 -37
  192. package/utils/upload.d.ts +1 -0
  193. package/utils/upload.js +8 -0
  194. package/utils/upload.ts +12 -0
  195. package/utils/windowInfo.d.ts +1 -9
  196. package/utils/windowInfo.js +10 -17
  197. package/utils/round.d.ts +0 -2
  198. package/utils/round.js +0 -13
  199. 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>