@ticatec/uniface-element 0.2.3 → 0.3.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.
Files changed (178) hide show
  1. package/dist/accordion/README.md +169 -0
  2. package/dist/accordion/README_CN.md +176 -0
  3. package/dist/action-bar/README.md +275 -0
  4. package/dist/action-bar/README_CN.md +285 -0
  5. package/dist/app-layout/README.md +374 -0
  6. package/dist/app-layout/README_CN.md +381 -0
  7. package/dist/app-top-bar/README.md +357 -0
  8. package/dist/app-top-bar/README_CN.md +365 -0
  9. package/dist/attachment-files/AttachmentFilesField.svelte +67 -1
  10. package/dist/attachment-files/AttachmentFilesField.svelte.d.ts +8 -1
  11. package/dist/base-calendar/CalendarBase.svelte +0 -3
  12. package/dist/box/README.md +385 -0
  13. package/dist/box/README_CN.md +393 -0
  14. package/dist/breadcrumbs/README.md +442 -0
  15. package/dist/breadcrumbs/README_CN.md +442 -0
  16. package/dist/button/Button.svelte +27 -7
  17. package/dist/button/Button.svelte.d.ts +3 -1
  18. package/dist/button/IconButton.svelte +26 -6
  19. package/dist/button/IconButton.svelte.d.ts +3 -1
  20. package/dist/button/IconButton_README.md +513 -0
  21. package/dist/button/IconButton_README_CN.md +513 -0
  22. package/dist/button/README.md +428 -0
  23. package/dist/button/README_CN.md +428 -0
  24. package/dist/button/TextButton.svelte +26 -6
  25. package/dist/button/TextButton.svelte.d.ts +3 -1
  26. package/dist/button/TextButton_README.md +571 -0
  27. package/dist/button/TextButton_README_CN.md +571 -0
  28. package/dist/card/README.md +666 -0
  29. package/dist/card/README_CN.md +666 -0
  30. package/dist/cascade-options-select/CascadeOptionSelect.svelte +43 -7
  31. package/dist/cascade-options-select/CascadeOptionSelect.svelte.d.ts +3 -4
  32. package/dist/cascade-options-select/README.md +663 -2
  33. package/dist/cascade-options-select/README_CN.md +664 -0
  34. package/dist/checkbox/CheckBox.svelte +20 -6
  35. package/dist/checkbox/CheckBox.svelte.d.ts +4 -2
  36. package/dist/checkbox/README.md +562 -0
  37. package/dist/checkbox/README_CN.md +727 -0
  38. package/dist/common-editor/NumberInput.svelte +39 -26
  39. package/dist/common-editor/NumberInput.svelte.d.ts +5 -1
  40. package/dist/concise-data-table/ConciseListTable.svelte +133 -36
  41. package/dist/concise-data-table/ConciseListTable.svelte.d.ts +8 -1
  42. package/dist/concise-data-table/DataCell.svelte +5 -6
  43. package/dist/concise-data-table/DataRow.svelte +22 -2
  44. package/dist/concise-data-table/DataRow.svelte.d.ts +1 -0
  45. package/dist/concise-data-table/README.md +606 -0
  46. package/dist/concise-data-table/README_CN.md +611 -0
  47. package/dist/context-menu/ContextMenu.svelte +138 -21
  48. package/dist/context-menu/ContextMenu.svelte.d.ts +5 -0
  49. package/dist/context-menu/ContextMenuItem.d.ts +20 -4
  50. package/dist/context-menu/README.md +551 -0
  51. package/dist/context-menu/README_CN.md +551 -0
  52. package/dist/criteria-field/CriteriaField.svelte +3 -5
  53. package/dist/criteria-field/README.md +532 -0
  54. package/dist/criteria-field/README_CN.md +532 -0
  55. package/dist/data-table/README.md +712 -0
  56. package/dist/data-table/README_CN.md +712 -0
  57. package/dist/date-picker/DatePicker.svelte +44 -7
  58. package/dist/date-picker/DatePicker.svelte.d.ts +3 -4
  59. package/dist/date-picker/DateTimePicker.svelte +32 -8
  60. package/dist/date-picker/DateTimePicker.svelte.d.ts +3 -4
  61. package/dist/date-picker/README.md +326 -0
  62. package/dist/date-picker/README_CN.md +316 -0
  63. package/dist/date-picker/ScrollBar.svelte +18 -8
  64. package/dist/date-picker/ScrollBar.svelte.d.ts +1 -1
  65. package/dist/date-picker/TimePanel.svelte +5 -5
  66. package/dist/date-picker/TimePanel.svelte.d.ts +1 -1
  67. package/dist/date-range/README.md +374 -0
  68. package/dist/date-range/README_CN.md +374 -0
  69. package/dist/dialog/DialogBoard.svelte +1 -1
  70. package/dist/dialog/README.md +596 -0
  71. package/dist/dialog/README_CN.md +596 -0
  72. package/dist/drawer/README.md +679 -1
  73. package/dist/drawer/README_CN.md +679 -0
  74. package/dist/group-box/group-check-box/GroupCheckBox.svelte +62 -5
  75. package/dist/group-box/group-check-box/GroupCheckBox.svelte.d.ts +7 -2
  76. package/dist/group-box/group-check-box/README.md +749 -0
  77. package/dist/group-box/group-check-box/README_CN.md +750 -0
  78. package/dist/group-box/group-radio-box/GroupRadioBox.svelte +69 -4
  79. package/dist/group-box/group-radio-box/GroupRadioBox.svelte.d.ts +8 -2
  80. package/dist/group-box/group-radio-box/README.md +920 -0
  81. package/dist/group-box/group-radio-box/README_CN.md +920 -0
  82. package/dist/image-files/README.md +1346 -0
  83. package/dist/image-files/README_CN.md +1346 -0
  84. package/dist/input-options-select/InputOptionsSelect.svelte +161 -65
  85. package/dist/input-options-select/InputOptionsSelect.svelte.d.ts +4 -1
  86. package/dist/lib/forwardAllEvents.d.ts +3 -0
  87. package/dist/lib/forwardAllEvents.js +24 -0
  88. package/dist/list-box/README.md +579 -0
  89. package/dist/list-box/README_CN.md +682 -0
  90. package/dist/lookup-editor/LookupEditor.svelte +15 -3
  91. package/dist/lookup-editor/LookupEditor.svelte.d.ts +3 -4
  92. package/dist/lookup-editor/README.md +684 -0
  93. package/dist/lookup-editor/README_CN.md +684 -0
  94. package/dist/memo-editor/MemoEditor.svelte +77 -27
  95. package/dist/memo-editor/MemoEditor.svelte.d.ts +6 -3
  96. package/dist/memo-editor/README.md +819 -0
  97. package/dist/memo-editor/README_CN.md +819 -0
  98. package/dist/message-box/README.md +398 -0
  99. package/dist/message-box/README_CN.md +398 -0
  100. package/dist/number-editor/NumberEditor.svelte +2 -2
  101. package/dist/number-editor/NumberEditor.svelte.d.ts +1 -1
  102. package/dist/number-editor/README.md +353 -22
  103. package/dist/number-editor/README_CN.md +357 -0
  104. package/dist/number-range/README.md +443 -0
  105. package/dist/number-range/README_CN.md +443 -0
  106. package/dist/options-multi-select/OptionsMultiSelect.svelte +87 -12
  107. package/dist/options-multi-select/OptionsMultiSelect.svelte.d.ts +3 -1
  108. package/dist/options-multi-select/README.md +480 -0
  109. package/dist/options-multi-select/README_CN.md +480 -0
  110. package/dist/options-select/OptionsSelect.svelte +36 -5
  111. package/dist/options-select/OptionsSelect.svelte.d.ts +3 -4
  112. package/dist/options-select/README.md +632 -0
  113. package/dist/options-select/README_CN.md +632 -0
  114. package/dist/page/README.md +605 -0
  115. package/dist/page/README_CN.md +605 -0
  116. package/dist/pagination/README.md +946 -0
  117. package/dist/pagination/README_CN.md +946 -0
  118. package/dist/pagination-panel/README.md +1075 -0
  119. package/dist/pagination-panel/README_CN.md +1075 -0
  120. package/dist/popup-hint/README.md +841 -0
  121. package/dist/popup-hint/README_CN.md +840 -0
  122. package/dist/progress-bar/README.md +242 -0
  123. package/dist/progress-bar/README_CN.md +242 -0
  124. package/dist/progress-step-bar/README.md +316 -0
  125. package/dist/progress-step-bar/README_CN.md +316 -0
  126. package/dist/prompts-text-editor/PromptsTextEditor.svelte +6 -3
  127. package/dist/prompts-text-editor/PromptsTextEditor.svelte.d.ts +3 -1
  128. package/dist/prompts-text-editor/README.md +329 -0
  129. package/dist/prompts-text-editor/README_CN.md +329 -0
  130. package/dist/property-editor/README.md +969 -0
  131. package/dist/property-editor/README_CN.md +969 -0
  132. package/dist/radio-button/README.md +372 -0
  133. package/dist/radio-button/README_CN.md +372 -0
  134. package/dist/radio-button/RadioButton.svelte +32 -3
  135. package/dist/radio-button/RadioButton.svelte.d.ts +12 -1
  136. package/dist/search-box/README.md +425 -0
  137. package/dist/search-box/README_CN.md +424 -0
  138. package/dist/search-box/SearchBox.svelte +22 -6
  139. package/dist/search-box/SearchBox.svelte.d.ts +4 -1
  140. package/dist/tabs/README.md +919 -0
  141. package/dist/tabs/README_CN.md +920 -0
  142. package/dist/tag/README.md +922 -0
  143. package/dist/tag/README_CN.md +922 -0
  144. package/dist/text-editor/PasswordEditor.svelte +17 -2
  145. package/dist/text-editor/PasswordEditor.svelte.d.ts +3 -3
  146. package/dist/text-editor/PasswordEditor_README.md +551 -0
  147. package/dist/text-editor/PasswordEditor_README_CN.md +551 -0
  148. package/dist/text-editor/README.md +466 -0
  149. package/dist/text-editor/README_CN.md +466 -0
  150. package/dist/text-editor/TextEditor.svelte +51 -19
  151. package/dist/text-editor/TextEditor.svelte.d.ts +0 -1
  152. package/dist/ticatec-uniface-web.css +1 -0
  153. package/dist/time-editor/README.md +498 -24
  154. package/dist/time-editor/README_CN.md +498 -0
  155. package/dist/time-editor/TimeEditor.svelte +7 -3
  156. package/dist/time-editor/TimeEditor.svelte.d.ts +3 -1
  157. package/dist/toast/README.md +667 -0
  158. package/dist/toast/README_CN.md +667 -0
  159. package/dist/transfer/README.md +795 -0
  160. package/dist/transfer/Transfer.svelte +6 -4
  161. package/dist/transfer/Transfer.svelte.d.ts +3 -1
  162. package/dist/tree-view/README.md +761 -0
  163. package/dist/tree-view/README_CN.md +761 -0
  164. package/dist/tree-view/TreeView.svelte +5 -3
  165. package/dist/tree-view/TreeView.svelte.d.ts +3 -1
  166. package/dist/unit-number-editor/README.md +546 -20
  167. package/dist/unit-number-editor/README_CN.md +525 -0
  168. package/dist/unit-number-editor/UnitNumberEditor.svelte +62 -8
  169. package/dist/unit-number-editor/UnitNumberEditor.svelte.d.ts +5 -1
  170. package/docs/form-field/cn/README_CN.md +364 -0
  171. package/docs/form-field/en/README.md +364 -0
  172. package/docs/form-panel/flex-form/cn/README_CN.md +586 -0
  173. package/docs/form-panel/flex-form/en/README.md +586 -0
  174. package/docs/form-panel/flex-row-form/cn/README_CN.md +787 -0
  175. package/docs/form-panel/flex-row-form/en/README.md +787 -0
  176. package/docs/form-panel/grid-form/en/README.md +744 -0
  177. package/package.json +1 -1
  178. package/dist/drawer/README-CN.md +0 -1
@@ -0,0 +1,1346 @@
1
+ # ImageFilesField 图片文件上传组件
2
+
3
+ 一个全面的图片上传和管理组件,提供拖放上传功能、图片预览、缩略图生成和文件管理功能。适用于处理图片库、头像上传以及带视觉预览的文档附件。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install @ticatec/uniface-element
9
+ ```
10
+
11
+ ## 导入
12
+
13
+ ```typescript
14
+ import ImageFilesField from "@ticatec/uniface-element/ImageFilesField";
15
+ import type { ImageFile, RemoveConfirm } from "@ticatec/uniface-element/ImageFilesField";
16
+ import type { FileUpload } from "@ticatec/uniface-element";
17
+ ```
18
+
19
+ ## 基本用法
20
+
21
+ ```svelte
22
+ <script>
23
+ import ImageFilesField from "@ticatec/uniface-element/ImageFilesField";
24
+
25
+ let files = [];
26
+
27
+ // 简单上传函数
28
+ const uploadImage = (file, progressUpdate, onUploaded, errorHandler) => {
29
+ // 模拟上传进度
30
+ let progress = 0;
31
+ const interval = setInterval(() => {
32
+ progress += 10;
33
+ progressUpdate(Math.floor(file.size / 100 * progress));
34
+
35
+ if (progress >= 100) {
36
+ clearInterval(interval);
37
+ // 调用 onUploaded 返回上传的文件URL和缩略图URL
38
+ onUploaded(`/uploads/${file.name}`, `/thumbnails/${file.name}`);
39
+ }
40
+ }, 100);
41
+
42
+ return {
43
+ cancel: async () => {
44
+ clearInterval(interval);
45
+ return true;
46
+ }
47
+ };
48
+ };
49
+ </script>
50
+
51
+ <ImageFilesField
52
+ bind:files={files}
53
+ uploadFile={uploadImage}
54
+ maxFiles={5}
55
+ />
56
+ ```
57
+
58
+ ## 属性
59
+
60
+ | 属性 | 类型 | 默认值 | 描述 |
61
+ |------|------|---------|-------------|
62
+ | `files` | `Array<ImageFile>` | `[]` | 已上传的图片文件数组 |
63
+ | `uploadFile` | `FileUpload` | 必需 | 上传文件处理函数 |
64
+ | `maxFiles` | `number` | `1` | 允许的最大文件数量 |
65
+ | `variant` | `"" \| "plain" \| "outlined" \| "filled"` | `""` | 视觉变体 |
66
+ | `disabled` | `boolean` | `false` | 组件是否禁用 |
67
+ | `readonly` | `boolean` | `false` | 组件是否只读 |
68
+ | `displayMode` | `DisplayMode` | `DisplayMode.Edit` | 显示模式(编辑或查看) |
69
+ | `removeFileConfirm` | `RemoveConfirm \| null` | `null` | 文件移除确认函数 |
70
+ | `style` | `string` | `""` | 附加 CSS 样式 |
71
+ | `text` | `string \| null` | `null` | 文件的文本表示(自动生成) |
72
+
73
+ ## 类型
74
+
75
+ ### ImageFile
76
+
77
+ ```typescript
78
+ interface ImageFile {
79
+ name: string; // 文件名称
80
+ uri: string; // 完整尺寸图片URL
81
+ thumbnail?: string; // 缩略图URL(可选)
82
+ }
83
+ ```
84
+
85
+ ### RemoveConfirm
86
+
87
+ ```typescript
88
+ type RemoveConfirm = (file: ImageFile) => Promise<boolean>;
89
+ ```
90
+
91
+ ### FileUpload
92
+
93
+ ```typescript
94
+ type FileUpload = (
95
+ file: File,
96
+ progressUpdate: ProgressUpdate,
97
+ onUploaded: OnUploaded,
98
+ errorHandler: ErrorHandler
99
+ ) => UploadProgress;
100
+
101
+ type ProgressUpdate = (bytesUploaded: number) => void;
102
+ type OnUploaded = (url: string, thumbnail?: string) => void;
103
+ type ErrorHandler = (error: Error) => void;
104
+
105
+ interface UploadProgress {
106
+ cancel: () => Promise<boolean>;
107
+ }
108
+ ```
109
+
110
+ ## 示例
111
+
112
+ ### 基本图片上传与预览
113
+
114
+ ```svelte
115
+ <script>
116
+ import ImageFilesField from "@ticatec/uniface-element/ImageFilesField";
117
+ import FormField from "@ticatec/uniface-element/FormField";
118
+
119
+ let profileImages = [];
120
+
121
+ const uploadProfileImage = (file, progressUpdate, onUploaded, errorHandler) => {
122
+ const formData = new FormData();
123
+ formData.append('image', file);
124
+
125
+ const xhr = new XMLHttpRequest();
126
+
127
+ xhr.upload.addEventListener('progress', (e) => {
128
+ if (e.lengthComputable) {
129
+ progressUpdate(e.loaded);
130
+ }
131
+ });
132
+
133
+ xhr.onload = () => {
134
+ if (xhr.status === 200) {
135
+ const response = JSON.parse(xhr.responseText);
136
+ onUploaded(response.url, response.thumbnail);
137
+ } else {
138
+ errorHandler(new Error(`上传失败: ${xhr.statusText}`));
139
+ }
140
+ };
141
+
142
+ xhr.onerror = () => {
143
+ errorHandler(new Error('上传过程中发生网络错误'));
144
+ };
145
+
146
+ xhr.open('POST', '/api/upload/profile-image');
147
+ xhr.send(formData);
148
+
149
+ return {
150
+ cancel: async () => {
151
+ xhr.abort();
152
+ return true;
153
+ }
154
+ };
155
+ };
156
+
157
+ const confirmRemoveImage = async (file) => {
158
+ return confirm(`确定要移除 ${file.name} 吗?`);
159
+ };
160
+ </script>
161
+
162
+ <div class="profile-section">
163
+ <FormField label="个人头像">
164
+ <ImageFilesField
165
+ bind:files={profileImages}
166
+ uploadFile={uploadProfileImage}
167
+ removeFileConfirm={confirmRemoveImage}
168
+ maxFiles={3}
169
+ variant="outlined"
170
+ />
171
+ </FormField>
172
+
173
+ <div class="image-info">
174
+ <p>已上传 {profileImages.length} 张图片</p>
175
+ {#each profileImages as image}
176
+ <p>• {image.name}</p>
177
+ {/each}
178
+ </div>
179
+ </div>
180
+
181
+ <style>
182
+ .profile-section {
183
+ max-width: 600px;
184
+ margin: 20px;
185
+ padding: 20px;
186
+ border: 1px solid #e2e8f0;
187
+ border-radius: 8px;
188
+ }
189
+
190
+ .image-info {
191
+ margin-top: 16px;
192
+ padding: 12px;
193
+ background: #f8fafc;
194
+ border-radius: 6px;
195
+ }
196
+ </style>
197
+ ```
198
+
199
+ ### 产品图片库管理
200
+
201
+ ```svelte
202
+ <script>
203
+ import ImageFilesField from "@ticatec/uniface-element/ImageFilesField";
204
+ import FormField from "@ticatec/uniface-element/FormField";
205
+
206
+ let productImages = [
207
+ {
208
+ name: "product-main.jpg",
209
+ uri: "/images/products/product-main.jpg",
210
+ thumbnail: "/images/products/thumbs/product-main.jpg"
211
+ },
212
+ {
213
+ name: "product-side.jpg",
214
+ uri: "/images/products/product-side.jpg",
215
+ thumbnail: "/images/products/thumbs/product-side.jpg"
216
+ }
217
+ ];
218
+
219
+ let isUploading = false;
220
+ let uploadProgress = {};
221
+
222
+ const uploadProductImage = (file, progressUpdate, onUploaded, errorHandler) => {
223
+ isUploading = true;
224
+ const fileId = `${file.name}-${Date.now()}`;
225
+ uploadProgress[fileId] = 0;
226
+
227
+ // 模拟图片处理和上传
228
+ const totalSteps = 100;
229
+ let currentStep = 0;
230
+
231
+ const interval = setInterval(() => {
232
+ currentStep += Math.random() * 5;
233
+
234
+ if (currentStep >= totalSteps) {
235
+ currentStep = totalSteps;
236
+ clearInterval(interval);
237
+
238
+ // 模拟成功上传
239
+ const baseUrl = '/api/products/images';
240
+ const imageUrl = `${baseUrl}/${fileId}.jpg`;
241
+ const thumbnailUrl = `${baseUrl}/thumbs/${fileId}.jpg`;
242
+
243
+ onUploaded(imageUrl, thumbnailUrl);
244
+ delete uploadProgress[fileId];
245
+ isUploading = false;
246
+ }
247
+
248
+ const bytesUploaded = Math.floor(file.size * (currentStep / totalSteps));
249
+ progressUpdate(bytesUploaded);
250
+ uploadProgress[fileId] = currentStep;
251
+ uploadProgress = { ...uploadProgress };
252
+ }, 50);
253
+
254
+ return {
255
+ cancel: async () => {
256
+ clearInterval(interval);
257
+ delete uploadProgress[fileId];
258
+ uploadProgress = { ...uploadProgress };
259
+ isUploading = false;
260
+ return true;
261
+ }
262
+ };
263
+ };
264
+
265
+ const confirmRemoveProductImage = async (file) => {
266
+ const result = await new Promise((resolve) => {
267
+ const modal = document.createElement('div');
268
+ modal.innerHTML = `
269
+ <div style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000;">
270
+ <div style="background: white; padding: 24px; border-radius: 8px; max-width: 400px;">
271
+ <h3>移除图片</h3>
272
+ <p>确定要移除 <strong>${file.name}</strong> 吗?</p>
273
+ <p>此操作无法撤销。</p>
274
+ <div style="display: flex; gap: 12px; justify-content: flex-end; margin-top: 20px;">
275
+ <button id="cancel">取消</button>
276
+ <button id="confirm" style="background: #dc2626; color: white;">移除</button>
277
+ </div>
278
+ </div>
279
+ </div>
280
+ `;
281
+
282
+ document.body.appendChild(modal);
283
+
284
+ modal.querySelector('#cancel').onclick = () => {
285
+ document.body.removeChild(modal);
286
+ resolve(false);
287
+ };
288
+
289
+ modal.querySelector('#confirm').onclick = () => {
290
+ document.body.removeChild(modal);
291
+ resolve(true);
292
+ };
293
+ });
294
+
295
+ return result;
296
+ };
297
+
298
+ $: totalImages = productImages.length;
299
+ $: canAddMore = totalImages < 10;
300
+ </script>
301
+
302
+ <div class="product-gallery">
303
+ <h2>产品图片库</h2>
304
+
305
+ <FormField label="产品图片(最多10张)" error={!canAddMore ? "最多允许10张图片" : ""}>
306
+ <ImageFilesField
307
+ bind:files={productImages}
308
+ uploadFile={uploadProductImage}
309
+ removeFileConfirm={confirmRemoveProductImage}
310
+ maxFiles={10}
311
+ variant="filled"
312
+ disabled={!canAddMore}
313
+ />
314
+ </FormField>
315
+
316
+ <div class="gallery-stats">
317
+ <div class="stat-item">
318
+ <span class="stat-label">图片总数:</span>
319
+ <span class="stat-value">{totalImages}/10</span>
320
+ </div>
321
+
322
+ {#if isUploading}
323
+ <div class="upload-status">
324
+ <span>正在上传图片...</span>
325
+ <div class="progress-indicators">
326
+ {#each Object.entries(uploadProgress) as [fileId, progress]}
327
+ <div class="progress-bar">
328
+ <div class="progress-fill" style="width: {progress}%"></div>
329
+ </div>
330
+ {/each}
331
+ </div>
332
+ </div>
333
+ {/if}
334
+ </div>
335
+
336
+ <div class="image-list">
337
+ <h3>当前图片</h3>
338
+ {#each productImages as image, index}
339
+ <div class="image-item">
340
+ <img src={image.thumbnail || image.uri} alt={image.name} width="50" height="50" />
341
+ <div class="image-details">
342
+ <span class="image-name">{image.name}</span>
343
+ <span class="image-position">位置 {index + 1}</span>
344
+ </div>
345
+ </div>
346
+ {/each}
347
+ </div>
348
+ </div>
349
+
350
+ <style>
351
+ .product-gallery {
352
+ max-width: 800px;
353
+ margin: 20px;
354
+ padding: 24px;
355
+ border: 1px solid #e2e8f0;
356
+ border-radius: 8px;
357
+ }
358
+
359
+ .gallery-stats {
360
+ margin: 16px 0;
361
+ padding: 12px;
362
+ background: #f8fafc;
363
+ border-radius: 6px;
364
+ }
365
+
366
+ .stat-item {
367
+ display: flex;
368
+ justify-content: space-between;
369
+ margin-bottom: 8px;
370
+ }
371
+
372
+ .stat-label {
373
+ font-weight: 500;
374
+ }
375
+
376
+ .stat-value {
377
+ color: #3b82f6;
378
+ font-weight: 600;
379
+ }
380
+
381
+ .upload-status {
382
+ margin-top: 12px;
383
+ }
384
+
385
+ .progress-indicators {
386
+ margin-top: 8px;
387
+ }
388
+
389
+ .progress-bar {
390
+ height: 4px;
391
+ background: #e5e7eb;
392
+ border-radius: 2px;
393
+ overflow: hidden;
394
+ margin: 4px 0;
395
+ }
396
+
397
+ .progress-fill {
398
+ height: 100%;
399
+ background: #3b82f6;
400
+ transition: width 0.3s ease;
401
+ }
402
+
403
+ .image-list {
404
+ margin-top: 20px;
405
+ }
406
+
407
+ .image-item {
408
+ display: flex;
409
+ align-items: center;
410
+ gap: 12px;
411
+ padding: 8px;
412
+ border-bottom: 1px solid #f3f4f6;
413
+ }
414
+
415
+ .image-item img {
416
+ border-radius: 4px;
417
+ object-fit: cover;
418
+ }
419
+
420
+ .image-details {
421
+ display: flex;
422
+ flex-direction: column;
423
+ gap: 2px;
424
+ }
425
+
426
+ .image-name {
427
+ font-weight: 500;
428
+ }
429
+
430
+ .image-position {
431
+ font-size: 0.875rem;
432
+ color: #6b7280;
433
+ }
434
+ </style>
435
+ ```
436
+
437
+ ### 头像上传与裁剪
438
+
439
+ ```svelte
440
+ <script>
441
+ import ImageFilesField from "@ticatec/uniface-element/ImageFilesField";
442
+ import FormField from "@ticatec/uniface-element/FormField";
443
+ import { DisplayMode } from "@ticatec/uniface-element";
444
+
445
+ let userAvatar = [];
446
+ let displayMode = DisplayMode.Edit;
447
+
448
+ const uploadAvatar = (file, progressUpdate, onUploaded, errorHandler) => {
449
+ // 验证文件类型
450
+ if (!file.type.startsWith('image/')) {
451
+ errorHandler(new Error('请选择图片文件'));
452
+ return { cancel: async () => true };
453
+ }
454
+
455
+ // 验证文件大小(最大5MB)
456
+ if (file.size > 5 * 1024 * 1024) {
457
+ errorHandler(new Error('图片必须小于5MB'));
458
+ return { cancel: async () => true };
459
+ }
460
+
461
+ // 创建图片用于验证
462
+ const img = new Image();
463
+ const canvas = document.createElement('canvas');
464
+ const ctx = canvas.getContext('2d');
465
+
466
+ return new Promise((resolve) => {
467
+ img.onload = () => {
468
+ // 设置画布大小为正方形缩略图
469
+ const size = 200;
470
+ canvas.width = size;
471
+ canvas.height = size;
472
+
473
+ // 计算裁剪尺寸(中心裁剪为正方形)
474
+ const minDimension = Math.min(img.width, img.height);
475
+ const x = (img.width - minDimension) / 2;
476
+ const y = (img.height - minDimension) / 2;
477
+
478
+ // 绘制裁剪并调整大小的图片
479
+ ctx.drawImage(img, x, y, minDimension, minDimension, 0, 0, size, size);
480
+
481
+ // 模拟上传进度
482
+ let progress = 0;
483
+ const interval = setInterval(() => {
484
+ progress += 10;
485
+ progressUpdate(Math.floor(file.size * (progress / 100)));
486
+
487
+ if (progress >= 100) {
488
+ clearInterval(interval);
489
+
490
+ // 将画布转换为blob并创建URL
491
+ canvas.toBlob((blob) => {
492
+ const thumbnailUrl = URL.createObjectURL(blob);
493
+ const fullUrl = URL.createObjectURL(file);
494
+
495
+ onUploaded(fullUrl, thumbnailUrl);
496
+ }, 'image/jpeg', 0.8);
497
+ }
498
+ }, 100);
499
+
500
+ resolve({
501
+ cancel: async () => {
502
+ clearInterval(interval);
503
+ return true;
504
+ }
505
+ });
506
+ };
507
+
508
+ img.onerror = () => {
509
+ errorHandler(new Error('无效的图片文件'));
510
+ resolve({ cancel: async () => true });
511
+ };
512
+
513
+ img.src = URL.createObjectURL(file);
514
+ });
515
+ };
516
+
517
+ const confirmRemoveAvatar = async (file) => {
518
+ return confirm('移除您的个人头像?');
519
+ };
520
+
521
+ const toggleMode = () => {
522
+ displayMode = displayMode === DisplayMode.Edit ? DisplayMode.View : DisplayMode.Edit;
523
+ };
524
+
525
+ $: hasAvatar = userAvatar.length > 0;
526
+ </script>
527
+
528
+ <div class="avatar-section">
529
+ <div class="avatar-header">
530
+ <h2>个人头像</h2>
531
+ <button on:click={toggleMode} class="mode-toggle">
532
+ {displayMode === DisplayMode.Edit ? '预览模式' : '编辑模式'}
533
+ </button>
534
+ </div>
535
+
536
+ <div class="avatar-container">
537
+ <FormField label="上传头像">
538
+ <ImageFilesField
539
+ bind:files={userAvatar}
540
+ uploadFile={uploadAvatar}
541
+ removeFileConfirm={confirmRemoveAvatar}
542
+ maxFiles={1}
543
+ variant="outlined"
544
+ {displayMode}
545
+ style="min-height: 200px; display: flex; align-items: center; justify-content: center;"
546
+ />
547
+ </FormField>
548
+
549
+ {#if !hasAvatar && displayMode === DisplayMode.Edit}
550
+ <div class="avatar-placeholder">
551
+ <div class="placeholder-icon">👤</div>
552
+ <p>点击上传您的个人头像</p>
553
+ <p class="placeholder-hint">推荐:正方形图片,最大5MB</p>
554
+ </div>
555
+ {/if}
556
+ </div>
557
+
558
+ {#if hasAvatar}
559
+ <div class="avatar-info">
560
+ <h3>当前头像</h3>
561
+ <div class="avatar-details">
562
+ <img
563
+ src={userAvatar[0].thumbnail || userAvatar[0].uri}
564
+ alt="头像预览"
565
+ class="avatar-preview"
566
+ />
567
+ <div class="avatar-metadata">
568
+ <p><strong>文件:</strong> {userAvatar[0].name}</p>
569
+ <p><strong>状态:</strong> ✅ 上传成功</p>
570
+ </div>
571
+ </div>
572
+ </div>
573
+ {/if}
574
+ </div>
575
+
576
+ <style>
577
+ .avatar-section {
578
+ max-width: 500px;
579
+ margin: 20px;
580
+ padding: 24px;
581
+ border: 1px solid #e2e8f0;
582
+ border-radius: 8px;
583
+ }
584
+
585
+ .avatar-header {
586
+ display: flex;
587
+ justify-content: space-between;
588
+ align-items: center;
589
+ margin-bottom: 20px;
590
+ }
591
+
592
+ .mode-toggle {
593
+ padding: 6px 12px;
594
+ background: #f3f4f6;
595
+ border: 1px solid #d1d5db;
596
+ border-radius: 6px;
597
+ cursor: pointer;
598
+ }
599
+
600
+ .avatar-container {
601
+ position: relative;
602
+ }
603
+
604
+ .avatar-placeholder {
605
+ position: absolute;
606
+ top: 50%;
607
+ left: 50%;
608
+ transform: translate(-50%, -50%);
609
+ text-align: center;
610
+ pointer-events: none;
611
+ }
612
+
613
+ .placeholder-icon {
614
+ font-size: 48px;
615
+ margin-bottom: 12px;
616
+ }
617
+
618
+ .placeholder-hint {
619
+ font-size: 0.875rem;
620
+ color: #6b7280;
621
+ }
622
+
623
+ .avatar-info {
624
+ margin-top: 20px;
625
+ padding: 16px;
626
+ background: #f8fafc;
627
+ border-radius: 8px;
628
+ }
629
+
630
+ .avatar-details {
631
+ display: flex;
632
+ align-items: center;
633
+ gap: 16px;
634
+ }
635
+
636
+ .avatar-preview {
637
+ width: 80px;
638
+ height: 80px;
639
+ border-radius: 50%;
640
+ object-fit: cover;
641
+ border: 3px solid white;
642
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
643
+ }
644
+
645
+ .avatar-metadata p {
646
+ margin: 4px 0;
647
+ }
648
+ </style>
649
+ ```
650
+
651
+ ### 文档附件与缩略图
652
+
653
+ ```svelte
654
+ <script>
655
+ import ImageFilesField from "@ticatec/uniface-element/ImageFilesField";
656
+ import FormField from "@ticatec/uniface-element/FormField";
657
+
658
+ let documentImages = [];
659
+ let uploadErrors = [];
660
+
661
+ const uploadDocument = (file, progressUpdate, onUploaded, errorHandler) => {
662
+ // 清除之前的错误
663
+ uploadErrors = [];
664
+
665
+ // 验证文件类型(本例仅限图片)
666
+ const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
667
+ if (!allowedTypes.includes(file.type)) {
668
+ const error = new Error(`不支持的文件类型: ${file.type}。请上传JPEG、PNG、GIF或WebP图片。`);
669
+ errorHandler(error);
670
+ uploadErrors = [...uploadErrors, error.message];
671
+ return { cancel: async () => true };
672
+ }
673
+
674
+ // 模拟文档处理(OCR、缩略图生成等)
675
+ let progress = 0;
676
+ const stages = [
677
+ '正在上传文件...',
678
+ '正在生成缩略图...',
679
+ '正在处理文档...',
680
+ '正在保存到数据库...',
681
+ '完成!'
682
+ ];
683
+ let currentStage = 0;
684
+
685
+ const interval = setInterval(() => {
686
+ progress += Math.random() * 15;
687
+
688
+ if (progress > (currentStage + 1) * 20 && currentStage < stages.length - 1) {
689
+ currentStage++;
690
+ console.log(stages[currentStage]);
691
+ }
692
+
693
+ if (progress >= 100) {
694
+ progress = 100;
695
+ clearInterval(interval);
696
+
697
+ // 模拟成功上传
698
+ const documentId = Date.now().toString();
699
+ const documentUrl = `/api/documents/${documentId}`;
700
+ const thumbnailUrl = `/api/documents/${documentId}/thumbnail`;
701
+
702
+ onUploaded(documentUrl, thumbnailUrl);
703
+ }
704
+
705
+ progressUpdate(Math.floor(file.size * (progress / 100)));
706
+ }, 200);
707
+
708
+ return {
709
+ cancel: async () => {
710
+ clearInterval(interval);
711
+ console.log('上传已取消');
712
+ return true;
713
+ }
714
+ };
715
+ };
716
+
717
+ const confirmRemoveDocument = async (file) => {
718
+ const userConfirmed = confirm(`移除文档 "${file.name}"?\n\n这将永久删除文档及其缩略图。`);
719
+
720
+ if (userConfirmed) {
721
+ // 模拟API调用以删除文档
722
+ console.log('正在删除文档:', file.name);
723
+ // 这里可以添加实际删除逻辑
724
+ }
725
+
726
+ return userConfirmed;
727
+ };
728
+
729
+ $: documentCount = documentImages.length;
730
+ $: hasErrors = uploadErrors.length > 0;
731
+ </script>
732
+
733
+ <div class="document-section">
734
+ <h2>文档附件</h2>
735
+
736
+ <FormField label="上传文档" error={hasErrors ? uploadErrors.join(', ') : ''}>
737
+ <ImageFilesField
738
+ bind:files={documentImages}
739
+ uploadFile={uploadDocument}
740
+ removeFileConfirm={confirmRemoveDocument}
741
+ maxFiles={20}
742
+ variant="filled"
743
+ />
744
+ </FormField>
745
+
746
+ <div class="document-summary">
747
+ <div class="summary-stats">
748
+ <div class="stat">
749
+ <span class="stat-number">{documentCount}</span>
750
+ <span class="stat-label">文档</span>
751
+ </div>
752
+ <div class="stat">
753
+ <span class="stat-number">{20 - documentCount}</span>
754
+ <span class="stat-label">剩余</span>
755
+ </div>
756
+ </div>
757
+
758
+ {#if hasErrors}
759
+ <div class="error-panel">
760
+ <h4>上传错误</h4>
761
+ {#each uploadErrors as error}
762
+ <p class="error-message">❌ {error}</p>
763
+ {/each}
764
+ </div>
765
+ {/if}
766
+ </div>
767
+
768
+ {#if documentCount > 0}
769
+ <div class="document-list">
770
+ <h3>已上传文档</h3>
771
+ <div class="document-grid">
772
+ {#each documentImages as doc, index}
773
+ <div class="document-card">
774
+ <div class="document-thumbnail">
775
+ <img src={doc.thumbnail || doc.uri} alt={doc.name} />
776
+ <div class="document-index">{index + 1}</div>
777
+ </div>
778
+ <div class="document-info">
779
+ <p class="document-name" title={doc.name}>{doc.name}</p>
780
+ <p class="document-status">✅ 已处理</p>
781
+ </div>
782
+ </div>
783
+ {/each}
784
+ </div>
785
+ </div>
786
+ {/if}
787
+ </div>
788
+
789
+ <style>
790
+ .document-section {
791
+ max-width: 800px;
792
+ margin: 20px;
793
+ padding: 24px;
794
+ border: 1px solid #e2e8f0;
795
+ border-radius: 8px;
796
+ }
797
+
798
+ .document-summary {
799
+ margin: 16px 0;
800
+ }
801
+
802
+ .summary-stats {
803
+ display: flex;
804
+ gap: 24px;
805
+ margin-bottom: 16px;
806
+ }
807
+
808
+ .stat {
809
+ display: flex;
810
+ flex-direction: column;
811
+ align-items: center;
812
+ padding: 12px;
813
+ background: #f8fafc;
814
+ border-radius: 8px;
815
+ min-width: 80px;
816
+ }
817
+
818
+ .stat-number {
819
+ font-size: 1.5rem;
820
+ font-weight: 600;
821
+ color: #3b82f6;
822
+ }
823
+
824
+ .stat-label {
825
+ font-size: 0.875rem;
826
+ color: #6b7280;
827
+ }
828
+
829
+ .error-panel {
830
+ padding: 12px;
831
+ background: #fef2f2;
832
+ border: 1px solid #fecaca;
833
+ border-radius: 6px;
834
+ }
835
+
836
+ .error-message {
837
+ margin: 4px 0;
838
+ color: #dc2626;
839
+ font-size: 0.875rem;
840
+ }
841
+
842
+ .document-list {
843
+ margin-top: 24px;
844
+ }
845
+
846
+ .document-grid {
847
+ display: grid;
848
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
849
+ gap: 16px;
850
+ margin-top: 12px;
851
+ }
852
+
853
+ .document-card {
854
+ border: 1px solid #e5e7eb;
855
+ border-radius: 8px;
856
+ overflow: hidden;
857
+ background: white;
858
+ }
859
+
860
+ .document-thumbnail {
861
+ position: relative;
862
+ height: 120px;
863
+ background: #f9fafb;
864
+ display: flex;
865
+ align-items: center;
866
+ justify-content: center;
867
+ }
868
+
869
+ .document-thumbnail img {
870
+ max-width: 100%;
871
+ max-height: 100%;
872
+ object-fit: cover;
873
+ }
874
+
875
+ .document-index {
876
+ position: absolute;
877
+ top: 8px;
878
+ right: 8px;
879
+ background: #3b82f6;
880
+ color: white;
881
+ border-radius: 50%;
882
+ width: 24px;
883
+ height: 24px;
884
+ display: flex;
885
+ align-items: center;
886
+ justify-content: center;
887
+ font-size: 0.75rem;
888
+ font-weight: 600;
889
+ }
890
+
891
+ .document-info {
892
+ padding: 12px;
893
+ }
894
+
895
+ .document-name {
896
+ margin: 0 0 4px 0;
897
+ font-weight: 500;
898
+ font-size: 0.875rem;
899
+ overflow: hidden;
900
+ text-overflow: ellipsis;
901
+ white-space: nowrap;
902
+ }
903
+
904
+ .document-status {
905
+ margin: 0;
906
+ font-size: 0.75rem;
907
+ color: #16a34a;
908
+ }
909
+ </style>
910
+ ```
911
+
912
+ ### 自定义上传进度与错误处理
913
+
914
+ ```svelte
915
+ <script>
916
+ import ImageFilesField from "@ticatec/uniface-element/ImageFilesField";
917
+ import FormField from "@ticatec/uniface-element/FormField";
918
+
919
+ let galleryImages = [];
920
+ let activeUploads = new Map();
921
+ let uploadHistory = [];
922
+
923
+ const uploadWithErrorHandling = (file, progressUpdate, onUploaded, errorHandler) => {
924
+ const uploadId = Date.now() + Math.random();
925
+
926
+ // 添加到活跃上传跟踪
927
+ activeUploads.set(uploadId, {
928
+ file: file.name,
929
+ startTime: new Date(),
930
+ progress: 0
931
+ });
932
+ activeUploads = new Map(activeUploads);
933
+
934
+ // 模拟各种上传场景
935
+ const scenarios = [
936
+ { success: true, delay: 2000 }, // 2秒后成功
937
+ { success: false, error: '网络超时', delay: 3000 }, // 网络错误
938
+ { success: false, error: '服务器错误 500', delay: 1500 }, // 服务器错误
939
+ { success: true, delay: 4000 }, // 慢速成功
940
+ ];
941
+
942
+ const scenario = scenarios[Math.floor(Math.random() * scenarios.length)];
943
+ let progress = 0;
944
+ let cancelled = false;
945
+
946
+ const interval = setInterval(() => {
947
+ if (cancelled) {
948
+ clearInterval(interval);
949
+ return;
950
+ }
951
+
952
+ progress += Math.random() * 20;
953
+
954
+ // 更新跟踪
955
+ if (activeUploads.has(uploadId)) {
956
+ activeUploads.get(uploadId).progress = Math.min(progress, 95);
957
+ activeUploads = new Map(activeUploads);
958
+ }
959
+
960
+ progressUpdate(Math.floor(file.size * (Math.min(progress, 95) / 100)));
961
+
962
+ if (progress >= 100) {
963
+ clearInterval(interval);
964
+
965
+ setTimeout(() => {
966
+ if (cancelled) return;
967
+
968
+ // 从活跃上传中移除
969
+ activeUploads.delete(uploadId);
970
+ activeUploads = new Map(activeUploads);
971
+
972
+ if (scenario.success) {
973
+ // 成功上传
974
+ const imageUrl = `/uploads/${uploadId}_${file.name}`;
975
+ const thumbnailUrl = `/uploads/thumbs/${uploadId}_${file.name}`;
976
+
977
+ onUploaded(imageUrl, thumbnailUrl);
978
+
979
+ // 添加到历史记录
980
+ uploadHistory = [{
981
+ file: file.name,
982
+ status: 'success',
983
+ timestamp: new Date(),
984
+ message: '上传成功完成'
985
+ }, ...uploadHistory];
986
+ } else {
987
+ // 上传失败
988
+ const error = new Error(scenario.error);
989
+ errorHandler(error);
990
+
991
+ // 添加到历史记录
992
+ uploadHistory = [{
993
+ file: file.name,
994
+ status: 'error',
995
+ timestamp: new Date(),
996
+ message: scenario.error
997
+ }, ...uploadHistory];
998
+ }
999
+ }, scenario.delay - (progress > 95 ? (progress - 95) * 50 : 0));
1000
+ }
1001
+ }, 100);
1002
+
1003
+ return {
1004
+ cancel: async () => {
1005
+ cancelled = true;
1006
+ clearInterval(interval);
1007
+
1008
+ // 从活跃上传中移除
1009
+ activeUploads.delete(uploadId);
1010
+ activeUploads = new Map(activeUploads);
1011
+
1012
+ // 添加到历史记录
1013
+ uploadHistory = [{
1014
+ file: file.name,
1015
+ status: 'cancelled',
1016
+ timestamp: new Date(),
1017
+ message: '用户取消上传'
1018
+ }, ...uploadHistory];
1019
+
1020
+ return true;
1021
+ }
1022
+ };
1023
+ };
1024
+
1025
+ const confirmRemoveGalleryImage = async (file) => {
1026
+ const shouldRemove = confirm(`从图片库中移除 "${file.name}"?`);
1027
+
1028
+ if (shouldRemove) {
1029
+ // 添加到历史记录
1030
+ uploadHistory = [{
1031
+ file: file.name,
1032
+ status: 'removed',
1033
+ timestamp: new Date(),
1034
+ message: '从图片库中移除图片'
1035
+ }, ...uploadHistory];
1036
+ }
1037
+
1038
+ return shouldRemove;
1039
+ };
1040
+
1041
+ const clearHistory = () => {
1042
+ uploadHistory = [];
1043
+ };
1044
+
1045
+ const formatTime = (date) => {
1046
+ return date.toLocaleTimeString('zh-CN');
1047
+ };
1048
+
1049
+ const getStatusIcon = (status) => {
1050
+ const icons = {
1051
+ success: '✅',
1052
+ error: '❌',
1053
+ cancelled: '⏹️',
1054
+ removed: '🗑️'
1055
+ };
1056
+ return icons[status] || '📄';
1057
+ };
1058
+
1059
+ $: hasActiveUploads = activeUploads.size > 0;
1060
+ $: totalImages = galleryImages.length;
1061
+ </script>
1062
+
1063
+ <div class="advanced-upload">
1064
+ <h2>高级图片上传</h2>
1065
+
1066
+ <FormField label="图片库">
1067
+ <ImageFilesField
1068
+ bind:files={galleryImages}
1069
+ uploadFile={uploadWithErrorHandling}
1070
+ removeFileConfirm={confirmRemoveGalleryImage}
1071
+ maxFiles={15}
1072
+ variant="outlined"
1073
+ />
1074
+ </FormField>
1075
+
1076
+ {#if hasActiveUploads}
1077
+ <div class="active-uploads">
1078
+ <h3>活跃上传</h3>
1079
+ {#each Array.from(activeUploads.entries()) as [id, upload]}
1080
+ <div class="upload-item">
1081
+ <div class="upload-info">
1082
+ <span class="upload-filename">{upload.file}</span>
1083
+ <span class="upload-time">开始时间: {formatTime(upload.startTime)}</span>
1084
+ </div>
1085
+ <div class="upload-progress">
1086
+ <div class="progress-bar">
1087
+ <div class="progress-fill" style="width: {upload.progress}%"></div>
1088
+ </div>
1089
+ <span class="progress-text">{Math.round(upload.progress)}%</span>
1090
+ </div>
1091
+ </div>
1092
+ {/each}
1093
+ </div>
1094
+ {/if}
1095
+
1096
+ <div class="gallery-stats">
1097
+ <div class="stat-row">
1098
+ <span>图片总数: {totalImages}/15</span>
1099
+ <span>活跃上传: {activeUploads.size}</span>
1100
+ </div>
1101
+ </div>
1102
+
1103
+ {#if uploadHistory.length > 0}
1104
+ <div class="upload-history">
1105
+ <div class="history-header">
1106
+ <h3>上传历史</h3>
1107
+ <button on:click={clearHistory} class="clear-history">清除</button>
1108
+ </div>
1109
+
1110
+ <div class="history-list">
1111
+ {#each uploadHistory.slice(0, 10) as entry}
1112
+ <div class="history-item" class:error={entry.status === 'error'}>
1113
+ <span class="history-icon">{getStatusIcon(entry.status)}</span>
1114
+ <div class="history-details">
1115
+ <span class="history-file">{entry.file}</span>
1116
+ <span class="history-message">{entry.message}</span>
1117
+ </div>
1118
+ <span class="history-time">{formatTime(entry.timestamp)}</span>
1119
+ </div>
1120
+ {/each}
1121
+ </div>
1122
+ </div>
1123
+ {/if}
1124
+ </div>
1125
+
1126
+ <style>
1127
+ .advanced-upload {
1128
+ max-width: 800px;
1129
+ margin: 20px;
1130
+ padding: 24px;
1131
+ border: 1px solid #e2e8f0;
1132
+ border-radius: 8px;
1133
+ }
1134
+
1135
+ .active-uploads {
1136
+ margin: 20px 0;
1137
+ padding: 16px;
1138
+ background: #fef3c7;
1139
+ border-radius: 8px;
1140
+ border-left: 4px solid #f59e0b;
1141
+ }
1142
+
1143
+ .upload-item {
1144
+ display: flex;
1145
+ align-items: center;
1146
+ gap: 16px;
1147
+ margin: 8px 0;
1148
+ }
1149
+
1150
+ .upload-info {
1151
+ flex: 1;
1152
+ display: flex;
1153
+ flex-direction: column;
1154
+ gap: 2px;
1155
+ }
1156
+
1157
+ .upload-filename {
1158
+ font-weight: 500;
1159
+ }
1160
+
1161
+ .upload-time {
1162
+ font-size: 0.75rem;
1163
+ color: #6b7280;
1164
+ }
1165
+
1166
+ .upload-progress {
1167
+ display: flex;
1168
+ align-items: center;
1169
+ gap: 8px;
1170
+ min-width: 120px;
1171
+ }
1172
+
1173
+ .progress-bar {
1174
+ flex: 1;
1175
+ height: 6px;
1176
+ background: #e5e7eb;
1177
+ border-radius: 3px;
1178
+ overflow: hidden;
1179
+ }
1180
+
1181
+ .progress-fill {
1182
+ height: 100%;
1183
+ background: #3b82f6;
1184
+ transition: width 0.3s ease;
1185
+ }
1186
+
1187
+ .progress-text {
1188
+ font-size: 0.875rem;
1189
+ font-weight: 500;
1190
+ min-width: 40px;
1191
+ }
1192
+
1193
+ .gallery-stats {
1194
+ margin: 16px 0;
1195
+ padding: 12px;
1196
+ background: #f8fafc;
1197
+ border-radius: 6px;
1198
+ }
1199
+
1200
+ .stat-row {
1201
+ display: flex;
1202
+ justify-content: space-between;
1203
+ font-weight: 500;
1204
+ }
1205
+
1206
+ .upload-history {
1207
+ margin-top: 24px;
1208
+ }
1209
+
1210
+ .history-header {
1211
+ display: flex;
1212
+ justify-content: space-between;
1213
+ align-items: center;
1214
+ margin-bottom: 12px;
1215
+ }
1216
+
1217
+ .clear-history {
1218
+ padding: 4px 8px;
1219
+ background: #f3f4f6;
1220
+ border: 1px solid #d1d5db;
1221
+ border-radius: 4px;
1222
+ cursor: pointer;
1223
+ font-size: 0.875rem;
1224
+ }
1225
+
1226
+ .history-list {
1227
+ border: 1px solid #e5e7eb;
1228
+ border-radius: 6px;
1229
+ overflow: hidden;
1230
+ }
1231
+
1232
+ .history-item {
1233
+ display: flex;
1234
+ align-items: center;
1235
+ gap: 12px;
1236
+ padding: 12px;
1237
+ border-bottom: 1px solid #f3f4f6;
1238
+ background: white;
1239
+ }
1240
+
1241
+ .history-item:last-child {
1242
+ border-bottom: none;
1243
+ }
1244
+
1245
+ .history-item.error {
1246
+ background: #fef2f2;
1247
+ }
1248
+
1249
+ .history-icon {
1250
+ font-size: 1.25rem;
1251
+ }
1252
+
1253
+ .history-details {
1254
+ flex: 1;
1255
+ display: flex;
1256
+ flex-direction: column;
1257
+ gap: 2px;
1258
+ }
1259
+
1260
+ .history-file {
1261
+ font-weight: 500;
1262
+ font-size: 0.875rem;
1263
+ }
1264
+
1265
+ .history-message {
1266
+ font-size: 0.75rem;
1267
+ color: #6b7280;
1268
+ }
1269
+
1270
+ .history-time {
1271
+ font-size: 0.75rem;
1272
+ color: #9ca3af;
1273
+ min-width: 80px;
1274
+ text-align: right;
1275
+ }
1276
+ </style>
1277
+ ```
1278
+
1279
+ ## 样式
1280
+
1281
+ ImageFilesField 组件可通过 CSS 自定义属性进行样式化:
1282
+
1283
+ ```css
1284
+ .uniface-images-field {
1285
+ --border-color: #e2e8f0;
1286
+ --background-color: #ffffff;
1287
+ --hover-background: #f9fafb;
1288
+ --upload-icon-color: #6b7280;
1289
+ --preview-overlay: rgba(0, 0, 0, 0.7);
1290
+ }
1291
+
1292
+ /* 变体特定样式 */
1293
+ .uniface-common-editor.outlined {
1294
+ border: 2px dashed var(--border-color);
1295
+ border-radius: 8px;
1296
+ padding: 20px;
1297
+ }
1298
+
1299
+ .uniface-common-editor.filled {
1300
+ background-color: #f8fafc;
1301
+ border: 1px solid var(--border-color);
1302
+ border-radius: 8px;
1303
+ padding: 20px;
1304
+ }
1305
+
1306
+ /* 上传区域样式 */
1307
+ .editor-action-icon {
1308
+ display: flex;
1309
+ align-items: center;
1310
+ justify-content: center;
1311
+ padding: 20px;
1312
+ border: 2px dashed #d1d5db;
1313
+ border-radius: 8px;
1314
+ cursor: pointer;
1315
+ transition: all 0.2s;
1316
+ }
1317
+
1318
+ .editor-action-icon:hover {
1319
+ border-color: #3b82f6;
1320
+ background-color: #f0f9ff;
1321
+ }
1322
+ ```
1323
+
1324
+ ## 可访问性
1325
+
1326
+ ImageFilesField 组件包括以下可访问性功能:
1327
+
1328
+ - 支持键盘导航
1329
+ - 屏幕阅读器的 ARIA 标签
1330
+ - 交互元素的焦点管理
1331
+ - 图片的替代文本支持
1332
+ - 上传进度的屏幕阅读器提示
1333
+
1334
+ ## 最佳实践
1335
+
1336
+ 1. **文件验证**:在上传前始终验证文件类型和大小
1337
+ 2. **错误处理**:提供清晰的错误消息和重试机制
1338
+ 3. **进度反馈**:显示上传进度并允许取消
1339
+ 4. **缩略图生成**:生成适当的缩略图以提高性能
1340
+ 5. **安全性**:在客户端和服务器端验证文件
1341
+ 6. **存储**:实现适当的文件存储和清理策略
1342
+ 7. **性能**:对于大型图片库考虑懒加载
1343
+
1344
+ ## 许可证
1345
+
1346
+ MIT