@ticatec/uniface-element 0.2.0 → 0.2.1
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/README.md +273 -1
- package/README_CN.md +341 -0
- package/dist/common/MouseClickHandler.d.ts +1 -1
- package/dist/dialog/CommonDialog.svelte +6 -3
- package/dist/dialog/CommonDialog.svelte.d.ts +2 -1
- package/dist/dialog/Dialog.svelte +36 -10
- package/dist/dialog/Dialog.svelte.d.ts +4 -3
- package/dist/dialog/DialogBoard.svelte +11 -8
- package/dist/dialog/DialogWrapper.svelte +10 -0
- package/dist/dialog/DialogWrapper.svelte.d.ts +22 -0
- package/dist/dialog/Dialogs.js +1 -1
- package/dist/dialog/ModalResult.d.ts +5 -0
- package/dist/dialog/ModalResult.js +5 -0
- package/docs/buttons/cn/ActionBar.md +459 -0
- package/docs/buttons/cn/Button.md +228 -0
- package/docs/buttons/cn/IconButton.md +294 -0
- package/docs/buttons/cn/README.md +600 -0
- package/docs/buttons/cn/TextButton.md +163 -0
- package/docs/buttons/en/ActionBar.md +459 -0
- package/docs/buttons/en/Button.md +228 -0
- package/docs/buttons/en/IconButton.md +294 -0
- package/docs/buttons/en/README.md +600 -0
- package/docs/buttons/en/TextButton.md +163 -0
- package/docs/containers/cn/accordion.md +622 -0
- package/docs/containers/cn/box.md +445 -0
- package/docs/containers/cn/card.md +404 -0
- package/docs/containers/cn/page.md +576 -0
- package/docs/containers/cn/tabs.md +686 -0
- package/docs/containers/en/accordion.md +622 -0
- package/docs/containers/en/box.md +445 -0
- package/docs/containers/en/card.md +404 -0
- package/docs/containers/en/page.md +576 -0
- package/docs/containers/en/tabs.md +686 -0
- package/docs/data-display/cn/concise-data-table.md +731 -0
- package/docs/data-display/cn/listbox.md +786 -0
- package/docs/data-display/cn/treeview.md +863 -0
- package/docs/data-display/en/concise-data-table.md +732 -0
- package/docs/data-display/en/listbox.md +786 -0
- package/docs/data-display/en/treeview.md +863 -0
- package/docs/data-table/cn/README.md +596 -0
- package/docs/data-table/en/README.md +596 -0
- package/docs/dialog/cn/README.md +767 -0
- package/docs/dialog/en/README.md +767 -0
- package/docs/form/cn/README.md +847 -0
- package/docs/form/en/README.md +847 -0
- package/docs/form-components/cn/README.md +495 -0
- package/docs/form-components/cn/attachmentfilesfield.md +757 -0
- package/docs/form-components/cn/cascade-options-select.md +374 -0
- package/docs/form-components/cn/checkbox.md +58 -0
- package/docs/form-components/cn/date-picker.md +138 -0
- package/docs/form-components/cn/datetime-picker.md +228 -0
- package/docs/form-components/cn/group-checkbox.md +405 -0
- package/docs/form-components/cn/group-radiobox.md +525 -0
- package/docs/form-components/cn/imagefilesfield.md +651 -0
- package/docs/form-components/cn/input-options-select.md +250 -0
- package/docs/form-components/cn/lookup-editor.md +406 -0
- package/docs/form-components/cn/memoeditor.md +409 -0
- package/docs/form-components/cn/number-editor.md +261 -0
- package/docs/form-components/cn/options-multi-select.md +177 -0
- package/docs/form-components/cn/options-select.md +83 -0
- package/docs/form-components/cn/passwordeditor.md +538 -0
- package/docs/form-components/cn/prompts-text-editor.md +241 -0
- package/docs/form-components/cn/radiobutton.md +84 -0
- package/docs/form-components/cn/switch.md +178 -0
- package/docs/form-components/cn/text-editor.md +142 -0
- package/docs/form-components/cn/time-editor.md +159 -0
- package/docs/form-components/cn/unit-number-editor.md +164 -0
- package/docs/form-components/en/README.md +463 -0
- package/docs/form-components/en/attachmentfilesfield.md +757 -0
- package/docs/form-components/en/cascade-options-select.md +358 -0
- package/docs/form-components/en/checkbox.md +59 -0
- package/docs/form-components/en/date-picker.md +138 -0
- package/docs/form-components/en/datetime-picker.md +228 -0
- package/docs/form-components/en/group-checkbox.md +395 -0
- package/docs/form-components/en/group-radiobox.md +498 -0
- package/docs/form-components/en/imagefilesfield.md +651 -0
- package/docs/form-components/en/input-options-select.md +239 -0
- package/docs/form-components/en/lookup-editor.md +389 -0
- package/docs/form-components/en/memoeditor.md +409 -0
- package/docs/form-components/en/number-editor.md +249 -0
- package/docs/form-components/en/options-multi-select.md +177 -0
- package/docs/form-components/en/options-select.md +83 -0
- package/docs/form-components/en/passwordeditor.md +538 -0
- package/docs/form-components/en/prompts-text-editor.md +241 -0
- package/docs/form-components/en/radiobutton.md +84 -0
- package/docs/form-components/en/switch.md +180 -0
- package/docs/form-components/en/text-editor.md +142 -0
- package/docs/form-components/en/time-editor.md +159 -0
- package/docs/form-components/en/unit-number-editor.md +164 -0
- package/docs/form-layout/cn/criteria-field.md +652 -0
- package/docs/form-layout/cn/flex-form.md +454 -0
- package/docs/form-layout/cn/flex-row-form.md +542 -0
- package/docs/form-layout/cn/form-field.md +923 -0
- package/docs/form-layout/cn/grid-form.md +676 -0
- package/docs/form-layout/en/criteria-field.md +652 -0
- package/docs/form-layout/en/flex-form.md +454 -0
- package/docs/form-layout/en/flex-row-form.md +542 -0
- package/docs/form-layout/en/form-field.md +923 -0
- package/docs/form-layout/en/grid-form.md +676 -0
- package/docs/global/cn/README.md +534 -0
- package/docs/global/en/README.md +534 -0
- package/docs/layout/cn/classiclayout.md +460 -0
- package/docs/layout/cn/headerlayout.md +524 -0
- package/docs/layout/cn/sidebarlayout.md +263 -0
- package/docs/layout/en/classiclayout.md +951 -0
- package/docs/layout/en/headerlayout.md +524 -0
- package/docs/layout/en/sidebarlayout.md +262 -0
- package/docs/misc/cn/README.md +1217 -0
- package/docs/misc/en/README.md +1217 -0
- package/docs/navigation/cn/breadcrumb.md +291 -0
- package/docs/navigation/cn/navigator.md +322 -0
- package/docs/navigation/cn/navigatormenu.md +512 -0
- package/docs/navigation/cn/progressbar.md +199 -0
- package/docs/navigation/cn/progressstepbar.md +299 -0
- package/docs/navigation/en/breadcrumb.md +291 -0
- package/docs/navigation/en/navigator.md +322 -0
- package/docs/navigation/en/navigatormenu.md +512 -0
- package/docs/navigation/en/progressbar.md +199 -0
- package/docs/navigation/en/progressstepbar.md +299 -0
- package/docs/search-filters/cn/daterange.md +1156 -0
- package/docs/search-filters/cn/numberrange.md +688 -0
- package/docs/search-filters/cn/searchbox.md +1582 -0
- package/docs/search-filters/en/daterange.md +1156 -0
- package/docs/search-filters/en/numberrange.md +688 -0
- package/docs/search-filters/en/searchbox.md +1582 -0
- package/package.json +38 -5
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
# AttachmentFilesField 附件文件字段组件
|
|
2
|
+
|
|
3
|
+
AttachmentFilesField 组件提供全面的文件上传和管理界面,支持多种文件类型、拖拽上传、进度跟踪和文件删除确认。
|
|
4
|
+
|
|
5
|
+
## 功能特性
|
|
6
|
+
|
|
7
|
+
- **多文件上传**:支持同时上传多个文件
|
|
8
|
+
- **文件类型检测**:自动检测和分类文件类型(图片、PDF、文档等)
|
|
9
|
+
- **拖拽支持**:直观的拖拽文件上传界面
|
|
10
|
+
- **上传进度**:实时上传进度跟踪和取消功能
|
|
11
|
+
- **文件管理**:查看、删除和管理已上传的文件
|
|
12
|
+
- **显示模式**:编辑和查看模式适应不同场景
|
|
13
|
+
- **变体支持**:多种样式变体(plain、outlined、filled)
|
|
14
|
+
- **确认对话框**:文件删除的可选确认对话框
|
|
15
|
+
- **响应式设计**:适应不同屏幕尺寸和布局
|
|
16
|
+
|
|
17
|
+
## 导入
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import AttachmentFilesField from '@ticatec/uniface-element/AttachmentFilesField';
|
|
21
|
+
import type { AttachmentFile, FileUpload } from '@ticatec/uniface-element/AttachmentFilesField';
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 基本用法
|
|
25
|
+
|
|
26
|
+
### 简单文件上传字段
|
|
27
|
+
|
|
28
|
+
```svelte
|
|
29
|
+
<script>
|
|
30
|
+
import AttachmentFilesField from '@ticatec/uniface-element/AttachmentFilesField';
|
|
31
|
+
import type { AttachmentFile, FileUpload } from '@ticatec/uniface-element/AttachmentFilesField';
|
|
32
|
+
|
|
33
|
+
let attachments: AttachmentFile[] = [];
|
|
34
|
+
|
|
35
|
+
const uploadFile: FileUpload = (file, progressUpdate, onUploaded, errorHandler) => {
|
|
36
|
+
// 模拟文件上传
|
|
37
|
+
let uploadedBytes = 0;
|
|
38
|
+
const totalBytes = file.size;
|
|
39
|
+
|
|
40
|
+
const uploadInterval = setInterval(() => {
|
|
41
|
+
uploadedBytes += Math.min(1024 * 100, totalBytes - uploadedBytes); // 100KB 块
|
|
42
|
+
progressUpdate(uploadedBytes);
|
|
43
|
+
|
|
44
|
+
if (uploadedBytes >= totalBytes) {
|
|
45
|
+
clearInterval(uploadInterval);
|
|
46
|
+
// 模拟成功上传并返回模拟URL
|
|
47
|
+
const mockUrl = URL.createObjectURL(file);
|
|
48
|
+
onUploaded(mockUrl);
|
|
49
|
+
}
|
|
50
|
+
}, 100);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
cancel: async () => {
|
|
54
|
+
clearInterval(uploadInterval);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<AttachmentFilesField
|
|
62
|
+
bind:files={attachments}
|
|
63
|
+
{uploadFile}
|
|
64
|
+
/>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 带验证的样式化文件上传
|
|
68
|
+
|
|
69
|
+
```svelte
|
|
70
|
+
<script>
|
|
71
|
+
import AttachmentFilesField from '@ticatec/uniface-element/AttachmentFilesField';
|
|
72
|
+
import type { AttachmentFile, FileUpload } from '@ticatec/uniface-element/AttachmentFilesField';
|
|
73
|
+
|
|
74
|
+
let documents: AttachmentFile[] = [];
|
|
75
|
+
|
|
76
|
+
const uploadDocument: FileUpload = (file, progressUpdate, onUploaded, errorHandler) => {
|
|
77
|
+
// 验证文件大小(最大 10MB)
|
|
78
|
+
if (file.size > 10 * 1024 * 1024) {
|
|
79
|
+
errorHandler(new Error('文件大小不能超过 10MB'));
|
|
80
|
+
return { cancel: async () => true };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 验证文件类型
|
|
84
|
+
const allowedTypes = ['pdf', 'doc', 'docx', 'txt', 'jpg', 'png'];
|
|
85
|
+
const fileExtension = file.name.split('.').pop()?.toLowerCase();
|
|
86
|
+
|
|
87
|
+
if (!fileExtension || !allowedTypes.includes(fileExtension)) {
|
|
88
|
+
errorHandler(new Error('不支持的文件类型。允许的类型:PDF、DOC、DOCX、TXT、JPG、PNG'));
|
|
89
|
+
return { cancel: async () => true };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 模拟 API 上传
|
|
93
|
+
const formData = new FormData();
|
|
94
|
+
formData.append('file', file);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
cancel: async () => {
|
|
98
|
+
// 取消上传的实现
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const confirmRemoval = async (file: AttachmentFile) => {
|
|
105
|
+
return confirm(`确定要删除"${file.name}"吗?`);
|
|
106
|
+
};
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
<div class="document-upload">
|
|
110
|
+
<label>项目文档</label>
|
|
111
|
+
<AttachmentFilesField
|
|
112
|
+
bind:files={documents}
|
|
113
|
+
variant="outlined"
|
|
114
|
+
uploadFile={uploadDocument}
|
|
115
|
+
removeFileConfirm={confirmRemoval}
|
|
116
|
+
style="margin-top: 8px;"
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## API 参考
|
|
122
|
+
|
|
123
|
+
### 属性
|
|
124
|
+
|
|
125
|
+
| 属性 | 类型 | 默认值 | 描述 |
|
|
126
|
+
|------|------|--------|------|
|
|
127
|
+
| `files` | `Array<AttachmentFile>` | `[]` | 附件文件数组 |
|
|
128
|
+
| `variant` | `'' \| 'plain' \| 'outlined' \| 'filled'` | `''` | 视觉变体 |
|
|
129
|
+
| `disabled` | `boolean` | `false` | 字段是否禁用 |
|
|
130
|
+
| `readonly` | `boolean` | `false` | 字段是否只读 |
|
|
131
|
+
| `style` | `string` | `''` | 附加CSS样式 |
|
|
132
|
+
| `displayMode` | `DisplayMode` | `DisplayMode.Edit` | 显示模式(编辑/查看) |
|
|
133
|
+
| `removeFileConfirm` | `RemoveConfirm` | `null` | 文件删除的可选确认函数 |
|
|
134
|
+
| `uploadFile` | `FileUpload` | 必需 | 处理文件上传的函数 |
|
|
135
|
+
| `text` | `string \| null` | `null` | 显示文本(从文件名自动生成) |
|
|
136
|
+
|
|
137
|
+
### 类型定义
|
|
138
|
+
|
|
139
|
+
#### AttachmentFile 接口
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
interface AttachmentFile {
|
|
143
|
+
name: string; // 文件名
|
|
144
|
+
type: FileType; // 文件类型分类
|
|
145
|
+
uri: string; // 文件URL或路径
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
#### FileType 枚举
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
enum FileType {
|
|
153
|
+
IMAGE = 'img', // 图片
|
|
154
|
+
PDF = 'pdf', // PDF文档
|
|
155
|
+
DOC = 'doc', // Word文档
|
|
156
|
+
XLS = 'xml', // Excel表格
|
|
157
|
+
PPT = 'ppt', // PowerPoint演示文稿
|
|
158
|
+
AUDIO = 'wav', // 音频文件
|
|
159
|
+
VIDEO = 'mov', // 视频文件
|
|
160
|
+
OTHER = 'dat' // 其他文件
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
#### FileUpload 函数类型
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
type FileUpload = (
|
|
168
|
+
file: File,
|
|
169
|
+
progressUpdate: ProgressUpdate,
|
|
170
|
+
onUploaded: OnUploaded,
|
|
171
|
+
errorHandler: ErrorHandler
|
|
172
|
+
) => UploadProgress;
|
|
173
|
+
|
|
174
|
+
type ProgressUpdate = (uploadBytes: number) => void;
|
|
175
|
+
type OnUploaded = (url: string, thumbnail?: string) => void;
|
|
176
|
+
type ErrorHandler = (e: Error) => void;
|
|
177
|
+
|
|
178
|
+
interface UploadProgress {
|
|
179
|
+
cancel: () => Promise<boolean>;
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
#### RemoveConfirm 函数类型
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
type RemoveConfirm = (file: AttachmentFile) => Promise<boolean>;
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## 高级示例
|
|
190
|
+
|
|
191
|
+
### 文档管理系统
|
|
192
|
+
|
|
193
|
+
```svelte
|
|
194
|
+
<script>
|
|
195
|
+
import AttachmentFilesField, { DisplayMode } from '@ticatec/uniface-element/AttachmentFilesField';
|
|
196
|
+
import type { AttachmentFile, FileUpload } from '@ticatec/uniface-element/AttachmentFilesField';
|
|
197
|
+
|
|
198
|
+
let projectFiles: AttachmentFile[] = [
|
|
199
|
+
{
|
|
200
|
+
name: "项目提案.pdf",
|
|
201
|
+
type: FileType.PDF,
|
|
202
|
+
uri: "/documents/proposal.pdf"
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: "需求文档.docx",
|
|
206
|
+
type: FileType.DOC,
|
|
207
|
+
uri: "/documents/requirements.docx"
|
|
208
|
+
}
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
let isEditMode = true;
|
|
212
|
+
let uploadProgress = new Map<string, number>();
|
|
213
|
+
|
|
214
|
+
const uploadToServer: FileUpload = async (file, progressUpdate, onUploaded, errorHandler) => {
|
|
215
|
+
try {
|
|
216
|
+
const formData = new FormData();
|
|
217
|
+
formData.append('file', file);
|
|
218
|
+
formData.append('category', 'project-documents');
|
|
219
|
+
|
|
220
|
+
const xhr = new XMLHttpRequest();
|
|
221
|
+
|
|
222
|
+
xhr.upload.addEventListener('progress', (e) => {
|
|
223
|
+
if (e.lengthComputable) {
|
|
224
|
+
progressUpdate(e.loaded);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
xhr.addEventListener('load', () => {
|
|
229
|
+
if (xhr.status === 200) {
|
|
230
|
+
const response = JSON.parse(xhr.responseText);
|
|
231
|
+
onUploaded(response.url, response.thumbnail);
|
|
232
|
+
} else {
|
|
233
|
+
errorHandler(new Error(`上传失败: ${xhr.statusText}`));
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
xhr.addEventListener('error', () => {
|
|
238
|
+
errorHandler(new Error('上传过程中发生网络错误'));
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
xhr.open('POST', '/api/upload');
|
|
242
|
+
xhr.send(formData);
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
cancel: async () => {
|
|
246
|
+
xhr.abort();
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
} catch (error) {
|
|
251
|
+
errorHandler(error as Error);
|
|
252
|
+
return { cancel: async () => true };
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const confirmFileRemoval = async (file: AttachmentFile) => {
|
|
257
|
+
const result = await showCustomDialog({
|
|
258
|
+
title: '删除文件',
|
|
259
|
+
message: `确定要删除"${file.name}"吗?此操作无法撤销。`,
|
|
260
|
+
buttons: ['取消', '删除'],
|
|
261
|
+
type: 'warning'
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
return result === '删除';
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const showCustomDialog = (options: any) => {
|
|
268
|
+
return new Promise((resolve) => {
|
|
269
|
+
// 自定义对话框的实现
|
|
270
|
+
const confirmed = confirm(`${options.title}: ${options.message}`);
|
|
271
|
+
resolve(confirmed ? '删除' : '取消');
|
|
272
|
+
});
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const toggleEditMode = () => {
|
|
276
|
+
isEditMode = !isEditMode;
|
|
277
|
+
};
|
|
278
|
+
</script>
|
|
279
|
+
|
|
280
|
+
<div class="document-manager">
|
|
281
|
+
<div class="manager-header">
|
|
282
|
+
<h3>项目文档</h3>
|
|
283
|
+
<button on:click={toggleEditMode}>
|
|
284
|
+
{isEditMode ? '查看模式' : '编辑模式'}
|
|
285
|
+
</button>
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
<AttachmentFilesField
|
|
289
|
+
bind:files={projectFiles}
|
|
290
|
+
variant="outlined"
|
|
291
|
+
displayMode={isEditMode ? DisplayMode.Edit : DisplayMode.View}
|
|
292
|
+
uploadFile={uploadToServer}
|
|
293
|
+
removeFileConfirm={confirmFileRemoval}
|
|
294
|
+
style="margin-top: 15px; min-height: 120px;"
|
|
295
|
+
/>
|
|
296
|
+
|
|
297
|
+
<div class="file-summary">
|
|
298
|
+
<p>文件总数: {projectFiles.length}</p>
|
|
299
|
+
<p>类型: {[...new Set(projectFiles.map(f => f.type))].join(', ')}</p>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
|
|
303
|
+
<style>
|
|
304
|
+
.document-manager {
|
|
305
|
+
max-width: 600px;
|
|
306
|
+
margin: 20px 0;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.manager-header {
|
|
310
|
+
display: flex;
|
|
311
|
+
justify-content: space-between;
|
|
312
|
+
align-items: center;
|
|
313
|
+
margin-bottom: 10px;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.file-summary {
|
|
317
|
+
margin-top: 10px;
|
|
318
|
+
padding: 10px;
|
|
319
|
+
background: #f5f5f5;
|
|
320
|
+
border-radius: 4px;
|
|
321
|
+
font-size: 0.9em;
|
|
322
|
+
color: #666;
|
|
323
|
+
}
|
|
324
|
+
</style>
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### 带附件的表单
|
|
328
|
+
|
|
329
|
+
```svelte
|
|
330
|
+
<script>
|
|
331
|
+
import AttachmentFilesField from '@ticatec/uniface-element/AttachmentFilesField';
|
|
332
|
+
import type { AttachmentFile, FileUpload } from '@ticatec/uniface-element/AttachmentFilesField';
|
|
333
|
+
|
|
334
|
+
let formData = {
|
|
335
|
+
title: '',
|
|
336
|
+
description: '',
|
|
337
|
+
attachments: [] as AttachmentFile[]
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
let errors = {};
|
|
341
|
+
let isSubmitting = false;
|
|
342
|
+
|
|
343
|
+
const uploadFile: FileUpload = (file, progressUpdate, onUploaded, errorHandler) => {
|
|
344
|
+
// 文件大小限制:5MB
|
|
345
|
+
if (file.size > 5 * 1024 * 1024) {
|
|
346
|
+
errorHandler(new Error('文件大小必须小于 5MB'));
|
|
347
|
+
return { cancel: async () => true };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// 模拟上传过程
|
|
351
|
+
let uploaded = 0;
|
|
352
|
+
const total = file.size;
|
|
353
|
+
|
|
354
|
+
const interval = setInterval(() => {
|
|
355
|
+
uploaded += Math.min(1024 * 50, total - uploaded);
|
|
356
|
+
progressUpdate(uploaded);
|
|
357
|
+
|
|
358
|
+
if (uploaded >= total) {
|
|
359
|
+
clearInterval(interval);
|
|
360
|
+
setTimeout(() => {
|
|
361
|
+
const mockUrl = `/files/${Date.now()}_${file.name}`;
|
|
362
|
+
onUploaded(mockUrl);
|
|
363
|
+
}, 200);
|
|
364
|
+
}
|
|
365
|
+
}, 50);
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
cancel: async () => {
|
|
369
|
+
clearInterval(interval);
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const validateForm = () => {
|
|
376
|
+
errors = {};
|
|
377
|
+
|
|
378
|
+
if (!formData.title.trim()) {
|
|
379
|
+
errors.title = '标题是必填项';
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (!formData.description.trim()) {
|
|
383
|
+
errors.description = '描述是必填项';
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (formData.attachments.length === 0) {
|
|
387
|
+
errors.attachments = '至少需要一个附件';
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return Object.keys(errors).length === 0;
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const submitForm = async () => {
|
|
394
|
+
if (!validateForm()) return;
|
|
395
|
+
|
|
396
|
+
isSubmitting = true;
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
// 模拟表单提交
|
|
400
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
401
|
+
|
|
402
|
+
alert('表单提交成功!');
|
|
403
|
+
formData = { title: '', description: '', attachments: [] };
|
|
404
|
+
} catch (error) {
|
|
405
|
+
alert('提交失败,请重试。');
|
|
406
|
+
} finally {
|
|
407
|
+
isSubmitting = false;
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const confirmRemoval = async (file: AttachmentFile) => {
|
|
412
|
+
return confirm(`从附件中删除"${file.name}"?`);
|
|
413
|
+
};
|
|
414
|
+
</script>
|
|
415
|
+
|
|
416
|
+
<form class="attachment-form" on:submit|preventDefault={submitForm}>
|
|
417
|
+
<div class="form-group">
|
|
418
|
+
<label for="title">标题 *</label>
|
|
419
|
+
<input
|
|
420
|
+
id="title"
|
|
421
|
+
type="text"
|
|
422
|
+
bind:value={formData.title}
|
|
423
|
+
disabled={isSubmitting}
|
|
424
|
+
class:error={errors.title}
|
|
425
|
+
/>
|
|
426
|
+
{#if errors.title}
|
|
427
|
+
<div class="error-message">{errors.title}</div>
|
|
428
|
+
{/if}
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
<div class="form-group">
|
|
432
|
+
<label for="description">描述 *</label>
|
|
433
|
+
<textarea
|
|
434
|
+
id="description"
|
|
435
|
+
rows="4"
|
|
436
|
+
bind:value={formData.description}
|
|
437
|
+
disabled={isSubmitting}
|
|
438
|
+
class:error={errors.description}
|
|
439
|
+
></textarea>
|
|
440
|
+
{#if errors.description}
|
|
441
|
+
<div class="error-message">{errors.description}</div>
|
|
442
|
+
{/if}
|
|
443
|
+
</div>
|
|
444
|
+
|
|
445
|
+
<div class="form-group">
|
|
446
|
+
<label>附件 *</label>
|
|
447
|
+
<AttachmentFilesField
|
|
448
|
+
bind:files={formData.attachments}
|
|
449
|
+
variant="outlined"
|
|
450
|
+
{uploadFile}
|
|
451
|
+
removeFileConfirm={confirmRemoval}
|
|
452
|
+
disabled={isSubmitting}
|
|
453
|
+
/>
|
|
454
|
+
{#if errors.attachments}
|
|
455
|
+
<div class="error-message">{errors.attachments}</div>
|
|
456
|
+
{/if}
|
|
457
|
+
</div>
|
|
458
|
+
|
|
459
|
+
<div class="form-actions">
|
|
460
|
+
<button
|
|
461
|
+
type="button"
|
|
462
|
+
on:click={() => formData = { title: '', description: '', attachments: [] }}
|
|
463
|
+
disabled={isSubmitting}
|
|
464
|
+
>
|
|
465
|
+
清除
|
|
466
|
+
</button>
|
|
467
|
+
<button
|
|
468
|
+
type="submit"
|
|
469
|
+
disabled={isSubmitting}
|
|
470
|
+
class="primary"
|
|
471
|
+
>
|
|
472
|
+
{isSubmitting ? '提交中...' : '提交'}
|
|
473
|
+
</button>
|
|
474
|
+
</div>
|
|
475
|
+
</form>
|
|
476
|
+
|
|
477
|
+
<style>
|
|
478
|
+
.attachment-form {
|
|
479
|
+
max-width: 600px;
|
|
480
|
+
margin: 0 auto;
|
|
481
|
+
padding: 20px;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
.form-group {
|
|
485
|
+
margin-bottom: 20px;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.form-group label {
|
|
489
|
+
display: block;
|
|
490
|
+
margin-bottom: 5px;
|
|
491
|
+
font-weight: bold;
|
|
492
|
+
color: #333;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.form-group input, .form-group textarea {
|
|
496
|
+
width: 100%;
|
|
497
|
+
padding: 10px;
|
|
498
|
+
border: 1px solid #ddd;
|
|
499
|
+
border-radius: 4px;
|
|
500
|
+
font-size: 14px;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
.form-group input.error, .form-group textarea.error {
|
|
504
|
+
border-color: #d32f2f;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
.error-message {
|
|
508
|
+
color: #d32f2f;
|
|
509
|
+
font-size: 0.875em;
|
|
510
|
+
margin-top: 4px;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.form-actions {
|
|
514
|
+
display: flex;
|
|
515
|
+
gap: 10px;
|
|
516
|
+
justify-content: flex-end;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
button {
|
|
520
|
+
padding: 10px 20px;
|
|
521
|
+
border: 1px solid #ddd;
|
|
522
|
+
border-radius: 4px;
|
|
523
|
+
cursor: pointer;
|
|
524
|
+
font-size: 14px;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
button.primary {
|
|
528
|
+
background: #1976d2;
|
|
529
|
+
color: white;
|
|
530
|
+
border-color: #1976d2;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
button:disabled {
|
|
534
|
+
opacity: 0.6;
|
|
535
|
+
cursor: not-allowed;
|
|
536
|
+
}
|
|
537
|
+
</style>
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### 多分类文件管理器
|
|
541
|
+
|
|
542
|
+
```svelte
|
|
543
|
+
<script>
|
|
544
|
+
import AttachmentFilesField, { DisplayMode } from '@ticatec/uniface-element/AttachmentFilesField';
|
|
545
|
+
import type { AttachmentFile, FileUpload } from '@ticatec/uniface-element/AttachmentFilesField';
|
|
546
|
+
|
|
547
|
+
let fileCategories = {
|
|
548
|
+
documents: [] as AttachmentFile[],
|
|
549
|
+
images: [] as AttachmentFile[],
|
|
550
|
+
archives: [] as AttachmentFile[]
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
let activeCategory = 'documents';
|
|
554
|
+
|
|
555
|
+
const categoryConfigs = {
|
|
556
|
+
documents: {
|
|
557
|
+
title: '文档',
|
|
558
|
+
accept: ['.pdf', '.doc', '.docx', '.txt', '.rtf'],
|
|
559
|
+
maxSize: 10 * 1024 * 1024, // 10MB
|
|
560
|
+
description: 'PDF、DOC、DOCX、TXT、RTF 文件,最大 10MB'
|
|
561
|
+
},
|
|
562
|
+
images: {
|
|
563
|
+
title: '图片',
|
|
564
|
+
accept: ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'],
|
|
565
|
+
maxSize: 5 * 1024 * 1024, // 5MB
|
|
566
|
+
description: '图片文件,最大 5MB'
|
|
567
|
+
},
|
|
568
|
+
archives: {
|
|
569
|
+
title: '压缩包',
|
|
570
|
+
accept: ['.zip', '.rar', '.7z', '.tar', '.gz'],
|
|
571
|
+
maxSize: 50 * 1024 * 1024, // 50MB
|
|
572
|
+
description: '压缩包文件,最大 50MB'
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
const createUploadFunction = (category: string): FileUpload => {
|
|
577
|
+
return (file, progressUpdate, onUploaded, errorHandler) => {
|
|
578
|
+
const config = categoryConfigs[category];
|
|
579
|
+
|
|
580
|
+
// 验证文件大小
|
|
581
|
+
if (file.size > config.maxSize) {
|
|
582
|
+
const maxSizeMB = Math.round(config.maxSize / (1024 * 1024));
|
|
583
|
+
errorHandler(new Error(`文件大小超过 ${maxSizeMB}MB 限制`));
|
|
584
|
+
return { cancel: async () => true };
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// 验证文件类型
|
|
588
|
+
const fileExt = '.' + file.name.split('.').pop()?.toLowerCase();
|
|
589
|
+
if (!config.accept.includes(fileExt)) {
|
|
590
|
+
errorHandler(new Error(`不支持的文件类型。允许的类型:${config.accept.join(', ')}`));
|
|
591
|
+
return { cancel: async () => true };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// 模拟分类上传
|
|
595
|
+
let uploaded = 0;
|
|
596
|
+
const total = file.size;
|
|
597
|
+
|
|
598
|
+
const interval = setInterval(() => {
|
|
599
|
+
uploaded += Math.min(1024 * 100, total - uploaded);
|
|
600
|
+
progressUpdate(uploaded);
|
|
601
|
+
|
|
602
|
+
if (uploaded >= total) {
|
|
603
|
+
clearInterval(interval);
|
|
604
|
+
setTimeout(() => {
|
|
605
|
+
const mockUrl = `/files/${category}/${Date.now()}_${file.name}`;
|
|
606
|
+
onUploaded(mockUrl);
|
|
607
|
+
}, 300);
|
|
608
|
+
}
|
|
609
|
+
}, 100);
|
|
610
|
+
|
|
611
|
+
return {
|
|
612
|
+
cancel: async () => {
|
|
613
|
+
clearInterval(interval);
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
};
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
const confirmRemoval = async (file: AttachmentFile) => {
|
|
621
|
+
return confirm(`从${categoryConfigs[activeCategory].title.toLowerCase()}中删除"${file.name}"?`);
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
const getTotalFiles = () => {
|
|
625
|
+
return Object.values(fileCategories).reduce((sum, files) => sum + files.length, 0);
|
|
626
|
+
};
|
|
627
|
+
</script>
|
|
628
|
+
|
|
629
|
+
<div class="multi-category-manager">
|
|
630
|
+
<div class="category-tabs">
|
|
631
|
+
{#each Object.keys(categoryConfigs) as category}
|
|
632
|
+
<button
|
|
633
|
+
class="tab"
|
|
634
|
+
class:active={activeCategory === category}
|
|
635
|
+
on:click={() => activeCategory = category}
|
|
636
|
+
>
|
|
637
|
+
{categoryConfigs[category].title}
|
|
638
|
+
({fileCategories[category].length})
|
|
639
|
+
</button>
|
|
640
|
+
{/each}
|
|
641
|
+
</div>
|
|
642
|
+
|
|
643
|
+
<div class="category-content">
|
|
644
|
+
<div class="category-header">
|
|
645
|
+
<h3>{categoryConfigs[activeCategory].title}</h3>
|
|
646
|
+
<p class="category-description">{categoryConfigs[activeCategory].description}</p>
|
|
647
|
+
</div>
|
|
648
|
+
|
|
649
|
+
<AttachmentFilesField
|
|
650
|
+
bind:files={fileCategories[activeCategory]}
|
|
651
|
+
variant="outlined"
|
|
652
|
+
uploadFile={createUploadFunction(activeCategory)}
|
|
653
|
+
removeFileConfirm={confirmRemoval}
|
|
654
|
+
style="min-height: 150px;"
|
|
655
|
+
/>
|
|
656
|
+
</div>
|
|
657
|
+
|
|
658
|
+
<div class="summary">
|
|
659
|
+
<p>所有分类的文件总数: {getTotalFiles()}</p>
|
|
660
|
+
</div>
|
|
661
|
+
</div>
|
|
662
|
+
|
|
663
|
+
<style>
|
|
664
|
+
.multi-category-manager {
|
|
665
|
+
max-width: 800px;
|
|
666
|
+
margin: 20px auto;
|
|
667
|
+
border: 1px solid #e0e0e0;
|
|
668
|
+
border-radius: 8px;
|
|
669
|
+
overflow: hidden;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
.category-tabs {
|
|
673
|
+
display: flex;
|
|
674
|
+
background: #f5f5f5;
|
|
675
|
+
border-bottom: 1px solid #e0e0e0;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
.tab {
|
|
679
|
+
flex: 1;
|
|
680
|
+
padding: 15px 20px;
|
|
681
|
+
border: none;
|
|
682
|
+
background: transparent;
|
|
683
|
+
cursor: pointer;
|
|
684
|
+
transition: background-color 0.2s;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
.tab:hover {
|
|
688
|
+
background: #e8e8e8;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
.tab.active {
|
|
692
|
+
background: white;
|
|
693
|
+
border-bottom: 2px solid #1976d2;
|
|
694
|
+
color: #1976d2;
|
|
695
|
+
font-weight: bold;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
.category-content {
|
|
699
|
+
padding: 20px;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
.category-header {
|
|
703
|
+
margin-bottom: 15px;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
.category-header h3 {
|
|
707
|
+
margin: 0 0 5px 0;
|
|
708
|
+
color: #333;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
.category-description {
|
|
712
|
+
margin: 0;
|
|
713
|
+
font-size: 0.9em;
|
|
714
|
+
color: #666;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
.summary {
|
|
718
|
+
padding: 15px 20px;
|
|
719
|
+
background: #f9f9f9;
|
|
720
|
+
border-top: 1px solid #e0e0e0;
|
|
721
|
+
font-size: 0.9em;
|
|
722
|
+
color: #666;
|
|
723
|
+
}
|
|
724
|
+
</style>
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
## 最佳实践
|
|
728
|
+
|
|
729
|
+
1. **文件验证**:始终在上传前验证文件大小和类型
|
|
730
|
+
2. **进度反馈**:在上传过程中提供清晰的进度指示器
|
|
731
|
+
3. **错误处理**:实施全面的错误处理以应对网络问题
|
|
732
|
+
4. **确认对话框**:对破坏性操作使用确认对话框
|
|
733
|
+
5. **无障碍访问**:确保适当的ARIA标签和键盘导航
|
|
734
|
+
6. **性能优化**:对大型文件列表考虑延迟加载
|
|
735
|
+
7. **安全性**:在服务器端也要验证文件
|
|
736
|
+
8. **用户体验**:明确提供文件限制和要求的反馈
|
|
737
|
+
|
|
738
|
+
## 样式说明
|
|
739
|
+
|
|
740
|
+
- 组件支持标准变体样式(plain、outlined、filled)
|
|
741
|
+
- 文件图标根据文件类型自动选择
|
|
742
|
+
- 上传进度通过进度条可视化显示
|
|
743
|
+
- 自定义样式可以通过`style`属性应用
|
|
744
|
+
|
|
745
|
+
## 无障碍访问注意事项
|
|
746
|
+
|
|
747
|
+
- 上传按钮支持键盘访问
|
|
748
|
+
- 文件删除按钮包含适当的ARIA标签
|
|
749
|
+
- 屏幕阅读器可以导航文件列表
|
|
750
|
+
- 进度更新会通知辅助技术
|
|
751
|
+
- 颜色对比度符合无障碍标准
|
|
752
|
+
|
|
753
|
+
## 浏览器支持
|
|
754
|
+
|
|
755
|
+
- 支持File API的现代浏览器
|
|
756
|
+
- 拖拽功能需要HTML5支持
|
|
757
|
+
- 进度跟踪在支持XMLHttpRequest Level 2的浏览器中工作
|