@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.
Files changed (136) hide show
  1. package/FLEXICRITERIASET_GUIDE.md +1559 -0
  2. package/FLEXICRITERIASET_GUIDE_CN.md +1133 -0
  3. package/FLEXIDATATABLE_GUIDE.md +1650 -0
  4. package/FLEXIDATATABLE_GUIDE_CN.md +1650 -0
  5. package/FLEXIFORM_GUIDE.md +1068 -0
  6. package/FLEXIFORM_GUIDE_CN.md +1068 -0
  7. package/FLEXI_CONTEXT_GUIDE_CN.md +172 -0
  8. package/MODULE_LOADER_CN.md +228 -0
  9. package/README.md +307 -0
  10. package/README_CN.md +51 -0
  11. package/SANDBOX_CN.md +201 -0
  12. package/dist/FlexiContext.d.ts +28 -0
  13. package/dist/FlexiContext.js +45 -0
  14. package/dist/ModuleLoader.d.ts +41 -0
  15. package/dist/ModuleLoader.js +55 -0
  16. package/dist/Sandbox.d.ts +33 -0
  17. package/dist/Sandbox.js +101 -0
  18. package/dist/criteria-panel/CriteriaFieldsPanel.svelte +26 -0
  19. package/dist/criteria-panel/CriteriaFieldsPanel.svelte.d.ts +22 -0
  20. package/dist/criteria-panel/components/CascadeSelectSearchField.svelte +10 -0
  21. package/dist/criteria-panel/components/CascadeSelectSearchField.svelte.d.ts +25 -0
  22. package/dist/criteria-panel/components/DateRangeField.svelte +11 -0
  23. package/dist/criteria-panel/components/DateRangeField.svelte.d.ts +25 -0
  24. package/dist/criteria-panel/components/DateSearchField.svelte +10 -0
  25. package/dist/criteria-panel/components/DateSearchField.svelte.d.ts +24 -0
  26. package/dist/criteria-panel/components/DateTimeSearchField.svelte +10 -0
  27. package/dist/criteria-panel/components/DateTimeSearchField.svelte.d.ts +24 -0
  28. package/dist/criteria-panel/components/InputOptionSelectSearchField.svelte +9 -0
  29. package/dist/criteria-panel/components/InputOptionSelectSearchField.svelte.d.ts +24 -0
  30. package/dist/criteria-panel/components/NumberRangeField.svelte +11 -0
  31. package/dist/criteria-panel/components/NumberRangeField.svelte.d.ts +25 -0
  32. package/dist/criteria-panel/components/NumberSearchField.svelte +9 -0
  33. package/dist/criteria-panel/components/NumberSearchField.svelte.d.ts +24 -0
  34. package/dist/criteria-panel/components/OptionMultiSelectSearchField.svelte +9 -0
  35. package/dist/criteria-panel/components/OptionMultiSelectSearchField.svelte.d.ts +24 -0
  36. package/dist/criteria-panel/components/OptionSelectSearchField.svelte +9 -0
  37. package/dist/criteria-panel/components/OptionSelectSearchField.svelte.d.ts +24 -0
  38. package/dist/criteria-panel/components/SearchField.svelte +14 -0
  39. package/dist/criteria-panel/components/SearchField.svelte.d.ts +33 -0
  40. package/dist/criteria-panel/components/TextSearchField.svelte +9 -0
  41. package/dist/criteria-panel/components/TextSearchField.svelte.d.ts +24 -0
  42. package/dist/criteria-panel/components/UnknownCriteriaField.svelte +9 -0
  43. package/dist/criteria-panel/components/UnknownCriteriaField.svelte.d.ts +24 -0
  44. package/dist/criteria-panel/index.d.ts +6 -0
  45. package/dist/criteria-panel/index.js +6 -0
  46. package/dist/criteria-panel/lib/CriteriaComponentBuilder.d.ts +19 -0
  47. package/dist/criteria-panel/lib/CriteriaComponentBuilder.js +31 -0
  48. package/dist/criteria-panel/lib/CriteriaFieldBuilder.d.ts +1 -0
  49. package/dist/criteria-panel/lib/CriteriaFieldBuilder.js +127 -0
  50. package/dist/criteria-panel/lib/FlexiCriteriaField.d.ts +38 -0
  51. package/dist/criteria-panel/lib/FlexiCriteriaField.js +31 -0
  52. package/dist/criteria-panel/lib/FlexiCriteriaSet.d.ts +24 -0
  53. package/dist/criteria-panel/lib/FlexiCriteriaSet.js +48 -0
  54. package/dist/flexi-datatable/FlexiDataTable.d.ts +111 -0
  55. package/dist/flexi-datatable/FlexiDataTable.js +90 -0
  56. package/dist/flexi-datatable/index.d.ts +2 -0
  57. package/dist/flexi-datatable/index.js +2 -0
  58. package/dist/flexi-form/FlexiCompound.d.ts +34 -0
  59. package/dist/flexi-form/FlexiCompound.js +84 -0
  60. package/dist/flexi-form/FlexiFormDialog.svelte +24 -0
  61. package/dist/flexi-form/FlexiFormDialog.svelte.d.ts +21 -0
  62. package/dist/flexi-form/FlexiFormPage.svelte +26 -0
  63. package/dist/flexi-form/FlexiFormPage.svelte.d.ts +25 -0
  64. package/dist/flexi-form/Schema.d.ts +6 -0
  65. package/dist/flexi-form/Schema.js +1 -0
  66. package/dist/flexi-form/components/BreakLine.svelte +1 -0
  67. package/dist/flexi-form/components/BreakLine.svelte.d.ts +26 -0
  68. package/dist/flexi-form/components/CardTitleBar.svelte +18 -0
  69. package/dist/flexi-form/components/CardTitleBar.svelte.d.ts +22 -0
  70. package/dist/flexi-form/components/CascadeOptionSelectField.svelte +13 -0
  71. package/dist/flexi-form/components/CascadeOptionSelectField.svelte.d.ts +24 -0
  72. package/dist/flexi-form/components/CellFieldBuilder.d.ts +1 -0
  73. package/dist/flexi-form/components/CellFieldBuilder.js +178 -0
  74. package/dist/flexi-form/components/DateField.svelte +12 -0
  75. package/dist/flexi-form/components/DateField.svelte.d.ts +24 -0
  76. package/dist/flexi-form/components/DateTimeField.svelte +13 -0
  77. package/dist/flexi-form/components/DateTimeField.svelte.d.ts +24 -0
  78. package/dist/flexi-form/components/InputOptionSelectField.svelte +13 -0
  79. package/dist/flexi-form/components/InputOptionSelectField.svelte.d.ts +24 -0
  80. package/dist/flexi-form/components/MemoField.svelte +12 -0
  81. package/dist/flexi-form/components/MemoField.svelte.d.ts +24 -0
  82. package/dist/flexi-form/components/NumberField.svelte +12 -0
  83. package/dist/flexi-form/components/NumberField.svelte.d.ts +24 -0
  84. package/dist/flexi-form/components/OptionsMultiSelectField.svelte +13 -0
  85. package/dist/flexi-form/components/OptionsMultiSelectField.svelte.d.ts +24 -0
  86. package/dist/flexi-form/components/OptionsSelectField.svelte +13 -0
  87. package/dist/flexi-form/components/OptionsSelectField.svelte.d.ts +24 -0
  88. package/dist/flexi-form/components/TextField.svelte +12 -0
  89. package/dist/flexi-form/components/TextField.svelte.d.ts +24 -0
  90. package/dist/flexi-form/components/UnitNumberField.svelte +12 -0
  91. package/dist/flexi-form/components/UnitNumberField.svelte.d.ts +24 -0
  92. package/dist/flexi-form/components/UnknownTypeField.svelte +5 -0
  93. package/dist/flexi-form/components/UnknownTypeField.svelte.d.ts +18 -0
  94. package/dist/flexi-form/containers/FlexiPanel.svelte +13 -0
  95. package/dist/flexi-form/containers/FlexiPanel.svelte.d.ts +33 -0
  96. package/dist/flexi-form/flexi_card/FlexiCard.d.ts +64 -0
  97. package/dist/flexi-form/flexi_card/FlexiCard.js +66 -0
  98. package/dist/flexi-form/flexi_card/FlexiCardPanel.svelte +57 -0
  99. package/dist/flexi-form/flexi_card/FlexiCardPanel.svelte.d.ts +22 -0
  100. package/dist/flexi-form/flexi_composite/FlexiComposite.d.ts +50 -0
  101. package/dist/flexi-form/flexi_composite/FlexiComposite.js +26 -0
  102. package/dist/flexi-form/flexi_composite/FlexiCompositePanel.svelte +42 -0
  103. package/dist/flexi-form/flexi_composite/FlexiCompositePanel.svelte.d.ts +25 -0
  104. package/dist/flexi-form/flexi_composite/README.md +50 -0
  105. package/dist/flexi-form/flexi_datasheet/FlexiDataSheet.d.ts +4 -0
  106. package/dist/flexi-form/flexi_datasheet/FlexiDataSheet.js +2 -0
  107. package/dist/flexi-form/flexi_field/FlexiField.d.ts +76 -0
  108. package/dist/flexi-form/flexi_field/FlexiField.js +128 -0
  109. package/dist/flexi-form/flexi_field/FlexiFieldCell.svelte +35 -0
  110. package/dist/flexi-form/flexi_field/FlexiFieldCell.svelte.d.ts +25 -0
  111. package/dist/flexi-form/flexi_field/UnknownField.d.ts +3 -0
  112. package/dist/flexi-form/flexi_field/UnknownField.js +3 -0
  113. package/dist/flexi-form/flexi_form/FlexiForm.d.ts +127 -0
  114. package/dist/flexi-form/flexi_form/FlexiForm.js +160 -0
  115. package/dist/flexi-form/flexi_form/FlexiFormPanel.svelte +57 -0
  116. package/dist/flexi-form/flexi_form/FlexiFormPanel.svelte.d.ts +25 -0
  117. package/dist/flexi-form/index.d.ts +11 -0
  118. package/dist/flexi-form/index.js +11 -0
  119. package/dist/flexi-form/lib/ComponentBuilder.d.ts +15 -0
  120. package/dist/flexi-form/lib/ComponentBuilder.js +31 -0
  121. package/dist/flexi-form/lib/index.d.ts +5 -0
  122. package/dist/flexi-form/lib/index.js +2 -0
  123. package/dist/flexi-form/lib/types.d.ts +7 -0
  124. package/dist/flexi-form/lib/types.js +6 -0
  125. package/dist/flexi-form/lib/utils.d.ts +10 -0
  126. package/dist/flexi-form/lib/utils.js +48 -0
  127. package/dist/i18n-res/i18nRes.d.ts +2 -0
  128. package/dist/i18n-res/i18nRes.js +8 -0
  129. package/dist/i18n-res/index.d.ts +2 -0
  130. package/dist/i18n-res/index.js +2 -0
  131. package/dist/index.d.ts +5 -0
  132. package/dist/index.js +5 -0
  133. package/dist/uniface-flexi-module.css +46 -0
  134. package/dist/utils.d.ts +4 -0
  135. package/dist/utils.js +8 -0
  136. package/package.json +135 -0
@@ -0,0 +1,1133 @@
1
+ # FlexiCriteriaSet 完整使用指南
2
+
3
+ 在 Svelte 应用中构建动态搜索和过滤面板的 FlexiCriteriaSet 综合指南。
4
+
5
+ ## 目录
6
+
7
+ 1. [概述](#概述)
8
+ 2. [基础用法](#基础用法)
9
+ 3. [Schema 结构](#schema-结构)
10
+ 4. [搜索字段类型](#搜索字段类型)
11
+ 5. [高级搜索模式](#高级搜索模式)
12
+ 6. [动态搜索生成](#动态搜索生成)
13
+ 7. [事件和交互](#事件和交互)
14
+ 8. [扩展 FlexiCriteriaSet](#扩展-flexicriteriaset)
15
+ 9. [集成模式](#集成模式)
16
+ 10. [最佳实践](#最佳实践)
17
+
18
+ ## 概述
19
+
20
+ FlexiCriteriaSet 是一个强大的搜索条件构建器,允许用户通过直观的界面构建复杂的搜索查询。它支持各种字段类型、操作符,并为自定义搜索组件提供可扩展的架构。
21
+
22
+ ### 主要特性
23
+
24
+ - **灵活的搜索字段**:支持文本、数字、日期、选择和范围搜索
25
+ - **动态操作符**:针对不同字段类型的上下文相关操作符
26
+ - **基于 Schema 的配置**:JSON 驱动的搜索面板设置
27
+ - **可扩展架构**:自定义字段类型和搜索逻辑
28
+ - **类型安全**:完整的 TypeScript 支持
29
+ - **实时更新**:即时搜索条件生成
30
+
31
+ ## 基础用法
32
+
33
+ ### 1. 安装和设置
34
+
35
+ ```typescript
36
+ import CriteriaPanel, { FlexiCriteriaSet } from '@ticatec/uniface-flexi-module/criteria-panel';
37
+ import {registerCriteriaFieldBuilder} from '@ticatec/uniface-flexi-module/criteria-panel';
38
+ import '@ticatec/uniface-flexi-form/uniface-flexi-module.css';
39
+ ```
40
+
41
+ ### 2. 简单搜索面板
42
+
43
+ ```ts
44
+ import {FlexiCriteriaSet} from '@ticatec/uniface-flexi-module/criteria-panel';
45
+
46
+ const userSearchSchema = {
47
+ arrangement: 'vertical',
48
+ variant: 'outlined',
49
+ fields: [
50
+ {
51
+ type: 'text-editor',
52
+ name: 'userName',
53
+ label: '用户名',
54
+ keys: {keyField: 'userName'},
55
+ size: 'x25',
56
+ props: {
57
+ placeholder: '请输入用户名'
58
+ }
59
+ },
60
+ {
61
+ type: 'options-selector',
62
+ name: 'status',
63
+ label: '状态',
64
+ keys: {keyField: 'status'},
65
+ dictName: 'user-status',
66
+ size: 'x20',
67
+ props: {
68
+ placeholder: '请选择状态'
69
+ }
70
+ },
71
+ {
72
+ type: 'date-range',
73
+ name: 'registrationDate',
74
+ label: '注册日期',
75
+ keys: {
76
+ fromField: 'registeredFrom',
77
+ toField: 'registeredTo'
78
+ },
79
+ size: 'x30'
80
+ },
81
+ {
82
+ type: 'number-range',
83
+ name: 'age',
84
+ label: '年龄范围',
85
+ keys: {
86
+ formField: 'ageMin',
87
+ toField: 'ageMax'
88
+ },
89
+ size: 'x25'
90
+ }
91
+ ]
92
+ };
93
+
94
+ class UserSearchCriteriaSet extends FlexiCriteriaSet {
95
+ constructor() {
96
+ super(userSearchSchema);
97
+ }
98
+ }
99
+
100
+ ```
101
+
102
+ ```svelte
103
+ <script lang="ts">
104
+
105
+ import CriteriaFieldsPanel from "$lib/criteria-panel";
106
+ import PagingListPage from "@ticatec/uniface-app-component/data-table/PagingListPage";
107
+ import {onMount} from "svelte";
108
+ import type {PageAttrs, PageInitialize} from "@ticatec/uniface-micro-frame/common";
109
+ import type {ActionsColumn, DataColumn, IndicatorColumn} from "@ticatec/uniface-element/DataTable";
110
+ import type {ButtonActions} from "@ticatec/uniface-element/ActionBar";
111
+ import FilterPanel from "@ticatec/uniface-filter-panel";
112
+ import {CustomDataTable} from "./CustomHome";
113
+ import Sandbox from "$lib/Sandbox";
114
+ import FlexiContext from "$lib";
115
+
116
+ const criteriaCode = `
117
+ const criteriaSchema = {
118
+ variant: "outlined",
119
+ fields: [
120
+ {
121
+ type: "text-editor",
122
+ name: "name",
123
+ keys: {valueField: "name"},
124
+ label: "姓名",
125
+ size: "x15",
126
+ props: {
127
+ input$placeholder: "*替代所有字符"
128
+ }
129
+ },
130
+ {
131
+ type: "text-editor",
132
+ name: "code",
133
+ keys: {valueField: "code"},
134
+ label: "编码",
135
+ size: "x15",
136
+ props: {
137
+ input$placeholder: "*替代所有字符"
138
+ }
139
+ },
140
+ {
141
+ type: "options-selector",
142
+ keys: {valueField: "gender"},
143
+ name: "gender",
144
+ label: "性别",
145
+ size: "x15",
146
+ props: {
147
+ emptyText: "所有",
148
+ options: [
149
+ {code: "M", text: "男"},
150
+ {code: "F", text: "女"}
151
+ ]
152
+ }
153
+ },
154
+ {
155
+ type: "number-range",
156
+ name: "age",
157
+ keys: {fromField: "fromAge", toField: "toAge"},
158
+ size: "x30",
159
+ label: "年龄范围",
160
+ props: {
161
+ emptyText: "所有"
162
+ }
163
+ }
164
+ ]
165
+ }
166
+
167
+ class CustomCriteriaSet extends FlexiCriteriaSet {
168
+
169
+ constructor() {
170
+ super(criteriaSchema);
171
+ }
172
+ }
173
+
174
+ return {CustomCriteriaSet}
175
+
176
+ `
177
+ let criteriaSet: any;
178
+
179
+ let criteria: any = {};
180
+ let dataTable;
181
+
182
+ onMount(async () => {
183
+ FlexiContext.initialize(async (dicNames)=> ({}))
184
+ let sandbox = Sandbox.initialize({}, []);
185
+ console.log("sandbox", sandbox);
186
+ const {CustomCriteriaSet} = await sandbox.createFormClass(criteriaCode);
187
+ console.log("CustomCriteriaSet", CustomCriteriaSet);
188
+ const set = new CustomCriteriaSet();
189
+ set.initialize();
190
+ criteriaSet = set;
191
+ console.log(set);
192
+ dataTable = new CustomDataTable();
193
+ await dataTable.initialize();
194
+ columns = dataTable.columns;
195
+ indicatorColumn = dataTable.indicatorColumn;
196
+ actionsColumn = dataTable.actionsColumn;
197
+ console.log('表格列:', columns, indicatorColumn, actionsColumn);
198
+ loaded = true;
199
+ })
200
+
201
+
202
+ $: console.log('当前查询条件:', criteria);
203
+
204
+ let page$attrs: PageAttrs = {
205
+ title: "演示页面"
206
+ };
207
+
208
+
209
+ let list: Array<any> = [{name:"我的数据1", status: 0}, {name:"我的数据2", status: 2}];
210
+
211
+ let indicatorColumn: IndicatorColumn;
212
+
213
+ let columns: Array<DataColumn> = [];
214
+ let actionsColumn: ActionsColumn | null = null;
215
+
216
+ let total: number = 125;
217
+ let pageCount: number = 5;
218
+ let pageNo: number = 3;
219
+ const onRowCountChanged = () => {
220
+
221
+ }
222
+ const onPageChange = () => {
223
+
224
+ }
225
+
226
+
227
+ const actions: ButtonActions = [];
228
+
229
+ const doSearch = async (reset: boolean = false) => {
230
+
231
+ }
232
+
233
+ let loaded: boolean = false;
234
+
235
+
236
+ </script>
237
+ {#if loaded}
238
+ <PagingListPage {page$attrs} {columns} {indicatorColumn} {actionsColumn} bind:list {onRowCountChanged} {onPageChange} {total}
239
+ {pageCount} {pageNo}>
240
+ <div slot="search-panel" style="padding: 8px 0">
241
+ <FilterPanel {actions} resetClickHandler={()=>doSearch(true)} searchClickHandler={()=>doSearch()}>
242
+ {#if criteriaSet}
243
+ <CriteriaFieldsPanel bind:criteria {criteriaSet}>
244
+ </CriteriaFieldsPanel>
245
+ {/if}
246
+ </FilterPanel>
247
+ </div>
248
+ </PagingListPage>
249
+ {/if}
250
+ ```
251
+
252
+ ## Schema 结构
253
+
254
+ ### FlexiCriteriaSetSchema
255
+
256
+ ```typescript
257
+ interface FlexiCriteriaSetSchema {
258
+ variant?: 'outlined' | 'filled'; // 字段变体
259
+ arrangement?: 'vertical' | 'horizontal'; // 布局方向
260
+ label$style?: string; // 标签样式
261
+ fields: Array<FlexiCriteriaFieldSchema>; // 搜索字段
262
+ }
263
+ ```
264
+
265
+ ### FlexiCriteriaFieldSchema
266
+
267
+ ```typescript
268
+ interface FlexiCriteriaFieldSchema {
269
+ type: string; // 字段类型标识符
270
+ keys: { [key: string]: string }; // 数据绑定键
271
+ name: string; // 字段名称
272
+ dictName?: string; // 选项字典
273
+ visible?: boolean; // 字段可见性
274
+ label: string; // 显示标签
275
+ events?: Record<string, string>; // 事件处理器
276
+ size?: 'x15' | 'x20' | 'x25' | 'x30' | 'x35' | 'x40'; // 字段宽度
277
+ props?: any; // 字段特定属性
278
+ }
279
+ ```
280
+
281
+ ## 搜索字段类型
282
+
283
+ ### 内置字段类型
284
+
285
+ | 类型 | 描述 | keys | 常用属性 |
286
+ |------|-------------|------------------------|------------------------|
287
+ | `cascade-options` | 级联选择搜索 | `keyField`, `textField` | `dictName`, `levels` |
288
+ | `date-range` | 日期范围搜索 | `fromField`, `toField` | `min`, `max`, `format` |
289
+ | `date-picker` | 日期搜索字段 | `keyField` | `min`, `max`, `format` |
290
+ | `datetime-picker` | 日期时间搜索 | `keyField` | `min`, `max`, `format` |
291
+ | `input-options-selector` | 可搜索选择 | `keyField` | `getOptions` |
292
+ | `number-range` | 数字范围搜索 | `fromField`, `toField` | |
293
+ | `number-editor` | 数字搜索字段 | `keyField` | `min`, `max`, `operators` |
294
+ | `options-multi-selector` | 多选搜索 | `keyField` | `dictName` |
295
+ | `options-selector` | 单选搜索 | `keyField` | `dictName`, `emptyText` |
296
+ | `text-editor` | 文本搜索字段 | `keyField` | `placeholder` |
297
+
298
+ ### 字段示例
299
+
300
+ ```typescript
301
+ // 带操作符的文本搜索
302
+ {
303
+ type: 'text-search',
304
+ name: 'productName',
305
+ label: '产品名称',
306
+ keys: { keyField: 'name' },
307
+ size: 'x30',
308
+ props: {
309
+ placeholder: '请输入产品名称',
310
+ operators: ['contains', 'startsWith', 'endsWith', 'equals']
311
+ }
312
+ }
313
+
314
+ // 数字范围搜索
315
+ {
316
+ type: 'number-range',
317
+ name: 'priceRange',
318
+ label: '价格范围',
319
+ keys: {
320
+ minField: 'priceMin',
321
+ maxField: 'priceMax'
322
+ },
323
+ size: 'x25',
324
+ props: {
325
+ min: 0,
326
+ max: 10000,
327
+ step: 10,
328
+ currency: 'CNY'
329
+ }
330
+ }
331
+
332
+ // 带约束的日期范围
333
+ {
334
+ type: 'date-range',
335
+ name: 'orderDate',
336
+ label: '订单日期',
337
+ keys: {
338
+ fromField: 'orderDateFrom',
339
+ toField: 'orderDateTo'
340
+ },
341
+ size: 'x30',
342
+ props: {
343
+ min: '2020-01-01',
344
+ max: new Date().toISOString().split('T')[0],
345
+ format: 'YYYY-MM-DD'
346
+ }
347
+ }
348
+
349
+ // 带字典的多选
350
+ {
351
+ type: 'option-multi-select-search',
352
+ name: 'categories',
353
+ label: '分类',
354
+ keys: { field: 'categoryIds' },
355
+ dictName: 'product-categories',
356
+ size: 'x25',
357
+ props: {
358
+ placeholder: '请选择分类'
359
+ }
360
+ }
361
+
362
+ // 动态可搜索选择
363
+ {
364
+ type: 'input-option-select-search',
365
+ name: 'assignee',
366
+ label: '分配给',
367
+ keys: { field: 'assigneeId' },
368
+ size: 'x25',
369
+ props: {
370
+ getOptions: 'loadUserSuggestions',
371
+ minLength: 2,
372
+ debounce: 300,
373
+ placeholder: '输入搜索用户'
374
+ }
375
+ }
376
+ ```
377
+
378
+ ## 事件和交互
379
+
380
+ ### 搜索条件变化事件
381
+
382
+ ```svelte
383
+ <script>
384
+ function handleCriteriaChange(event) {
385
+ const { criteria, field, value } = event.detail;
386
+ console.log(`字段 ${field} 改变为:`, value);
387
+ console.log('完整条件:', criteria);
388
+
389
+ // 触发实时搜索
390
+ debouncedSearch(criteria);
391
+ }
392
+
393
+ function handleFieldFocus(event) {
394
+ const { field } = event.detail;
395
+ console.log(`字段 ${field} 获得焦点`);
396
+
397
+ // 为此字段加载建议
398
+ loadFieldSuggestions(field);
399
+ }
400
+
401
+ function handleOperatorChange(event) {
402
+ const { field, operator } = event.detail;
403
+ console.log(`字段 ${field} 操作符改变为:`, operator);
404
+ }
405
+
406
+ // 防抖搜索函数
407
+ const debouncedSearch = debounce((criteria) => {
408
+ performSearch(criteria);
409
+ }, 300);
410
+
411
+ function debounce(func, wait) {
412
+ let timeout;
413
+ return function executedFunction(...args) {
414
+ const later = () => {
415
+ clearTimeout(timeout);
416
+ func(...args);
417
+ };
418
+ clearTimeout(timeout);
419
+ timeout = setTimeout(later, wait);
420
+ };
421
+ }
422
+ </script>
423
+
424
+ <CriteriaPanel
425
+ {schema}
426
+ bind:criteria={searchCriteria}
427
+ on:change={handleCriteriaChange}
428
+ on:fieldFocus={handleFieldFocus}
429
+ on:operatorChange={handleOperatorChange}
430
+ on:search={handleSearch}
431
+ on:reset={handleReset}
432
+ />
433
+ ```
434
+
435
+ ## 扩展 FlexiCriteriaSet
436
+
437
+ ### 创建自定义搜索字段类型
438
+
439
+ #### 1. 自定义范围滑块字段
440
+
441
+ ```svelte
442
+ <!-- CustomRangeSliderField.svelte -->
443
+ <script lang="ts">
444
+ import type FlexiCriteriaField from "$lib/criteria-panel/lib/FlexiCriteriaField";
445
+
446
+ export let field: FlexiCriteriaField;
447
+ export let readonly: boolean = false;
448
+ export let disabled: boolean = false;
449
+
450
+ $: props = field.props;
451
+ $: keys = field.keys;
452
+
453
+ let minValue = props.min || 0;
454
+ let maxValue = props.max || 100;
455
+ let currentMin = minValue;
456
+ let currentMax = maxValue;
457
+
458
+ // 从数据初始化
459
+ if (field.data) {
460
+ currentMin = field.data[keys.minField] || minValue;
461
+ currentMax = field.data[keys.maxField] || maxValue;
462
+ }
463
+
464
+ function updateRange() {
465
+ if (!field.data) field.data = {};
466
+ field.data[keys.minField] = currentMin;
467
+ field.data[keys.maxField] = currentMax;
468
+
469
+ // 触发变化事件
470
+ field.events.change?.({ min: currentMin, max: currentMax });
471
+ }
472
+
473
+ $: if (currentMin !== undefined && currentMax !== undefined) {
474
+ updateRange();
475
+ }
476
+ </script>
477
+
478
+ <div class="range-slider-field" class:readonly class:disabled>
479
+ <label>{field.label}</label>
480
+
481
+ <div class="range-inputs">
482
+ <input
483
+ type="number"
484
+ bind:value={currentMin}
485
+ min={minValue}
486
+ max={currentMax}
487
+ {disabled}
488
+ {readonly}
489
+ class="range-input"
490
+ />
491
+ <span class="range-separator">至</span>
492
+ <input
493
+ type="number"
494
+ bind:value={currentMax}
495
+ min={currentMin}
496
+ max={maxValue}
497
+ {disabled}
498
+ {readonly}
499
+ class="range-input"
500
+ />
501
+ </div>
502
+
503
+ <div class="slider-container">
504
+ <input
505
+ type="range"
506
+ bind:value={currentMin}
507
+ min={minValue}
508
+ max={maxValue}
509
+ step={props.step || 1}
510
+ {disabled}
511
+ {readonly}
512
+ class="slider slider-min"
513
+ />
514
+ <input
515
+ type="range"
516
+ bind:value={currentMax}
517
+ min={minValue}
518
+ max={maxValue}
519
+ step={props.step || 1}
520
+ {disabled}
521
+ {readonly}
522
+ class="slider slider-max"
523
+ />
524
+ </div>
525
+
526
+ <div class="range-display">
527
+ {props.prefix || ''}{currentMin} - {currentMax}{props.suffix || ''}
528
+ </div>
529
+ </div>
530
+
531
+ <style>
532
+ .range-slider-field {
533
+ display: flex;
534
+ flex-direction: column;
535
+ gap: 8px;
536
+ }
537
+
538
+ .range-inputs {
539
+ display: flex;
540
+ align-items: center;
541
+ gap: 8px;
542
+ }
543
+
544
+ .range-input {
545
+ width: 80px;
546
+ padding: 4px 8px;
547
+ border: 1px solid #ddd;
548
+ border-radius: 4px;
549
+ }
550
+
551
+ .range-separator {
552
+ color: #666;
553
+ }
554
+
555
+ .slider-container {
556
+ position: relative;
557
+ height: 20px;
558
+ }
559
+
560
+ .slider {
561
+ position: absolute;
562
+ width: 100%;
563
+ height: 20px;
564
+ -webkit-appearance: none;
565
+ background: transparent;
566
+ outline: none;
567
+ }
568
+
569
+ .slider::-webkit-slider-track {
570
+ height: 4px;
571
+ background: #ddd;
572
+ border-radius: 2px;
573
+ }
574
+
575
+ .slider::-webkit-slider-thumb {
576
+ -webkit-appearance: none;
577
+ width: 16px;
578
+ height: 16px;
579
+ background: #007bff;
580
+ border-radius: 50%;
581
+ cursor: pointer;
582
+ }
583
+
584
+ .range-display {
585
+ text-align: center;
586
+ font-weight: bold;
587
+ color: #333;
588
+ }
589
+
590
+ .readonly, .disabled {
591
+ opacity: 0.6;
592
+ pointer-events: none;
593
+ }
594
+ </style>
595
+ ```
596
+
597
+ #### 2. 注册自定义字段类型
598
+
599
+ ```typescript
600
+ import CriteriaComponentBuilder from '@ticatec/uniface-flexi-form/criteria-panel/lib/CriteriaComponentBuilder';
601
+ import CustomRangeSliderField from './CustomRangeSliderField.svelte';
602
+
603
+ // 创建构建器函数
604
+ const buildRangeSliderField = (schema: FlexiCriteriaFieldSchema, dictLoader: DictionaryLoader) => {
605
+ return {
606
+ component: CustomRangeSliderField,
607
+ props: schema.props,
608
+ keyFields: schema.keys
609
+ };
610
+ };
611
+
612
+ // 注册自定义字段类型
613
+ const componentBuilder = CriteriaComponentBuilder.getInstance();
614
+ componentBuilder.register('range-slider', buildRangeSliderField);
615
+
616
+ // 在 schema 中使用
617
+ const schemaWithRangeSlider = {
618
+ arrangement: 'vertical',
619
+ fields: [
620
+ {
621
+ type: 'range-slider',
622
+ name: 'priceRange',
623
+ label: '价格范围',
624
+ keys: {
625
+ minField: 'priceMin',
626
+ maxField: 'priceMax'
627
+ },
628
+ size: 'x30',
629
+ props: {
630
+ min: 0,
631
+ max: 1000,
632
+ step: 10,
633
+ prefix: '¥',
634
+ suffix: ' 元'
635
+ }
636
+ }
637
+ ]
638
+ };
639
+ ```
640
+
641
+ ### 创建自定义搜索集合
642
+
643
+ ```typescript
644
+ class AdvancedProductCriteriaSet extends FlexiCriteriaSet {
645
+ private productCategories: any[] = [];
646
+ private priceHistory: any[] = [];
647
+
648
+ constructor(schema: FlexiCriteriaSetSchema) {
649
+ super(schema);
650
+ this.setupAdvancedFeatures();
651
+ }
652
+
653
+ private async setupAdvancedFeatures(): Promise<void> {
654
+ await this.loadProductCategories();
655
+ await this.loadPriceHistory();
656
+ this.setupSmartDefaults();
657
+ this.setupAdvancedValidation();
658
+ }
659
+
660
+ private async loadProductCategories(): Promise<void> {
661
+ try {
662
+ const response = await fetch('/api/products/categories/tree');
663
+ this.productCategories = await response.json();
664
+ } catch (error) {
665
+ console.error('加载产品分类失败:', error);
666
+ }
667
+ }
668
+
669
+ private async loadPriceHistory(): Promise<void> {
670
+ try {
671
+ const response = await fetch('/api/products/price-history');
672
+ this.priceHistory = await response.json();
673
+ } catch (error) {
674
+ console.error('加载价格历史失败:', error);
675
+ }
676
+ }
677
+
678
+ private setupSmartDefaults(): void {
679
+ // 根据用户历史或热门搜索设置智能默认值
680
+ const categoryField = this.field['category'];
681
+ const priceField = this.field['priceRange'];
682
+
683
+ if (categoryField && this.productCategories.length > 0) {
684
+ // 设置最受欢迎的分类为默认值
685
+ const popularCategory = this.getMostPopularCategory();
686
+ categoryField.data = popularCategory?.id;
687
+ }
688
+
689
+ if (priceField && this.priceHistory.length > 0) {
690
+ // 设置常见价格范围为默认值
691
+ const commonRange = this.getCommonPriceRange();
692
+ priceField.data = commonRange;
693
+ }
694
+ }
695
+
696
+ private setupAdvancedValidation(): void {
697
+ // 添加跨字段验证
698
+ this.addValidator('priceRange', (value: any) => {
699
+ if (value.min > value.max) {
700
+ return '最小价格不能大于最大价格';
701
+ }
702
+ return null;
703
+ });
704
+
705
+ this.addValidator('dateRange', (value: any) => {
706
+ if (new Date(value.from) > new Date(value.to)) {
707
+ return '开始日期不能晚于结束日期';
708
+ }
709
+ return null;
710
+ });
711
+ }
712
+
713
+ private getMostPopularCategory(): any {
714
+ // 实现确定最受欢迎分类的逻辑
715
+ return this.productCategories[0];
716
+ }
717
+
718
+ private getCommonPriceRange(): any {
719
+ // 实现确定常见价格范围的逻辑
720
+ return { min: 10, max: 100 };
721
+ }
722
+
723
+ private addValidator(fieldName: string, validator: (value: any) => string | null): void {
724
+ const field = this.field[fieldName];
725
+ if (field) {
726
+ const originalChange = field.events.change;
727
+ field.events.change = (value: any) => {
728
+ const error = validator(value);
729
+ if (error) {
730
+ console.error(`${fieldName} 验证错误:`, error);
731
+ // 处理验证错误
732
+ } else {
733
+ originalChange?.(value);
734
+ }
735
+ };
736
+ }
737
+ }
738
+
739
+ // 公共 API 方法
740
+ getCriteriaQuery(): any {
741
+ const criteria = {};
742
+
743
+ for (const field of this.fields) {
744
+ if (field.data !== null && field.data !== undefined) {
745
+ // 将字段数据转换为查询格式
746
+ const queryPart = this.convertFieldToQuery(field);
747
+ Object.assign(criteria, queryPart);
748
+ }
749
+ }
750
+
751
+ return criteria;
752
+ }
753
+
754
+ private convertFieldToQuery(field: FlexiCriteriaField): any {
755
+ const query = {};
756
+
757
+ switch (field.component.name) {
758
+ case 'TextSearchField':
759
+ query[field.keys.field] = {
760
+ operator: field.props.selectedOperator || 'contains',
761
+ value: field.data
762
+ };
763
+ break;
764
+
765
+ case 'RangeSliderField':
766
+ if (field.data.min !== undefined) {
767
+ query[field.keys.minField] = {
768
+ operator: 'gte',
769
+ value: field.data.min
770
+ };
771
+ }
772
+ if (field.data.max !== undefined) {
773
+ query[field.keys.maxField] = {
774
+ operator: 'lte',
775
+ value: field.data.max
776
+ };
777
+ }
778
+ break;
779
+
780
+ // 处理其他字段类型...
781
+ }
782
+
783
+ return query;
784
+ }
785
+
786
+ exportCriteria(): string {
787
+ return JSON.stringify(this.getCriteriaQuery(), null, 2);
788
+ }
789
+
790
+ importCriteria(jsonString: string): void {
791
+ try {
792
+ const criteria = JSON.parse(jsonString);
793
+ this.applyCriteria(criteria);
794
+ } catch (error) {
795
+ console.error('导入搜索条件失败:', error);
796
+ }
797
+ }
798
+
799
+ private applyCriteria(criteria: any): void {
800
+ // 将导入的条件应用到字段
801
+ for (const [key, value] of Object.entries(criteria)) {
802
+ const field = this.findFieldByKey(key);
803
+ if (field) {
804
+ field.data = value;
805
+ }
806
+ }
807
+ this.invalidate();
808
+ }
809
+
810
+ private findFieldByKey(key: string): FlexiCriteriaField | null {
811
+ for (const field of this.fields) {
812
+ if (Object.values(field.keys).includes(key)) {
813
+ return field;
814
+ }
815
+ }
816
+ return null;
817
+ }
818
+ }
819
+ ```
820
+
821
+ ## 集成模式
822
+
823
+ ### 与数据表集成
824
+
825
+ ```svelte
826
+ <!-- ProductSearchAndTable.svelte -->
827
+ <script lang="ts">
828
+ import CriteriaPanel from '@ticatec/uniface-flexi-form/criteria-panel';
829
+ import DataTable from '@ticatec/uniface-element/DataTable';
830
+
831
+ let searchCriteria = {};
832
+ let tableData = [];
833
+ let loading = false;
834
+ let totalRecords = 0;
835
+ let currentPage = 1;
836
+ let pageSize = 20;
837
+
838
+ const criteriaSchema = {
839
+ arrangement: 'horizontal',
840
+ fields: [
841
+ {
842
+ type: 'text-search',
843
+ name: 'name',
844
+ label: '产品名称',
845
+ keys: { field: 'name' },
846
+ size: 'x25'
847
+ },
848
+ {
849
+ type: 'option-select-search',
850
+ name: 'category',
851
+ label: '分类',
852
+ keys: { field: 'categoryId' },
853
+ dictName: 'categories',
854
+ size: 'x20'
855
+ },
856
+ {
857
+ type: 'number-range',
858
+ name: 'price',
859
+ label: '价格范围',
860
+ keys: { minField: 'priceMin', maxField: 'priceMax' },
861
+ size: 'x25'
862
+ }
863
+ ]
864
+ };
865
+
866
+ const tableColumns = [
867
+ { field: 'name', text: '产品名称', width: 200 },
868
+ { field: 'category', text: '分类', width: 150 },
869
+ { field: 'price', text: '价格', width: 100, formatter: 'currency' },
870
+ { field: 'stock', text: '库存', width: 100 },
871
+ { field: 'status', text: '状态', width: 120 }
872
+ ];
873
+
874
+ // 当搜索条件改变时响应式搜索
875
+ $: if (searchCriteria) {
876
+ performSearch();
877
+ }
878
+
879
+ async function performSearch() {
880
+ loading = true;
881
+ try {
882
+ const queryParams = new URLSearchParams({
883
+ page: currentPage.toString(),
884
+ limit: pageSize.toString(),
885
+ ...flattenCriteria(searchCriteria)
886
+ });
887
+
888
+ const response = await fetch(`/api/products/search?${queryParams}`);
889
+ const result = await response.json();
890
+
891
+ tableData = result.data;
892
+ totalRecords = result.total;
893
+ } catch (error) {
894
+ console.error('搜索失败:', error);
895
+ tableData = [];
896
+ } finally {
897
+ loading = false;
898
+ }
899
+ }
900
+
901
+ function flattenCriteria(criteria) {
902
+ const flattened = {};
903
+ for (const [key, value] of Object.entries(criteria)) {
904
+ if (value !== null && value !== undefined && value !== '') {
905
+ flattened[key] = value;
906
+ }
907
+ }
908
+ return flattened;
909
+ }
910
+
911
+ function handlePageChange(event) {
912
+ currentPage = event.detail.page;
913
+ performSearch();
914
+ }
915
+
916
+ function handleCriteriaChange(event) {
917
+ searchCriteria = event.detail.criteria;
918
+ currentPage = 1; // 新搜索时重置到第一页
919
+ }
920
+
921
+ function handleReset() {
922
+ searchCriteria = {};
923
+ currentPage = 1;
924
+ }
925
+ </script>
926
+
927
+ <div class="search-and-table">
928
+ <div class="search-panel">
929
+ <h3>产品搜索</h3>
930
+ <CriteriaPanel
931
+ schema={criteriaSchema}
932
+ bind:criteria={searchCriteria}
933
+ on:change={handleCriteriaChange}
934
+ on:reset={handleReset}
935
+ />
936
+ </div>
937
+
938
+ <div class="results-panel">
939
+ <div class="results-header">
940
+ <h3>搜索结果 ({totalRecords} 项)</h3>
941
+ {#if loading}
942
+ <div class="loading">搜索中...</div>
943
+ {/if}
944
+ </div>
945
+
946
+ <DataTable
947
+ columns={tableColumns}
948
+ data={tableData}
949
+ {totalRecords}
950
+ {currentPage}
951
+ {pageSize}
952
+ on:pageChange={handlePageChange}
953
+ />
954
+ </div>
955
+ </div>
956
+
957
+ <style>
958
+ .search-and-table {
959
+ display: flex;
960
+ flex-direction: column;
961
+ gap: 20px;
962
+ }
963
+
964
+ .search-panel {
965
+ background: #f8f9fa;
966
+ padding: 16px;
967
+ border-radius: 8px;
968
+ border: 1px solid #dee2e6;
969
+ }
970
+
971
+ .results-panel {
972
+ flex: 1;
973
+ }
974
+
975
+ .results-header {
976
+ display: flex;
977
+ justify-content: space-between;
978
+ align-items: center;
979
+ margin-bottom: 16px;
980
+ }
981
+
982
+ .loading {
983
+ color: #6c757d;
984
+ font-style: italic;
985
+ }
986
+ </style>
987
+ ```
988
+
989
+ ## 最佳实践
990
+
991
+ ### 1. Schema 组织
992
+
993
+ ```typescript
994
+ // 好的:使用常量的有组织 schema
995
+ const FIELD_SIZES = {
996
+ SMALL: 'x15',
997
+ MEDIUM: 'x25',
998
+ LARGE: 'x35',
999
+ FULL: 'x40'
1000
+ } as const;
1001
+
1002
+ const COMMON_FIELDS = {
1003
+ NAME_SEARCH: {
1004
+ type: 'text-search',
1005
+ keys: { field: 'name' },
1006
+ size: FIELD_SIZES.MEDIUM,
1007
+ props: {
1008
+ placeholder: '请输入名称',
1009
+ operators: ['contains', 'startsWith', 'equals']
1010
+ }
1011
+ },
1012
+
1013
+ STATUS_SELECT: {
1014
+ type: 'option-select-search',
1015
+ keys: { field: 'status' },
1016
+ dictName: 'status-options',
1017
+ size: FIELD_SIZES.SMALL
1018
+ }
1019
+ } as const;
1020
+
1021
+ const PRODUCT_SEARCH_SCHEMA = {
1022
+ arrangement: 'horizontal' as const,
1023
+ variant: 'outlined' as const,
1024
+ fields: [
1025
+ {
1026
+ ...COMMON_FIELDS.NAME_SEARCH,
1027
+ name: 'productName',
1028
+ label: '产品名称'
1029
+ },
1030
+ {
1031
+ ...COMMON_FIELDS.STATUS_SELECT,
1032
+ name: 'status',
1033
+ label: '状态'
1034
+ }
1035
+ ]
1036
+ };
1037
+ ```
1038
+
1039
+ ### 2. 性能优化
1040
+
1041
+ ```typescript
1042
+ // 针对实时过滤的防抖搜索
1043
+ class OptimizedCriteriaHandler {
1044
+ private searchDebounceTime = 300;
1045
+ private dictionaryCache = new Map();
1046
+
1047
+ createDebouncedSearch(searchFunction: Function) {
1048
+ let timeoutId: number;
1049
+
1050
+ return (criteria: any) => {
1051
+ clearTimeout(timeoutId);
1052
+ timeoutId = setTimeout(() => {
1053
+ searchFunction(criteria);
1054
+ }, this.searchDebounceTime);
1055
+ };
1056
+ }
1057
+
1058
+ async getCachedDictionary(dictName: string): Promise<any[]> {
1059
+ if (this.dictionaryCache.has(dictName)) {
1060
+ return this.dictionaryCache.get(dictName);
1061
+ }
1062
+
1063
+ const dictionary = await this.loadDictionary(dictName);
1064
+ this.dictionaryCache.set(dictName, dictionary);
1065
+ return dictionary;
1066
+ }
1067
+
1068
+ private async loadDictionary(dictName: string): Promise<any[]> {
1069
+ const response = await fetch(`/api/dictionaries/${dictName}`);
1070
+ return await response.json();
1071
+ }
1072
+ }
1073
+ ```
1074
+
1075
+ ### 3. 错误处理
1076
+
1077
+ ```typescript
1078
+ class CriteriaErrorHandler {
1079
+ static handleCriteriaError(error: any, criteriaSet: FlexiCriteriaSet): void {
1080
+ if (error.fieldErrors) {
1081
+ this.displayFieldErrors(error.fieldErrors, criteriaSet);
1082
+ } else if (error.networkError) {
1083
+ this.displayNetworkError();
1084
+ } else {
1085
+ this.displayGenericError(error);
1086
+ }
1087
+ }
1088
+
1089
+ private static displayFieldErrors(errors: Record<string, string>, criteriaSet: FlexiCriteriaSet): void {
1090
+ for (const [fieldName, errorMessage] of Object.entries(errors)) {
1091
+ const field = criteriaSet.field[fieldName];
1092
+ if (field) {
1093
+ // 在字段附近显示错误
1094
+ console.error(`字段 ${fieldName}: ${errorMessage}`);
1095
+ }
1096
+ }
1097
+ }
1098
+
1099
+ private static displayNetworkError(): void {
1100
+ console.error('搜索时发生网络错误');
1101
+ }
1102
+
1103
+ private static displayGenericError(error: any): void {
1104
+ console.error('搜索错误:', error);
1105
+ }
1106
+ }
1107
+ ```
1108
+
1109
+ ### 4. 可访问性
1110
+
1111
+ ```typescript
1112
+ const accessibleCriteriaSchema = {
1113
+ arrangement: 'vertical',
1114
+ fields: [
1115
+ {
1116
+ type: 'text-editor',
1117
+ name: 'search',
1118
+ label: '搜索产品',
1119
+ keys: { field: 'query' },
1120
+ props: {
1121
+ placeholder: '请输入搜索词',
1122
+ 'aria-label': '按名称或描述搜索产品',
1123
+ 'aria-describedby': 'search-help'
1124
+ }
1125
+ }
1126
+ ]
1127
+ };
1128
+
1129
+ // 添加帮助文本和 ARIA 属性
1130
+ const helpText = {
1131
+ 'search-help': '使用关键词搜索产品名称和描述'
1132
+ };
1133
+ ```