@ticatec/uniface-flexi-module 0.0.2
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/FLEXICRITERIASET_GUIDE.md +1559 -0
- package/FLEXICRITERIASET_GUIDE_CN.md +1133 -0
- package/FLEXIDATATABLE_GUIDE.md +1650 -0
- package/FLEXIDATATABLE_GUIDE_CN.md +1650 -0
- package/FLEXIFORM_GUIDE.md +1068 -0
- package/FLEXIFORM_GUIDE_CN.md +1068 -0
- package/FLEXI_CONTEXT_GUIDE_CN.md +172 -0
- package/MODULE_LOADER_CN.md +228 -0
- package/README.md +307 -0
- package/README_CN.md +51 -0
- package/SANDBOX_CN.md +201 -0
- package/dist/FlexiContext.d.ts +28 -0
- package/dist/FlexiContext.js +45 -0
- package/dist/ModuleLoader.d.ts +41 -0
- package/dist/ModuleLoader.js +55 -0
- package/dist/Sandbox.d.ts +33 -0
- package/dist/Sandbox.js +101 -0
- package/dist/criteria-panel/CriteriaFieldsPanel.svelte +26 -0
- package/dist/criteria-panel/CriteriaFieldsPanel.svelte.d.ts +22 -0
- package/dist/criteria-panel/components/CascadeSelectSearchField.svelte +10 -0
- package/dist/criteria-panel/components/CascadeSelectSearchField.svelte.d.ts +25 -0
- package/dist/criteria-panel/components/DateRangeField.svelte +11 -0
- package/dist/criteria-panel/components/DateRangeField.svelte.d.ts +25 -0
- package/dist/criteria-panel/components/DateSearchField.svelte +10 -0
- package/dist/criteria-panel/components/DateSearchField.svelte.d.ts +24 -0
- package/dist/criteria-panel/components/DateTimeSearchField.svelte +10 -0
- package/dist/criteria-panel/components/DateTimeSearchField.svelte.d.ts +24 -0
- package/dist/criteria-panel/components/InputOptionSelectSearchField.svelte +9 -0
- package/dist/criteria-panel/components/InputOptionSelectSearchField.svelte.d.ts +24 -0
- package/dist/criteria-panel/components/NumberRangeField.svelte +11 -0
- package/dist/criteria-panel/components/NumberRangeField.svelte.d.ts +25 -0
- package/dist/criteria-panel/components/NumberSearchField.svelte +9 -0
- package/dist/criteria-panel/components/NumberSearchField.svelte.d.ts +24 -0
- package/dist/criteria-panel/components/OptionMultiSelectSearchField.svelte +9 -0
- package/dist/criteria-panel/components/OptionMultiSelectSearchField.svelte.d.ts +24 -0
- package/dist/criteria-panel/components/OptionSelectSearchField.svelte +9 -0
- package/dist/criteria-panel/components/OptionSelectSearchField.svelte.d.ts +24 -0
- package/dist/criteria-panel/components/SearchField.svelte +14 -0
- package/dist/criteria-panel/components/SearchField.svelte.d.ts +33 -0
- package/dist/criteria-panel/components/TextSearchField.svelte +9 -0
- package/dist/criteria-panel/components/TextSearchField.svelte.d.ts +24 -0
- package/dist/criteria-panel/components/UnknownCriteriaField.svelte +9 -0
- package/dist/criteria-panel/components/UnknownCriteriaField.svelte.d.ts +24 -0
- package/dist/criteria-panel/index.d.ts +6 -0
- package/dist/criteria-panel/index.js +6 -0
- package/dist/criteria-panel/lib/CriteriaComponentBuilder.d.ts +19 -0
- package/dist/criteria-panel/lib/CriteriaComponentBuilder.js +31 -0
- package/dist/criteria-panel/lib/CriteriaFieldBuilder.d.ts +1 -0
- package/dist/criteria-panel/lib/CriteriaFieldBuilder.js +127 -0
- package/dist/criteria-panel/lib/FlexiCriteriaField.d.ts +38 -0
- package/dist/criteria-panel/lib/FlexiCriteriaField.js +31 -0
- package/dist/criteria-panel/lib/FlexiCriteriaSet.d.ts +24 -0
- package/dist/criteria-panel/lib/FlexiCriteriaSet.js +48 -0
- package/dist/flexi-datatable/FlexiDataTable.d.ts +111 -0
- package/dist/flexi-datatable/FlexiDataTable.js +90 -0
- package/dist/flexi-datatable/index.d.ts +2 -0
- package/dist/flexi-datatable/index.js +2 -0
- package/dist/flexi-form/FlexiCompound.d.ts +34 -0
- package/dist/flexi-form/FlexiCompound.js +84 -0
- package/dist/flexi-form/FlexiFormDialog.svelte +24 -0
- package/dist/flexi-form/FlexiFormDialog.svelte.d.ts +21 -0
- package/dist/flexi-form/FlexiFormPage.svelte +26 -0
- package/dist/flexi-form/FlexiFormPage.svelte.d.ts +25 -0
- package/dist/flexi-form/Schema.d.ts +6 -0
- package/dist/flexi-form/Schema.js +1 -0
- package/dist/flexi-form/components/BreakLine.svelte +1 -0
- package/dist/flexi-form/components/BreakLine.svelte.d.ts +26 -0
- package/dist/flexi-form/components/CardTitleBar.svelte +18 -0
- package/dist/flexi-form/components/CardTitleBar.svelte.d.ts +22 -0
- package/dist/flexi-form/components/CascadeOptionSelectField.svelte +13 -0
- package/dist/flexi-form/components/CascadeOptionSelectField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/CellFieldBuilder.d.ts +1 -0
- package/dist/flexi-form/components/CellFieldBuilder.js +178 -0
- package/dist/flexi-form/components/DateField.svelte +12 -0
- package/dist/flexi-form/components/DateField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/DateTimeField.svelte +13 -0
- package/dist/flexi-form/components/DateTimeField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/InputOptionSelectField.svelte +13 -0
- package/dist/flexi-form/components/InputOptionSelectField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/MemoField.svelte +12 -0
- package/dist/flexi-form/components/MemoField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/NumberField.svelte +12 -0
- package/dist/flexi-form/components/NumberField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/OptionsMultiSelectField.svelte +13 -0
- package/dist/flexi-form/components/OptionsMultiSelectField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/OptionsSelectField.svelte +13 -0
- package/dist/flexi-form/components/OptionsSelectField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/TextField.svelte +12 -0
- package/dist/flexi-form/components/TextField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/UnitNumberField.svelte +12 -0
- package/dist/flexi-form/components/UnitNumberField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/UnknownTypeField.svelte +5 -0
- package/dist/flexi-form/components/UnknownTypeField.svelte.d.ts +18 -0
- package/dist/flexi-form/containers/FlexiPanel.svelte +13 -0
- package/dist/flexi-form/containers/FlexiPanel.svelte.d.ts +33 -0
- package/dist/flexi-form/flexi_card/FlexiCard.d.ts +64 -0
- package/dist/flexi-form/flexi_card/FlexiCard.js +66 -0
- package/dist/flexi-form/flexi_card/FlexiCardPanel.svelte +57 -0
- package/dist/flexi-form/flexi_card/FlexiCardPanel.svelte.d.ts +22 -0
- package/dist/flexi-form/flexi_composite/FlexiComposite.d.ts +50 -0
- package/dist/flexi-form/flexi_composite/FlexiComposite.js +26 -0
- package/dist/flexi-form/flexi_composite/FlexiCompositePanel.svelte +42 -0
- package/dist/flexi-form/flexi_composite/FlexiCompositePanel.svelte.d.ts +25 -0
- package/dist/flexi-form/flexi_composite/README.md +50 -0
- package/dist/flexi-form/flexi_datasheet/FlexiDataSheet.d.ts +4 -0
- package/dist/flexi-form/flexi_datasheet/FlexiDataSheet.js +2 -0
- package/dist/flexi-form/flexi_field/FlexiField.d.ts +76 -0
- package/dist/flexi-form/flexi_field/FlexiField.js +128 -0
- package/dist/flexi-form/flexi_field/FlexiFieldCell.svelte +35 -0
- package/dist/flexi-form/flexi_field/FlexiFieldCell.svelte.d.ts +25 -0
- package/dist/flexi-form/flexi_field/UnknownField.d.ts +3 -0
- package/dist/flexi-form/flexi_field/UnknownField.js +3 -0
- package/dist/flexi-form/flexi_form/FlexiForm.d.ts +127 -0
- package/dist/flexi-form/flexi_form/FlexiForm.js +160 -0
- package/dist/flexi-form/flexi_form/FlexiFormPanel.svelte +57 -0
- package/dist/flexi-form/flexi_form/FlexiFormPanel.svelte.d.ts +25 -0
- package/dist/flexi-form/index.d.ts +11 -0
- package/dist/flexi-form/index.js +11 -0
- package/dist/flexi-form/lib/ComponentBuilder.d.ts +15 -0
- package/dist/flexi-form/lib/ComponentBuilder.js +31 -0
- package/dist/flexi-form/lib/index.d.ts +5 -0
- package/dist/flexi-form/lib/index.js +2 -0
- package/dist/flexi-form/lib/types.d.ts +7 -0
- package/dist/flexi-form/lib/types.js +6 -0
- package/dist/flexi-form/lib/utils.d.ts +10 -0
- package/dist/flexi-form/lib/utils.js +48 -0
- package/dist/i18n-res/i18nRes.d.ts +2 -0
- package/dist/i18n-res/i18nRes.js +8 -0
- package/dist/i18n-res/index.d.ts +2 -0
- package/dist/i18n-res/index.js +2 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/uniface-flexi-module.css +46 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.js +8 -0
- package/package.json +135 -0
|
@@ -0,0 +1,1650 @@
|
|
|
1
|
+
# FlexiDataTable 完整使用指南
|
|
2
|
+
|
|
3
|
+
在 Svelte 应用中构建强大、灵活数据表的 FlexiDataTable 综合指南。
|
|
4
|
+
|
|
5
|
+
## 目录
|
|
6
|
+
|
|
7
|
+
1. [概述](#概述)
|
|
8
|
+
2. [基础用法](#基础用法)
|
|
9
|
+
3. [Schema 结构](#schema-结构)
|
|
10
|
+
4. [列类型和功能](#列类型和功能)
|
|
11
|
+
5. [高级表格功能](#高级表格功能)
|
|
12
|
+
6. [动态数据处理](#动态数据处理)
|
|
13
|
+
7. [事件和交互](#事件和交互)
|
|
14
|
+
8. [扩展 FlexiDataTable](#扩展-flexidatatable)
|
|
15
|
+
9. [集成模式](#集成模式)
|
|
16
|
+
10. [最佳实践](#最佳实践)
|
|
17
|
+
|
|
18
|
+
## 概述
|
|
19
|
+
|
|
20
|
+
FlexiDataTable 是一个强大的数据表组件,提供灵活的列配置、排序、过滤、分页和广泛的自定义选项。它构建在 uniface-element DataTable 基础上,具有增强的基于 schema 的配置功能。
|
|
21
|
+
|
|
22
|
+
### 主要特性
|
|
23
|
+
|
|
24
|
+
- **基于 Schema 的配置**:通过 JSON 配置定义表格
|
|
25
|
+
- **灵活的列类型**:数据列、指示列和操作列
|
|
26
|
+
- **自定义格式化器**:内置和自定义单元格格式化器
|
|
27
|
+
- **排序和过滤**:高级数据操作
|
|
28
|
+
- **行操作**:可配置的行级操作
|
|
29
|
+
- **响应式设计**:适应不同屏幕尺寸的自适应布局
|
|
30
|
+
- **可扩展架构**:自定义渲染器和格式化器
|
|
31
|
+
|
|
32
|
+
## 基础用法
|
|
33
|
+
|
|
34
|
+
### 1. 安装和设置
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import FlexiDataTable from '@ticatec/uniface-flexi-form/flexi-datatable';
|
|
38
|
+
import DataTable from '@ticatec/uniface-element/DataTable';
|
|
39
|
+
import '@ticatec/uniface-flexi-form/uniface-flexi-form.css';
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2. 简单数据表
|
|
43
|
+
|
|
44
|
+
```svelte
|
|
45
|
+
<!-- SimpleDataTable.svelte -->
|
|
46
|
+
<script lang="ts">
|
|
47
|
+
import DataTable from '@ticatec/uniface-element/DataTable';
|
|
48
|
+
import FlexiDataTable from '@ticatec/uniface-flexi-form/flexi-datatable';
|
|
49
|
+
|
|
50
|
+
// 示例数据
|
|
51
|
+
let tableData = [
|
|
52
|
+
{ id: 1, name: '张三', email: 'zhang@example.com', age: 30, status: 'active' },
|
|
53
|
+
{ id: 2, name: '李四', email: 'li@example.com', age: 25, status: 'active' },
|
|
54
|
+
{ id: 3, name: '王五', email: 'wang@example.com', age: 35, status: 'inactive' }
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
// 表格配置
|
|
58
|
+
const tableSchema = {
|
|
59
|
+
round: true,
|
|
60
|
+
indicatorColumn: {
|
|
61
|
+
width: 60,
|
|
62
|
+
displayNo: true,
|
|
63
|
+
selectable: true
|
|
64
|
+
},
|
|
65
|
+
actionsColumn: {
|
|
66
|
+
width: 120,
|
|
67
|
+
align: 'center',
|
|
68
|
+
getActions: 'getRowActions'
|
|
69
|
+
},
|
|
70
|
+
columns: [
|
|
71
|
+
{
|
|
72
|
+
text: '姓名',
|
|
73
|
+
field: 'name',
|
|
74
|
+
width: 150,
|
|
75
|
+
align: 'left',
|
|
76
|
+
resizable: true
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
text: '邮箱',
|
|
80
|
+
field: 'email',
|
|
81
|
+
width: 200,
|
|
82
|
+
align: 'left',
|
|
83
|
+
resizable: true
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
text: '年龄',
|
|
87
|
+
field: 'age',
|
|
88
|
+
width: 80,
|
|
89
|
+
align: 'center',
|
|
90
|
+
formatter: 'formatAge'
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
text: '状态',
|
|
94
|
+
field: 'status',
|
|
95
|
+
width: 100,
|
|
96
|
+
align: 'center',
|
|
97
|
+
formatter: 'formatStatus'
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// 创建 FlexiDataTable 实例
|
|
103
|
+
class UserDataTable extends FlexiDataTable {
|
|
104
|
+
constructor(schema) {
|
|
105
|
+
super(schema);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
formatAge(value, row) {
|
|
109
|
+
return `${value} 岁`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
formatStatus(value, row) {
|
|
113
|
+
const statusClass = value === 'active' ? 'status-active' : 'status-inactive';
|
|
114
|
+
return `<span class="${statusClass}">${value === 'active' ? '激活' : '未激活'}</span>`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
getRowActions(row) {
|
|
118
|
+
return [
|
|
119
|
+
{
|
|
120
|
+
text: '编辑',
|
|
121
|
+
icon: 'edit',
|
|
122
|
+
handler: () => this.editUser(row)
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
text: '删除',
|
|
126
|
+
icon: 'delete',
|
|
127
|
+
handler: () => this.deleteUser(row),
|
|
128
|
+
confirm: '确定要删除这个用户吗?'
|
|
129
|
+
}
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
editUser(row) {
|
|
134
|
+
console.log('编辑用户:', row);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
deleteUser(row) {
|
|
138
|
+
console.log('删除用户:', row);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let dataTable = new UserDataTable(tableSchema);
|
|
143
|
+
|
|
144
|
+
onMount(async () => {
|
|
145
|
+
await dataTable.initialize();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
function handleRowSelect(event) {
|
|
149
|
+
console.log('选中的行:', event.detail.selectedRows);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function handleSort(event) {
|
|
153
|
+
console.log('排序改变:', event.detail);
|
|
154
|
+
}
|
|
155
|
+
</script>
|
|
156
|
+
|
|
157
|
+
<div class="data-table-container">
|
|
158
|
+
<DataTable
|
|
159
|
+
columns={dataTable.columns}
|
|
160
|
+
data={tableData}
|
|
161
|
+
indicatorColumn={dataTable.indicatorColumn}
|
|
162
|
+
actionsColumn={dataTable.actionsColumn}
|
|
163
|
+
round={dataTable.round}
|
|
164
|
+
on:rowSelect={handleRowSelect}
|
|
165
|
+
on:sort={handleSort}
|
|
166
|
+
/>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<style>
|
|
170
|
+
.data-table-container {
|
|
171
|
+
padding: 16px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
:global(.status-active) {
|
|
175
|
+
color: #28a745;
|
|
176
|
+
font-weight: bold;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
:global(.status-inactive) {
|
|
180
|
+
color: #dc3545;
|
|
181
|
+
font-weight: bold;
|
|
182
|
+
}
|
|
183
|
+
</style>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Schema 结构
|
|
187
|
+
|
|
188
|
+
### FlexiDataTableSchema
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
interface FlexiDataTableSchema {
|
|
192
|
+
round?: boolean; // 圆角边框
|
|
193
|
+
indicatorColumn: IndicatorColumnSchema; // 行号/选择
|
|
194
|
+
actionsColumn?: ActionsColumnSchema; // 行操作
|
|
195
|
+
columns: Array<FlexiDataTableColumnSchema>; // 数据列
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### FlexiDataTableColumnSchema
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
interface FlexiDataTableColumnSchema {
|
|
203
|
+
text: string; // 列标题文本
|
|
204
|
+
field?: string; // 数据字段名
|
|
205
|
+
frozen?: boolean; // 冻结列
|
|
206
|
+
align?: 'left' | 'center' | 'right'; // 文本对齐
|
|
207
|
+
width: number; // 列宽度
|
|
208
|
+
minWidth?: number; // 最小宽度
|
|
209
|
+
warp?: boolean; // 文本换行
|
|
210
|
+
formatter?: string; // 格式化器函数名
|
|
211
|
+
escapeHTML?: boolean; // HTML转义
|
|
212
|
+
href?: string; // 链接函数名
|
|
213
|
+
hint?: string; // 提示函数名
|
|
214
|
+
render?: string; // 自定义渲染器名
|
|
215
|
+
visible?: boolean; // 列可见性
|
|
216
|
+
resizable?: boolean; // 列大小调整
|
|
217
|
+
compareFunction?: string; // 自定义排序函数名
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### IndicatorColumnSchema
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
interface IndicatorColumnSchema {
|
|
225
|
+
width: number; // 列宽度
|
|
226
|
+
displayNo?: boolean; // 显示行号
|
|
227
|
+
selectable?: boolean; // 行选择
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### ActionsColumnSchema
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
interface ActionsColumnSchema {
|
|
235
|
+
width: number; // 列宽度
|
|
236
|
+
align?: 'left' | 'center'; // 对齐方式
|
|
237
|
+
getActions: string; // 操作提供者函数名
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## 列类型和功能
|
|
242
|
+
|
|
243
|
+
### 数据列
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
// 基础数据列
|
|
247
|
+
{
|
|
248
|
+
text: '产品名称',
|
|
249
|
+
field: 'name',
|
|
250
|
+
width: 200,
|
|
251
|
+
align: 'left',
|
|
252
|
+
resizable: true
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 格式化列
|
|
256
|
+
{
|
|
257
|
+
text: '价格',
|
|
258
|
+
field: 'price',
|
|
259
|
+
width: 100,
|
|
260
|
+
align: 'right',
|
|
261
|
+
formatter: 'formatCurrency'
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 链接列
|
|
265
|
+
{
|
|
266
|
+
text: '网站',
|
|
267
|
+
field: 'website',
|
|
268
|
+
width: 150,
|
|
269
|
+
href: 'buildWebsiteLink'
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 自定义渲染列
|
|
273
|
+
{
|
|
274
|
+
text: '状态',
|
|
275
|
+
field: 'status',
|
|
276
|
+
width: 120,
|
|
277
|
+
render: 'renderStatusBadge'
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### 带实现的列示例
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
class ProductDataTable extends FlexiDataTable {
|
|
285
|
+
constructor(schema: FlexiDataTableSchema) {
|
|
286
|
+
super(schema);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 货币格式化器
|
|
290
|
+
formatCurrency(value: number, row: any): string {
|
|
291
|
+
return new Intl.NumberFormat('zh-CN', {
|
|
292
|
+
style: 'currency',
|
|
293
|
+
currency: 'CNY'
|
|
294
|
+
}).format(value);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// 日期格式化器
|
|
298
|
+
formatDate(value: string, row: any): string {
|
|
299
|
+
return new Date(value).toLocaleDateString('zh-CN');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// 百分比格式化器
|
|
303
|
+
formatPercentage(value: number, row: any): string {
|
|
304
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 文件大小格式化器
|
|
308
|
+
formatFileSize(bytes: number, row: any): string {
|
|
309
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
310
|
+
if (bytes === 0) return '0 B';
|
|
311
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
312
|
+
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// 链接构建器
|
|
316
|
+
buildWebsiteLink(value: string, row: any): string {
|
|
317
|
+
return value.startsWith('http') ? value : `https://${value}`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// 产品链接构建器
|
|
321
|
+
buildProductLink(value: string, row: any): string {
|
|
322
|
+
return `/products/${row.id}`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// 提示提供者
|
|
326
|
+
getProductHint(value: string, row: any): string {
|
|
327
|
+
return `产品ID: ${row.id}\\n分类: ${row.category}\\n库存: ${row.stock}`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// 状态徽章渲染器
|
|
331
|
+
renderStatusBadge(value: string, row: any): any {
|
|
332
|
+
// 返回 Svelte 组件配置
|
|
333
|
+
return {
|
|
334
|
+
component: StatusBadge,
|
|
335
|
+
props: {
|
|
336
|
+
status: value,
|
|
337
|
+
size: 'small'
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// 图片渲染器
|
|
343
|
+
renderProductImage(value: string, row: any): any {
|
|
344
|
+
return {
|
|
345
|
+
component: ProductImage,
|
|
346
|
+
props: {
|
|
347
|
+
src: value,
|
|
348
|
+
alt: row.name,
|
|
349
|
+
size: 'thumbnail'
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// 自定义排序函数
|
|
355
|
+
compareProductNames(a: any, b: any): number {
|
|
356
|
+
// 产品名称的自定义排序逻辑
|
|
357
|
+
const nameA = a.name.toLowerCase();
|
|
358
|
+
const nameB = b.name.toLowerCase();
|
|
359
|
+
return this.compareStrings(nameA, nameB, { ignoreCase: true });
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// 带空值处理的数字排序
|
|
363
|
+
compareStock(a: any, b: any): number {
|
|
364
|
+
const stockA = a.stock || 0;
|
|
365
|
+
const stockB = b.stock || 0;
|
|
366
|
+
return stockA - stockB;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// 行操作
|
|
370
|
+
getRowActions(row: any): any[] {
|
|
371
|
+
const actions = [
|
|
372
|
+
{
|
|
373
|
+
text: '查看',
|
|
374
|
+
icon: 'eye',
|
|
375
|
+
handler: () => this.viewProduct(row)
|
|
376
|
+
}
|
|
377
|
+
];
|
|
378
|
+
|
|
379
|
+
if (row.status === 'active') {
|
|
380
|
+
actions.push({
|
|
381
|
+
text: '编辑',
|
|
382
|
+
icon: 'edit',
|
|
383
|
+
handler: () => this.editProduct(row)
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (row.stock === 0) {
|
|
388
|
+
actions.push({
|
|
389
|
+
text: '补货',
|
|
390
|
+
icon: 'plus',
|
|
391
|
+
handler: () => this.restockProduct(row)
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
actions.push({
|
|
396
|
+
text: '删除',
|
|
397
|
+
icon: 'trash',
|
|
398
|
+
handler: () => this.deleteProduct(row),
|
|
399
|
+
confirm: '确定要删除这个产品吗?',
|
|
400
|
+
style: 'danger'
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
return actions;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
viewProduct(row: any): void {
|
|
407
|
+
window.open(`/products/${row.id}`, '_blank');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
editProduct(row: any): void {
|
|
411
|
+
// 导航到编辑页面或打开模态框
|
|
412
|
+
console.log('编辑产品:', row);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
restockProduct(row: any): void {
|
|
416
|
+
// 打开补货对话框
|
|
417
|
+
console.log('补货产品:', row);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
deleteProduct(row: any): void {
|
|
421
|
+
// 执行删除操作
|
|
422
|
+
console.log('删除产品:', row);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
## 高级表格功能
|
|
428
|
+
|
|
429
|
+
### 冻结列
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
const tableSchemaWithFrozenColumns = {
|
|
433
|
+
round: true,
|
|
434
|
+
indicatorColumn: {
|
|
435
|
+
width: 60,
|
|
436
|
+
displayNo: true,
|
|
437
|
+
selectable: true
|
|
438
|
+
},
|
|
439
|
+
columns: [
|
|
440
|
+
{
|
|
441
|
+
text: 'ID',
|
|
442
|
+
field: 'id',
|
|
443
|
+
width: 80,
|
|
444
|
+
frozen: true, // 此列保持固定
|
|
445
|
+
align: 'center'
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
text: '名称',
|
|
449
|
+
field: 'name',
|
|
450
|
+
width: 150,
|
|
451
|
+
frozen: true, // 此列也保持固定
|
|
452
|
+
align: 'left'
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
text: '描述',
|
|
456
|
+
field: 'description',
|
|
457
|
+
width: 300,
|
|
458
|
+
align: 'left',
|
|
459
|
+
warp: true // 允许文本换行
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
text: '价格',
|
|
463
|
+
field: 'price',
|
|
464
|
+
width: 100,
|
|
465
|
+
align: 'right',
|
|
466
|
+
formatter: 'formatCurrency'
|
|
467
|
+
}
|
|
468
|
+
]
|
|
469
|
+
};
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### 条件列可见性
|
|
473
|
+
|
|
474
|
+
```typescript
|
|
475
|
+
class ConditionalColumnsTable extends FlexiDataTable {
|
|
476
|
+
private userRole: string;
|
|
477
|
+
|
|
478
|
+
constructor(schema: FlexiDataTableSchema, userRole: string) {
|
|
479
|
+
super(schema);
|
|
480
|
+
this.userRole = userRole;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async initialize(): Promise<void> {
|
|
484
|
+
// 在初始化之前根据用户角色修改 schema
|
|
485
|
+
this.applyRoleBasedVisibility();
|
|
486
|
+
await super.initialize();
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private applyRoleBasedVisibility(): void {
|
|
490
|
+
this.schema.columns.forEach(column => {
|
|
491
|
+
switch (column.field) {
|
|
492
|
+
case 'salary':
|
|
493
|
+
column.visible = this.userRole === 'admin' || this.userRole === 'hr';
|
|
494
|
+
break;
|
|
495
|
+
case 'ssn':
|
|
496
|
+
column.visible = this.userRole === 'admin';
|
|
497
|
+
break;
|
|
498
|
+
case 'internalNotes':
|
|
499
|
+
column.visible = this.userRole !== 'guest';
|
|
500
|
+
break;
|
|
501
|
+
default:
|
|
502
|
+
column.visible = column.visible !== false;
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
### 动态列配置
|
|
510
|
+
|
|
511
|
+
```typescript
|
|
512
|
+
class DynamicColumnsTable extends FlexiDataTable {
|
|
513
|
+
constructor(schema: FlexiDataTableSchema) {
|
|
514
|
+
super(schema);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
updateColumnConfiguration(newConfig: any): void {
|
|
518
|
+
// 更新列宽度
|
|
519
|
+
if (newConfig.columnWidths) {
|
|
520
|
+
this.columns.forEach((column, index) => {
|
|
521
|
+
if (newConfig.columnWidths[index]) {
|
|
522
|
+
column.width = newConfig.columnWidths[index];
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// 更新列可见性
|
|
528
|
+
if (newConfig.visibleColumns) {
|
|
529
|
+
this.columns.forEach((column, index) => {
|
|
530
|
+
column.visible = newConfig.visibleColumns.includes(index);
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// 更新列顺序
|
|
535
|
+
if (newConfig.columnOrder) {
|
|
536
|
+
const reorderedColumns = newConfig.columnOrder.map(index => this.columns[index]);
|
|
537
|
+
this.#columns = reorderedColumns;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
getColumnConfiguration(): any {
|
|
542
|
+
return {
|
|
543
|
+
columnWidths: this.columns.map(col => col.width),
|
|
544
|
+
visibleColumns: this.columns
|
|
545
|
+
.map((col, index) => col.visible !== false ? index : null)
|
|
546
|
+
.filter(index => index !== null),
|
|
547
|
+
columnOrder: this.columns.map((_, index) => index)
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
saveColumnConfiguration(): void {
|
|
552
|
+
const config = this.getColumnConfiguration();
|
|
553
|
+
localStorage.setItem('table-config', JSON.stringify(config));
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
loadColumnConfiguration(): void {
|
|
557
|
+
try {
|
|
558
|
+
const config = JSON.parse(localStorage.getItem('table-config') || '{}');
|
|
559
|
+
this.updateColumnConfiguration(config);
|
|
560
|
+
} catch (error) {
|
|
561
|
+
console.error('加载列配置失败:', error);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
## 动态数据处理
|
|
568
|
+
|
|
569
|
+
### 分页数据加载
|
|
570
|
+
|
|
571
|
+
```typescript
|
|
572
|
+
class PaginatedDataTable extends FlexiDataTable {
|
|
573
|
+
private currentPage = 1;
|
|
574
|
+
private pageSize = 20;
|
|
575
|
+
private totalRecords = 0;
|
|
576
|
+
private loading = false;
|
|
577
|
+
private searchCriteria: any = {};
|
|
578
|
+
|
|
579
|
+
constructor(schema: FlexiDataTableSchema) {
|
|
580
|
+
super(schema);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async loadData(page: number = 1, criteria: any = {}): Promise<any[]> {
|
|
584
|
+
this.loading = true;
|
|
585
|
+
this.currentPage = page;
|
|
586
|
+
this.searchCriteria = criteria;
|
|
587
|
+
|
|
588
|
+
try {
|
|
589
|
+
const queryParams = new URLSearchParams({
|
|
590
|
+
page: page.toString(),
|
|
591
|
+
limit: this.pageSize.toString(),
|
|
592
|
+
...this.flattenCriteria(criteria)
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
const response = await fetch(`/api/data?${queryParams}`);
|
|
596
|
+
const result = await response.json();
|
|
597
|
+
|
|
598
|
+
this.totalRecords = result.total;
|
|
599
|
+
return result.data;
|
|
600
|
+
} catch (error) {
|
|
601
|
+
console.error('加载数据失败:', error);
|
|
602
|
+
return [];
|
|
603
|
+
} finally {
|
|
604
|
+
this.loading = false;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async refreshData(): Promise<any[]> {
|
|
609
|
+
return this.loadData(this.currentPage, this.searchCriteria);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
async nextPage(): Promise<any[]> {
|
|
613
|
+
if (this.hasNextPage()) {
|
|
614
|
+
return this.loadData(this.currentPage + 1, this.searchCriteria);
|
|
615
|
+
}
|
|
616
|
+
return [];
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
async previousPage(): Promise<any[]> {
|
|
620
|
+
if (this.hasPreviousPage()) {
|
|
621
|
+
return this.loadData(this.currentPage - 1, this.searchCriteria);
|
|
622
|
+
}
|
|
623
|
+
return [];
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
hasNextPage(): boolean {
|
|
627
|
+
return this.currentPage * this.pageSize < this.totalRecords;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
hasPreviousPage(): boolean {
|
|
631
|
+
return this.currentPage > 1;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
getCurrentPageInfo(): any {
|
|
635
|
+
const startRecord = (this.currentPage - 1) * this.pageSize + 1;
|
|
636
|
+
const endRecord = Math.min(this.currentPage * this.pageSize, this.totalRecords);
|
|
637
|
+
|
|
638
|
+
return {
|
|
639
|
+
currentPage: this.currentPage,
|
|
640
|
+
pageSize: this.pageSize,
|
|
641
|
+
totalRecords: this.totalRecords,
|
|
642
|
+
totalPages: Math.ceil(this.totalRecords / this.pageSize),
|
|
643
|
+
startRecord,
|
|
644
|
+
endRecord,
|
|
645
|
+
loading: this.loading
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
private flattenCriteria(criteria: any): Record<string, string> {
|
|
650
|
+
const flattened: Record<string, string> = {};
|
|
651
|
+
for (const [key, value] of Object.entries(criteria)) {
|
|
652
|
+
if (value !== null && value !== undefined && value !== '') {
|
|
653
|
+
flattened[key] = String(value);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return flattened;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
### 实时数据更新
|
|
662
|
+
|
|
663
|
+
```typescript
|
|
664
|
+
class RealtimeDataTable extends FlexiDataTable {
|
|
665
|
+
private websocket: WebSocket | null = null;
|
|
666
|
+
private updateCallback: ((data: any[]) => void) | null = null;
|
|
667
|
+
|
|
668
|
+
constructor(schema: FlexiDataTableSchema) {
|
|
669
|
+
super(schema);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
connectRealtime(wsUrl: string, onUpdate: (data: any[]) => void): void {
|
|
673
|
+
this.updateCallback = onUpdate;
|
|
674
|
+
this.websocket = new WebSocket(wsUrl);
|
|
675
|
+
|
|
676
|
+
this.websocket.onopen = () => {
|
|
677
|
+
console.log('WebSocket 已连接');
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
this.websocket.onmessage = (event) => {
|
|
681
|
+
try {
|
|
682
|
+
const message = JSON.parse(event.data);
|
|
683
|
+
this.handleRealtimeUpdate(message);
|
|
684
|
+
} catch (error) {
|
|
685
|
+
console.error('解析 WebSocket 消息失败:', error);
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
this.websocket.onclose = () => {
|
|
690
|
+
console.log('WebSocket 已断开');
|
|
691
|
+
// 尝试重新连接
|
|
692
|
+
setTimeout(() => this.connectRealtime(wsUrl, onUpdate), 5000);
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
this.websocket.onerror = (error) => {
|
|
696
|
+
console.error('WebSocket 错误:', error);
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
private handleRealtimeUpdate(message: any): void {
|
|
701
|
+
switch (message.type) {
|
|
702
|
+
case 'data_updated':
|
|
703
|
+
this.updateCallback?.(message.data);
|
|
704
|
+
break;
|
|
705
|
+
case 'record_added':
|
|
706
|
+
this.handleRecordAdded(message.record);
|
|
707
|
+
break;
|
|
708
|
+
case 'record_updated':
|
|
709
|
+
this.handleRecordUpdated(message.record);
|
|
710
|
+
break;
|
|
711
|
+
case 'record_deleted':
|
|
712
|
+
this.handleRecordDeleted(message.recordId);
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
private handleRecordAdded(record: any): void {
|
|
718
|
+
// 将记录添加到当前数据并通知
|
|
719
|
+
console.log('新记录添加:', record);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
private handleRecordUpdated(record: any): void {
|
|
723
|
+
// 更新现有记录并通知
|
|
724
|
+
console.log('记录更新:', record);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
private handleRecordDeleted(recordId: string): void {
|
|
728
|
+
// 从当前数据中移除记录并通知
|
|
729
|
+
console.log('记录删除:', recordId);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
disconnect(): void {
|
|
733
|
+
if (this.websocket) {
|
|
734
|
+
this.websocket.close();
|
|
735
|
+
this.websocket = null;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
## 事件和交互
|
|
742
|
+
|
|
743
|
+
### 表格事件
|
|
744
|
+
|
|
745
|
+
```svelte
|
|
746
|
+
<script>
|
|
747
|
+
function handleRowSelect(event) {
|
|
748
|
+
const { selectedRows, isAllSelected } = event.detail;
|
|
749
|
+
console.log('选中的行:', selectedRows);
|
|
750
|
+
console.log('全部选中:', isAllSelected);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function handleRowClick(event) {
|
|
754
|
+
const { row, rowIndex } = event.detail;
|
|
755
|
+
console.log('点击的行:', row, '索引:', rowIndex);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function handleRowDoubleClick(event) {
|
|
759
|
+
const { row, rowIndex } = event.detail;
|
|
760
|
+
console.log('双击的行:', row);
|
|
761
|
+
// 打开编辑对话框或导航到详情视图
|
|
762
|
+
openEditDialog(row);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function handleSort(event) {
|
|
766
|
+
const { column, direction } = event.detail;
|
|
767
|
+
console.log('排序改变:', column.field, direction);
|
|
768
|
+
|
|
769
|
+
// 执行排序
|
|
770
|
+
if (direction === 'asc') {
|
|
771
|
+
tableData.sort((a, b) => a[column.field] > b[column.field] ? 1 : -1);
|
|
772
|
+
} else if (direction === 'desc') {
|
|
773
|
+
tableData.sort((a, b) => a[column.field] < b[column.field] ? 1 : -1);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// 触发响应性
|
|
777
|
+
tableData = [...tableData];
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function handleColumnResize(event) {
|
|
781
|
+
const { column, newWidth } = event.detail;
|
|
782
|
+
console.log('列调整大小:', column.field, '新宽度:', newWidth);
|
|
783
|
+
|
|
784
|
+
// 保存列配置
|
|
785
|
+
dataTable.saveColumnConfiguration();
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function handlePageChange(event) {
|
|
789
|
+
const { page, pageSize } = event.detail;
|
|
790
|
+
console.log('页面改变:', page, '大小:', pageSize);
|
|
791
|
+
|
|
792
|
+
// 加载新页面数据
|
|
793
|
+
loadPageData(page, pageSize);
|
|
794
|
+
}
|
|
795
|
+
</script>
|
|
796
|
+
|
|
797
|
+
<DataTable
|
|
798
|
+
columns={dataTable.columns}
|
|
799
|
+
data={tableData}
|
|
800
|
+
indicatorColumn={dataTable.indicatorColumn}
|
|
801
|
+
actionsColumn={dataTable.actionsColumn}
|
|
802
|
+
on:rowSelect={handleRowSelect}
|
|
803
|
+
on:rowClick={handleRowClick}
|
|
804
|
+
on:rowDoubleClick={handleRowDoubleClick}
|
|
805
|
+
on:sort={handleSort}
|
|
806
|
+
on:columnResize={handleColumnResize}
|
|
807
|
+
on:pageChange={handlePageChange}
|
|
808
|
+
/>
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
### 批量操作
|
|
812
|
+
|
|
813
|
+
```typescript
|
|
814
|
+
class BulkOperationsTable extends FlexiDataTable {
|
|
815
|
+
private selectedRows: Set<any> = new Set();
|
|
816
|
+
|
|
817
|
+
constructor(schema: FlexiDataTableSchema) {
|
|
818
|
+
super(schema);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
handleRowSelection(selectedRows: any[]): void {
|
|
822
|
+
this.selectedRows = new Set(selectedRows);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
getBulkActions(): any[] {
|
|
826
|
+
if (this.selectedRows.size === 0) {
|
|
827
|
+
return [];
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
return [
|
|
831
|
+
{
|
|
832
|
+
text: `删除 ${this.selectedRows.size} 项`,
|
|
833
|
+
icon: 'trash',
|
|
834
|
+
handler: () => this.bulkDelete(),
|
|
835
|
+
confirm: `确定要删除 ${this.selectedRows.size} 项吗?`,
|
|
836
|
+
style: 'danger'
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
text: `导出 ${this.selectedRows.size} 项`,
|
|
840
|
+
icon: 'download',
|
|
841
|
+
handler: () => this.bulkExport()
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
text: '更新状态',
|
|
845
|
+
icon: 'edit',
|
|
846
|
+
handler: () => this.bulkUpdateStatus()
|
|
847
|
+
}
|
|
848
|
+
];
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
async bulkDelete(): Promise<void> {
|
|
852
|
+
try {
|
|
853
|
+
const ids = Array.from(this.selectedRows).map(row => row.id);
|
|
854
|
+
await fetch('/api/bulk/delete', {
|
|
855
|
+
method: 'POST',
|
|
856
|
+
headers: { 'Content-Type': 'application/json' },
|
|
857
|
+
body: JSON.stringify({ ids })
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
console.log('批量删除成功');
|
|
861
|
+
this.selectedRows.clear();
|
|
862
|
+
} catch (error) {
|
|
863
|
+
console.error('批量删除失败:', error);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
async bulkExport(): Promise<void> {
|
|
868
|
+
try {
|
|
869
|
+
const data = Array.from(this.selectedRows);
|
|
870
|
+
const csv = this.convertToCSV(data);
|
|
871
|
+
this.downloadCSV(csv, 'export.csv');
|
|
872
|
+
} catch (error) {
|
|
873
|
+
console.error('批量导出失败:', error);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
async bulkUpdateStatus(): Promise<void> {
|
|
878
|
+
const newStatus = prompt('请输入新状态:');
|
|
879
|
+
if (!newStatus) return;
|
|
880
|
+
|
|
881
|
+
try {
|
|
882
|
+
const ids = Array.from(this.selectedRows).map(row => row.id);
|
|
883
|
+
await fetch('/api/bulk/update-status', {
|
|
884
|
+
method: 'POST',
|
|
885
|
+
headers: { 'Content-Type': 'application/json' },
|
|
886
|
+
body: JSON.stringify({ ids, status: newStatus })
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
console.log('批量状态更新成功');
|
|
890
|
+
} catch (error) {
|
|
891
|
+
console.error('批量状态更新失败:', error);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
private convertToCSV(data: any[]): string {
|
|
896
|
+
if (data.length === 0) return '';
|
|
897
|
+
|
|
898
|
+
const headers = Object.keys(data[0]);
|
|
899
|
+
const csvContent = [
|
|
900
|
+
headers.join(','),
|
|
901
|
+
...data.map(row => headers.map(header => `"${row[header]}"`).join(','))
|
|
902
|
+
].join('\\n');
|
|
903
|
+
|
|
904
|
+
return csvContent;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
private downloadCSV(csv: string, filename: string): void {
|
|
908
|
+
const blob = new Blob([csv], { type: 'text/csv' });
|
|
909
|
+
const url = window.URL.createObjectURL(blob);
|
|
910
|
+
const link = document.createElement('a');
|
|
911
|
+
link.href = url;
|
|
912
|
+
link.download = filename;
|
|
913
|
+
link.click();
|
|
914
|
+
window.URL.revokeObjectURL(url);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
## 扩展 FlexiDataTable
|
|
920
|
+
|
|
921
|
+
### 自定义单元格渲染器
|
|
922
|
+
|
|
923
|
+
```svelte
|
|
924
|
+
<!-- StatusBadge.svelte -->
|
|
925
|
+
<script lang="ts">
|
|
926
|
+
export let status: string;
|
|
927
|
+
export let size: 'small' | 'medium' | 'large' = 'medium';
|
|
928
|
+
|
|
929
|
+
$: statusConfig = getStatusConfig(status);
|
|
930
|
+
|
|
931
|
+
function getStatusConfig(status: string) {
|
|
932
|
+
const configs = {
|
|
933
|
+
active: { color: '#28a745', background: '#d4edda', text: '激活' },
|
|
934
|
+
inactive: { color: '#6c757d', background: '#e2e3e5', text: '未激活' },
|
|
935
|
+
pending: { color: '#ffc107', background: '#fff3cd', text: '待处理' },
|
|
936
|
+
error: { color: '#dc3545', background: '#f8d7da', text: '错误' }
|
|
937
|
+
};
|
|
938
|
+
return configs[status] || configs.inactive;
|
|
939
|
+
}
|
|
940
|
+
</script>
|
|
941
|
+
|
|
942
|
+
<span
|
|
943
|
+
class="status-badge {size}"
|
|
944
|
+
style="color: {statusConfig.color}; background-color: {statusConfig.background};"
|
|
945
|
+
>
|
|
946
|
+
{statusConfig.text}
|
|
947
|
+
</span>
|
|
948
|
+
|
|
949
|
+
<style>
|
|
950
|
+
.status-badge {
|
|
951
|
+
display: inline-block;
|
|
952
|
+
padding: 4px 8px;
|
|
953
|
+
border-radius: 12px;
|
|
954
|
+
font-weight: 600;
|
|
955
|
+
text-align: center;
|
|
956
|
+
white-space: nowrap;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
.status-badge.small {
|
|
960
|
+
font-size: 0.75rem;
|
|
961
|
+
padding: 2px 6px;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
.status-badge.medium {
|
|
965
|
+
font-size: 0.875rem;
|
|
966
|
+
padding: 4px 8px;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
.status-badge.large {
|
|
970
|
+
font-size: 1rem;
|
|
971
|
+
padding: 6px 12px;
|
|
972
|
+
}
|
|
973
|
+
</style>
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
```svelte
|
|
977
|
+
<!-- ProgressBar.svelte -->
|
|
978
|
+
<script lang="ts">
|
|
979
|
+
export let value: number;
|
|
980
|
+
export let max: number = 100;
|
|
981
|
+
export let color: string = '#007bff';
|
|
982
|
+
export let showText: boolean = true;
|
|
983
|
+
|
|
984
|
+
$: percentage = Math.round((value / max) * 100);
|
|
985
|
+
</script>
|
|
986
|
+
|
|
987
|
+
<div class="progress-container">
|
|
988
|
+
<div class="progress-bar">
|
|
989
|
+
<div
|
|
990
|
+
class="progress-fill"
|
|
991
|
+
style="width: {percentage}%; background-color: {color};"
|
|
992
|
+
></div>
|
|
993
|
+
</div>
|
|
994
|
+
{#if showText}
|
|
995
|
+
<span class="progress-text">{percentage}%</span>
|
|
996
|
+
{/if}
|
|
997
|
+
</div>
|
|
998
|
+
|
|
999
|
+
<style>
|
|
1000
|
+
.progress-container {
|
|
1001
|
+
display: flex;
|
|
1002
|
+
align-items: center;
|
|
1003
|
+
gap: 8px;
|
|
1004
|
+
width: 100%;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
.progress-bar {
|
|
1008
|
+
flex: 1;
|
|
1009
|
+
height: 8px;
|
|
1010
|
+
background-color: #e9ecef;
|
|
1011
|
+
border-radius: 4px;
|
|
1012
|
+
overflow: hidden;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
.progress-fill {
|
|
1016
|
+
height: 100%;
|
|
1017
|
+
transition: width 0.3s ease;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
.progress-text {
|
|
1021
|
+
font-size: 0.75rem;
|
|
1022
|
+
color: #6c757d;
|
|
1023
|
+
min-width: 35px;
|
|
1024
|
+
}
|
|
1025
|
+
</style>
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
### 自定义表格类
|
|
1029
|
+
|
|
1030
|
+
```typescript
|
|
1031
|
+
class AdvancedProductTable extends FlexiDataTable {
|
|
1032
|
+
private productCategories: any[] = [];
|
|
1033
|
+
private userPermissions: string[] = [];
|
|
1034
|
+
|
|
1035
|
+
constructor(schema: FlexiDataTableSchema, userPermissions: string[]) {
|
|
1036
|
+
super(schema);
|
|
1037
|
+
this.userPermissions = userPermissions;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
async initialize(): Promise<void> {
|
|
1041
|
+
await this.loadProductCategories();
|
|
1042
|
+
this.setupPermissionBasedActions();
|
|
1043
|
+
await super.initialize();
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
private async loadProductCategories(): Promise<void> {
|
|
1047
|
+
try {
|
|
1048
|
+
const response = await fetch('/api/product-categories');
|
|
1049
|
+
this.productCategories = await response.json();
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
console.error('加载产品分类失败:', error);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
private setupPermissionBasedActions(): void {
|
|
1056
|
+
// 根据用户权限修改 schema
|
|
1057
|
+
if (!this.userPermissions.includes('edit_products')) {
|
|
1058
|
+
// 移除编辑相关的列或操作
|
|
1059
|
+
this.schema.columns = this.schema.columns.filter(col =>
|
|
1060
|
+
col.field !== 'editActions'
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// 高级格式化器
|
|
1066
|
+
formatProductCategory(categoryId: string, row: any): string {
|
|
1067
|
+
const category = this.productCategories.find(cat => cat.id === categoryId);
|
|
1068
|
+
return category ? category.name : '未知分类';
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
formatInventoryStatus(stock: number, row: any): string {
|
|
1072
|
+
if (stock === 0) {
|
|
1073
|
+
return '<span class="text-danger">缺货</span>';
|
|
1074
|
+
} else if (stock < row.minimumStock) {
|
|
1075
|
+
return '<span class="text-warning">库存不足</span>';
|
|
1076
|
+
} else {
|
|
1077
|
+
return '<span class="text-success">有库存</span>';
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
formatPriceWithDiscount(price: number, row: any): string {
|
|
1082
|
+
if (row.discount > 0) {
|
|
1083
|
+
const discountedPrice = price * (1 - row.discount);
|
|
1084
|
+
return `
|
|
1085
|
+
<span class="original-price">¥${price.toFixed(2)}</span>
|
|
1086
|
+
<span class="discounted-price">¥${discountedPrice.toFixed(2)}</span>
|
|
1087
|
+
`;
|
|
1088
|
+
}
|
|
1089
|
+
return `¥${price.toFixed(2)}`;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// 高级渲染器
|
|
1093
|
+
renderProductImage(imageUrl: string, row: any): any {
|
|
1094
|
+
return {
|
|
1095
|
+
component: ProductImage,
|
|
1096
|
+
props: {
|
|
1097
|
+
src: imageUrl,
|
|
1098
|
+
alt: row.name,
|
|
1099
|
+
size: 'thumbnail',
|
|
1100
|
+
lazy: true,
|
|
1101
|
+
fallback: '/images/product-placeholder.png'
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
renderStockLevel(stock: number, row: any): any {
|
|
1107
|
+
const percentage = Math.min((stock / row.maximumStock) * 100, 100);
|
|
1108
|
+
let color = '#28a745'; // 绿色
|
|
1109
|
+
|
|
1110
|
+
if (percentage < 20) {
|
|
1111
|
+
color = '#dc3545'; // 红色
|
|
1112
|
+
} else if (percentage < 50) {
|
|
1113
|
+
color = '#ffc107'; // 黄色
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
return {
|
|
1117
|
+
component: ProgressBar,
|
|
1118
|
+
props: {
|
|
1119
|
+
value: stock,
|
|
1120
|
+
max: row.maximumStock,
|
|
1121
|
+
color,
|
|
1122
|
+
showText: true
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
renderRating(rating: number, row: any): any {
|
|
1128
|
+
return {
|
|
1129
|
+
component: StarRating,
|
|
1130
|
+
props: {
|
|
1131
|
+
rating,
|
|
1132
|
+
maxRating: 5,
|
|
1133
|
+
readonly: true,
|
|
1134
|
+
size: 'small'
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// 高级操作
|
|
1140
|
+
getRowActions(row: any): any[] {
|
|
1141
|
+
const actions = [];
|
|
1142
|
+
|
|
1143
|
+
// 始终可用的操作
|
|
1144
|
+
actions.push({
|
|
1145
|
+
text: '查看详情',
|
|
1146
|
+
icon: 'eye',
|
|
1147
|
+
handler: () => this.viewProductDetails(row)
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
// 基于权限的操作
|
|
1151
|
+
if (this.userPermissions.includes('edit_products')) {
|
|
1152
|
+
actions.push({
|
|
1153
|
+
text: '编辑',
|
|
1154
|
+
icon: 'edit',
|
|
1155
|
+
handler: () => this.editProduct(row)
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
if (this.userPermissions.includes('manage_inventory')) {
|
|
1160
|
+
if (row.stock === 0) {
|
|
1161
|
+
actions.push({
|
|
1162
|
+
text: '补货',
|
|
1163
|
+
icon: 'plus-circle',
|
|
1164
|
+
handler: () => this.restockProduct(row)
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
actions.push({
|
|
1169
|
+
text: '调整库存',
|
|
1170
|
+
icon: 'warehouse',
|
|
1171
|
+
handler: () => this.adjustStock(row)
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
if (this.userPermissions.includes('delete_products')) {
|
|
1176
|
+
actions.push({
|
|
1177
|
+
text: '删除',
|
|
1178
|
+
icon: 'trash',
|
|
1179
|
+
handler: () => this.deleteProduct(row),
|
|
1180
|
+
confirm: '确定要删除这个产品吗?',
|
|
1181
|
+
style: 'danger'
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
return actions;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// 操作实现
|
|
1189
|
+
viewProductDetails(row: any): void {
|
|
1190
|
+
window.open(`/products/${row.id}`, '_blank');
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
editProduct(row: any): void {
|
|
1194
|
+
// 打开编辑模态框或导航到编辑页面
|
|
1195
|
+
const editEvent = new CustomEvent('edit-product', { detail: row });
|
|
1196
|
+
document.dispatchEvent(editEvent);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
restockProduct(row: any): void {
|
|
1200
|
+
// 打开补货对话框
|
|
1201
|
+
const restockEvent = new CustomEvent('restock-product', { detail: row });
|
|
1202
|
+
document.dispatchEvent(restockEvent);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
adjustStock(row: any): void {
|
|
1206
|
+
// 打开库存调整对话框
|
|
1207
|
+
const adjustEvent = new CustomEvent('adjust-stock', { detail: row });
|
|
1208
|
+
document.dispatchEvent(adjustEvent);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
deleteProduct(row: any): void {
|
|
1212
|
+
// 执行删除操作
|
|
1213
|
+
fetch(`/api/products/${row.id}`, { method: 'DELETE' })
|
|
1214
|
+
.then(() => {
|
|
1215
|
+
const deleteEvent = new CustomEvent('product-deleted', { detail: row });
|
|
1216
|
+
document.dispatchEvent(deleteEvent);
|
|
1217
|
+
})
|
|
1218
|
+
.catch(error => {
|
|
1219
|
+
console.error('删除失败:', error);
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// 高级排序
|
|
1224
|
+
compareProductNames(a: any, b: any): number {
|
|
1225
|
+
return this.compareStrings(a.name, b.name, { ignoreCase: true });
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
compareByCategory(a: any, b: any): number {
|
|
1229
|
+
const catA = this.formatProductCategory(a.categoryId, a);
|
|
1230
|
+
const catB = this.formatProductCategory(b.categoryId, b);
|
|
1231
|
+
return this.compareStrings(catA, catB);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
compareByStockLevel(a: any, b: any): number {
|
|
1235
|
+
const levelA = a.stock / a.maximumStock;
|
|
1236
|
+
const levelB = b.stock / b.maximumStock;
|
|
1237
|
+
return levelA - levelB;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
```
|
|
1241
|
+
|
|
1242
|
+
## 集成模式
|
|
1243
|
+
|
|
1244
|
+
### 与搜索集成的表格
|
|
1245
|
+
|
|
1246
|
+
```svelte
|
|
1247
|
+
<!-- ProductManagement.svelte -->
|
|
1248
|
+
<script lang="ts">
|
|
1249
|
+
import CriteriaPanel from '@ticatec/uniface-flexi-form/criteria-panel';
|
|
1250
|
+
import DataTable from '@ticatec/uniface-element/DataTable';
|
|
1251
|
+
import { AdvancedProductTable } from './AdvancedProductTable';
|
|
1252
|
+
|
|
1253
|
+
let searchCriteria = {};
|
|
1254
|
+
let tableData = [];
|
|
1255
|
+
let totalRecords = 0;
|
|
1256
|
+
let currentPage = 1;
|
|
1257
|
+
let pageSize = 20;
|
|
1258
|
+
let loading = false;
|
|
1259
|
+
|
|
1260
|
+
const searchSchema = {
|
|
1261
|
+
arrangement: 'horizontal',
|
|
1262
|
+
fields: [
|
|
1263
|
+
{
|
|
1264
|
+
type: 'text-search',
|
|
1265
|
+
name: 'name',
|
|
1266
|
+
label: '产品名称',
|
|
1267
|
+
keys: { field: 'name' },
|
|
1268
|
+
size: 'x25'
|
|
1269
|
+
},
|
|
1270
|
+
{
|
|
1271
|
+
type: 'option-select-search',
|
|
1272
|
+
name: 'category',
|
|
1273
|
+
label: '分类',
|
|
1274
|
+
keys: { field: 'categoryId' },
|
|
1275
|
+
dictName: 'categories',
|
|
1276
|
+
size: 'x20'
|
|
1277
|
+
},
|
|
1278
|
+
{
|
|
1279
|
+
type: 'number-range',
|
|
1280
|
+
name: 'price',
|
|
1281
|
+
label: '价格范围',
|
|
1282
|
+
keys: { minField: 'priceMin', maxField: 'priceMax' },
|
|
1283
|
+
size: 'x25'
|
|
1284
|
+
},
|
|
1285
|
+
{
|
|
1286
|
+
type: 'option-select-search',
|
|
1287
|
+
name: 'status',
|
|
1288
|
+
label: '状态',
|
|
1289
|
+
keys: { field: 'status' },
|
|
1290
|
+
dictName: 'product-status',
|
|
1291
|
+
size: 'x15'
|
|
1292
|
+
}
|
|
1293
|
+
]
|
|
1294
|
+
};
|
|
1295
|
+
|
|
1296
|
+
const tableSchema = {
|
|
1297
|
+
round: true,
|
|
1298
|
+
indicatorColumn: {
|
|
1299
|
+
width: 60,
|
|
1300
|
+
displayNo: true,
|
|
1301
|
+
selectable: true
|
|
1302
|
+
},
|
|
1303
|
+
actionsColumn: {
|
|
1304
|
+
width: 150,
|
|
1305
|
+
align: 'center',
|
|
1306
|
+
getActions: 'getRowActions'
|
|
1307
|
+
},
|
|
1308
|
+
columns: [
|
|
1309
|
+
{
|
|
1310
|
+
text: '图片',
|
|
1311
|
+
field: 'imageUrl',
|
|
1312
|
+
width: 80,
|
|
1313
|
+
align: 'center',
|
|
1314
|
+
render: 'renderProductImage'
|
|
1315
|
+
},
|
|
1316
|
+
{
|
|
1317
|
+
text: '名称',
|
|
1318
|
+
field: 'name',
|
|
1319
|
+
width: 200,
|
|
1320
|
+
align: 'left',
|
|
1321
|
+
resizable: true,
|
|
1322
|
+
compareFunction: 'compareProductNames'
|
|
1323
|
+
},
|
|
1324
|
+
{
|
|
1325
|
+
text: '分类',
|
|
1326
|
+
field: 'categoryId',
|
|
1327
|
+
width: 150,
|
|
1328
|
+
formatter: 'formatProductCategory',
|
|
1329
|
+
compareFunction: 'compareByCategory'
|
|
1330
|
+
},
|
|
1331
|
+
{
|
|
1332
|
+
text: '价格',
|
|
1333
|
+
field: 'price',
|
|
1334
|
+
width: 120,
|
|
1335
|
+
align: 'right',
|
|
1336
|
+
formatter: 'formatPriceWithDiscount'
|
|
1337
|
+
},
|
|
1338
|
+
{
|
|
1339
|
+
text: '库存',
|
|
1340
|
+
field: 'stock',
|
|
1341
|
+
width: 100,
|
|
1342
|
+
align: 'center',
|
|
1343
|
+
render: 'renderStockLevel',
|
|
1344
|
+
compareFunction: 'compareByStockLevel'
|
|
1345
|
+
},
|
|
1346
|
+
{
|
|
1347
|
+
text: '评分',
|
|
1348
|
+
field: 'rating',
|
|
1349
|
+
width: 120,
|
|
1350
|
+
align: 'center',
|
|
1351
|
+
render: 'renderRating'
|
|
1352
|
+
},
|
|
1353
|
+
{
|
|
1354
|
+
text: '状态',
|
|
1355
|
+
field: 'status',
|
|
1356
|
+
width: 100,
|
|
1357
|
+
align: 'center',
|
|
1358
|
+
formatter: 'formatInventoryStatus'
|
|
1359
|
+
}
|
|
1360
|
+
]
|
|
1361
|
+
};
|
|
1362
|
+
|
|
1363
|
+
let productTable = new AdvancedProductTable(tableSchema, ['edit_products', 'manage_inventory']);
|
|
1364
|
+
|
|
1365
|
+
onMount(async () => {
|
|
1366
|
+
await productTable.initialize();
|
|
1367
|
+
await loadData();
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1370
|
+
async function loadData() {
|
|
1371
|
+
loading = true;
|
|
1372
|
+
try {
|
|
1373
|
+
const queryParams = new URLSearchParams({
|
|
1374
|
+
page: currentPage.toString(),
|
|
1375
|
+
limit: pageSize.toString(),
|
|
1376
|
+
...flattenCriteria(searchCriteria)
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
const response = await fetch(`/api/products?${queryParams}`);
|
|
1380
|
+
const result = await response.json();
|
|
1381
|
+
|
|
1382
|
+
tableData = result.data;
|
|
1383
|
+
totalRecords = result.total;
|
|
1384
|
+
} catch (error) {
|
|
1385
|
+
console.error('加载产品失败:', error);
|
|
1386
|
+
} finally {
|
|
1387
|
+
loading = false;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
function flattenCriteria(criteria) {
|
|
1392
|
+
const flattened = {};
|
|
1393
|
+
for (const [key, value] of Object.entries(criteria)) {
|
|
1394
|
+
if (value !== null && value !== undefined && value !== '') {
|
|
1395
|
+
flattened[key] = value;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
return flattened;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
function handleCriteriaChange(event) {
|
|
1402
|
+
searchCriteria = event.detail.criteria;
|
|
1403
|
+
currentPage = 1;
|
|
1404
|
+
loadData();
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
function handlePageChange(event) {
|
|
1408
|
+
currentPage = event.detail.page;
|
|
1409
|
+
pageSize = event.detail.pageSize;
|
|
1410
|
+
loadData();
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
function handleSort(event) {
|
|
1414
|
+
console.log('排序:', event.detail);
|
|
1415
|
+
loadData();
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// 监听表格操作的自定义事件
|
|
1419
|
+
onMount(() => {
|
|
1420
|
+
document.addEventListener('edit-product', handleEditProduct);
|
|
1421
|
+
document.addEventListener('restock-product', handleRestockProduct);
|
|
1422
|
+
document.addEventListener('product-deleted', handleProductDeleted);
|
|
1423
|
+
|
|
1424
|
+
return () => {
|
|
1425
|
+
document.removeEventListener('edit-product', handleEditProduct);
|
|
1426
|
+
document.removeEventListener('restock-product', handleRestockProduct);
|
|
1427
|
+
document.removeEventListener('product-deleted', handleProductDeleted);
|
|
1428
|
+
};
|
|
1429
|
+
});
|
|
1430
|
+
|
|
1431
|
+
function handleEditProduct(event) {
|
|
1432
|
+
console.log('编辑产品:', event.detail);
|
|
1433
|
+
// 打开编辑模态框或导航到编辑页面
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
function handleRestockProduct(event) {
|
|
1437
|
+
console.log('补货产品:', event.detail);
|
|
1438
|
+
// 打开补货模态框
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
function handleProductDeleted(event) {
|
|
1442
|
+
console.log('产品已删除:', event.detail);
|
|
1443
|
+
// 刷新表格数据
|
|
1444
|
+
loadData();
|
|
1445
|
+
}
|
|
1446
|
+
</script>
|
|
1447
|
+
|
|
1448
|
+
<div class="product-management">
|
|
1449
|
+
<div class="search-section">
|
|
1450
|
+
<h3>产品搜索</h3>
|
|
1451
|
+
<CriteriaPanel
|
|
1452
|
+
schema={searchSchema}
|
|
1453
|
+
bind:criteria={searchCriteria}
|
|
1454
|
+
on:change={handleCriteriaChange}
|
|
1455
|
+
/>
|
|
1456
|
+
</div>
|
|
1457
|
+
|
|
1458
|
+
<div class="table-section">
|
|
1459
|
+
<div class="table-header">
|
|
1460
|
+
<h3>产品 ({totalRecords} 项)</h3>
|
|
1461
|
+
{#if loading}
|
|
1462
|
+
<div class="loading">加载中...</div>
|
|
1463
|
+
{/if}
|
|
1464
|
+
</div>
|
|
1465
|
+
|
|
1466
|
+
<DataTable
|
|
1467
|
+
columns={productTable.columns}
|
|
1468
|
+
data={tableData}
|
|
1469
|
+
indicatorColumn={productTable.indicatorColumn}
|
|
1470
|
+
actionsColumn={productTable.actionsColumn}
|
|
1471
|
+
round={productTable.round}
|
|
1472
|
+
{totalRecords}
|
|
1473
|
+
{currentPage}
|
|
1474
|
+
{pageSize}
|
|
1475
|
+
on:pageChange={handlePageChange}
|
|
1476
|
+
on:sort={handleSort}
|
|
1477
|
+
/>
|
|
1478
|
+
</div>
|
|
1479
|
+
</div>
|
|
1480
|
+
|
|
1481
|
+
<style>
|
|
1482
|
+
.product-management {
|
|
1483
|
+
display: flex;
|
|
1484
|
+
flex-direction: column;
|
|
1485
|
+
gap: 24px;
|
|
1486
|
+
padding: 16px;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
.search-section {
|
|
1490
|
+
background: #f8f9fa;
|
|
1491
|
+
padding: 16px;
|
|
1492
|
+
border-radius: 8px;
|
|
1493
|
+
border: 1px solid #dee2e6;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
.table-section {
|
|
1497
|
+
flex: 1;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
.table-header {
|
|
1501
|
+
display: flex;
|
|
1502
|
+
justify-content: space-between;
|
|
1503
|
+
align-items: center;
|
|
1504
|
+
margin-bottom: 16px;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
.loading {
|
|
1508
|
+
color: #6c757d;
|
|
1509
|
+
font-style: italic;
|
|
1510
|
+
}
|
|
1511
|
+
</style>
|
|
1512
|
+
```
|
|
1513
|
+
|
|
1514
|
+
## 最佳实践
|
|
1515
|
+
|
|
1516
|
+
### 1. 性能优化
|
|
1517
|
+
|
|
1518
|
+
```typescript
|
|
1519
|
+
// 使用虚拟滚动优化大数据集
|
|
1520
|
+
class VirtualizedTable extends FlexiDataTable {
|
|
1521
|
+
private virtualItemHeight = 40;
|
|
1522
|
+
private visibleItemCount = 20;
|
|
1523
|
+
|
|
1524
|
+
getVirtualizedProps() {
|
|
1525
|
+
return {
|
|
1526
|
+
itemHeight: this.virtualItemHeight,
|
|
1527
|
+
visibleCount: this.visibleItemCount,
|
|
1528
|
+
overscan: 5
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// 防抖搜索和过滤操作
|
|
1534
|
+
class OptimizedTable extends FlexiDataTable {
|
|
1535
|
+
private searchDebounce = 300;
|
|
1536
|
+
|
|
1537
|
+
createDebouncedSearch(searchFn: Function) {
|
|
1538
|
+
let timeoutId: number;
|
|
1539
|
+
return (...args: any[]) => {
|
|
1540
|
+
clearTimeout(timeoutId);
|
|
1541
|
+
timeoutId = setTimeout(() => searchFn(...args), this.searchDebounce);
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
```
|
|
1546
|
+
|
|
1547
|
+
### 2. 可访问性
|
|
1548
|
+
|
|
1549
|
+
```typescript
|
|
1550
|
+
const accessibleTableSchema = {
|
|
1551
|
+
round: true,
|
|
1552
|
+
indicatorColumn: {
|
|
1553
|
+
width: 60,
|
|
1554
|
+
displayNo: true,
|
|
1555
|
+
selectable: true
|
|
1556
|
+
},
|
|
1557
|
+
columns: [
|
|
1558
|
+
{
|
|
1559
|
+
text: '产品名称',
|
|
1560
|
+
field: 'name',
|
|
1561
|
+
width: 200,
|
|
1562
|
+
// 为屏幕阅读器添加 ARIA 属性
|
|
1563
|
+
'aria-label': '产品名称,可排序列',
|
|
1564
|
+
'aria-sort': 'none'
|
|
1565
|
+
}
|
|
1566
|
+
]
|
|
1567
|
+
};
|
|
1568
|
+
```
|
|
1569
|
+
|
|
1570
|
+
### 3. 错误处理
|
|
1571
|
+
|
|
1572
|
+
```typescript
|
|
1573
|
+
class RobustDataTable extends FlexiDataTable {
|
|
1574
|
+
handleError(error: any, context: string): void {
|
|
1575
|
+
console.error(`表格在 ${context} 中发生错误:`, error);
|
|
1576
|
+
|
|
1577
|
+
// 显示用户友好的错误消息
|
|
1578
|
+
this.showErrorMessage(`${context} 失败。请重试。`);
|
|
1579
|
+
|
|
1580
|
+
// 记录错误用于监控
|
|
1581
|
+
this.logError(error, context);
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
private showErrorMessage(message: string): void {
|
|
1585
|
+
// 实现用户通知
|
|
1586
|
+
console.log('用户消息:', message);
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
private logError(error: any, context: string): void {
|
|
1590
|
+
// 发送错误到监控服务
|
|
1591
|
+
fetch('/api/errors', {
|
|
1592
|
+
method: 'POST',
|
|
1593
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1594
|
+
body: JSON.stringify({
|
|
1595
|
+
error: error.message,
|
|
1596
|
+
context,
|
|
1597
|
+
timestamp: new Date().toISOString()
|
|
1598
|
+
})
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
```
|
|
1603
|
+
|
|
1604
|
+
### 4. 状态管理
|
|
1605
|
+
|
|
1606
|
+
```typescript
|
|
1607
|
+
class StatefulDataTable extends FlexiDataTable {
|
|
1608
|
+
private state = {
|
|
1609
|
+
sortColumn: null,
|
|
1610
|
+
sortDirection: 'asc',
|
|
1611
|
+
selectedRows: [],
|
|
1612
|
+
columnWidths: {},
|
|
1613
|
+
hiddenColumns: []
|
|
1614
|
+
};
|
|
1615
|
+
|
|
1616
|
+
saveState(): void {
|
|
1617
|
+
localStorage.setItem('table-state', JSON.stringify(this.state));
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
loadState(): void {
|
|
1621
|
+
try {
|
|
1622
|
+
const saved = localStorage.getItem('table-state');
|
|
1623
|
+
if (saved) {
|
|
1624
|
+
this.state = { ...this.state, ...JSON.parse(saved) };
|
|
1625
|
+
this.applyState();
|
|
1626
|
+
}
|
|
1627
|
+
} catch (error) {
|
|
1628
|
+
console.error('加载表格状态失败:', error);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
private applyState(): void {
|
|
1633
|
+
// 应用保存的列宽度
|
|
1634
|
+
this.columns.forEach((column, index) => {
|
|
1635
|
+
if (this.state.columnWidths[index]) {
|
|
1636
|
+
column.width = this.state.columnWidths[index];
|
|
1637
|
+
}
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
// 应用列可见性
|
|
1641
|
+
this.state.hiddenColumns.forEach(index => {
|
|
1642
|
+
if (this.columns[index]) {
|
|
1643
|
+
this.columns[index].visible = false;
|
|
1644
|
+
}
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
```
|
|
1649
|
+
|
|
1650
|
+
这个综合指南提供了有效使用和扩展 FlexiDataTable 构建复杂数据管理界面所需的一切。示例展示了如何创建强大、交互式的表格,具有实时更新、自定义渲染和复杂数据操作等高级功能。
|