@ticatec/uniface-element 0.2.0 → 0.2.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 (209) hide show
  1. package/README.md +320 -1
  2. package/README_CN.md +392 -0
  3. package/dist/accordion/Accordion.svelte +1 -1
  4. package/dist/action-bar/ActionBar.svelte +7 -1
  5. package/dist/attachment-files/FileRender.svelte +1 -1
  6. package/dist/attachment-files/FileUploadBar.svelte +7 -6
  7. package/dist/attachment-files/FileUploadPanel.svelte +2 -2
  8. package/dist/base-calendar/DateContext.js +5 -4
  9. package/dist/base-calendar/YearPickupPanel.svelte +4 -4
  10. package/dist/card/Card.svelte +1 -1
  11. package/dist/card/Card.svelte.d.ts +1 -1
  12. package/dist/cascade-options-select/CascadeOptionSelect.svelte +2 -3
  13. package/dist/cascade-options-select/CascadeOptionSelect.svelte.d.ts +46 -1
  14. package/dist/cascade-options-select/CascadePanel.svelte +1 -1
  15. package/dist/checkbox/CheckBox.svelte +1 -1
  16. package/dist/color-picker/ColorPicker.svelte +9 -9
  17. package/dist/color-picker/ColorPicker.svelte.d.ts +0 -2
  18. package/dist/color-picker/ColorPickerPanel.svelte +11 -15
  19. package/dist/color-picker/ColorPickerPanel.svelte.d.ts +1 -1
  20. package/dist/common/CommonPicker.svelte +2 -2
  21. package/dist/common/CommonPicker.svelte.d.ts +1 -1
  22. package/dist/common/MouseClickHandler.d.ts +1 -1
  23. package/dist/common/Popover.svelte +28 -6
  24. package/dist/common/Popover.svelte.d.ts +9 -3
  25. package/dist/common/uniface-utils.js +6 -4
  26. package/dist/common-editor/CommonEditor.svelte +1 -1
  27. package/dist/common-editor/CommonEditor.svelte.d.ts +1 -1
  28. package/dist/common-editor/NumberInput.svelte +1 -1
  29. package/dist/common-editor/NumberInput.svelte.d.ts +1 -1
  30. package/dist/concise-data-table/ConciseListTable.svelte +7 -2
  31. package/dist/concise-data-table/ConciseListTable.svelte.d.ts +1 -0
  32. package/dist/concise-data-table/DataRow.svelte +1 -1
  33. package/dist/concise-data-table/TableOptions.d.ts +1 -1
  34. package/dist/data-table/DataTable.svelte +1 -1
  35. package/dist/data-table/parts/ActionsPanel.svelte +4 -3
  36. package/dist/data-table/parts/ActionsRow.svelte +3 -3
  37. package/dist/data-table/parts/ContentPanel.svelte +3 -2
  38. package/dist/data-table/parts/FixedColumnsPanel.svelte +0 -1
  39. package/dist/data-table/parts/FixedColumnsPanel.svelte.d.ts +1 -2
  40. package/dist/data-table/parts/FixedHeaderPanel.svelte +2 -2
  41. package/dist/data-table/parts/FixedRow.svelte +2 -1
  42. package/dist/data-table/parts/FixedRow.svelte.d.ts +1 -1
  43. package/dist/data-table/parts/HrefCell.svelte +23 -2
  44. package/dist/data-table/parts/PopupMenu.svelte +25 -2
  45. package/dist/data-table/parts/PopupMenu.svelte.d.ts +8 -1
  46. package/dist/date-picker/DatePicker.svelte +0 -1
  47. package/dist/date-picker/DateTimePicker.svelte +3 -3
  48. package/dist/date-picker/ScrollBar.svelte +4 -7
  49. package/dist/date-range/DateRangeEditor.svelte +1 -1
  50. package/dist/dialog/CommonDialog.svelte +8 -5
  51. package/dist/dialog/CommonDialog.svelte.d.ts +2 -1
  52. package/dist/dialog/Dialog.svelte +37 -12
  53. package/dist/dialog/Dialog.svelte.d.ts +4 -3
  54. package/dist/dialog/DialogBoard.svelte +11 -8
  55. package/dist/dialog/DialogWrapper.svelte +10 -0
  56. package/dist/{common-editor/DateInput.svelte.d.ts → dialog/DialogWrapper.svelte.d.ts} +6 -13
  57. package/dist/dialog/Dialogs.js +1 -1
  58. package/dist/dialog/ModalResult.d.ts +5 -0
  59. package/dist/dialog/ModalResult.js +5 -0
  60. package/dist/drawer/Drawer.svelte +1 -1
  61. package/dist/form-field/FormField.svelte +2 -1
  62. package/dist/form-field/FormField.svelte.d.ts +1 -0
  63. package/dist/form-panel/flex-row-form/FormContainer.svelte +1 -1
  64. package/dist/form-panel/grid-form/EmptyCell.svelte +8 -7
  65. package/dist/form-panel/grid-form/EmptyCell.svelte.d.ts +2 -2
  66. package/dist/form-panel/grid-form/GridField.svelte +1 -1
  67. package/dist/i18nRes/i18nRes.d.ts +2 -0
  68. package/dist/i18nRes/i18nRes.js +44 -0
  69. package/dist/i18nRes/index.d.ts +2 -0
  70. package/dist/i18nRes/index.js +2 -0
  71. package/dist/image-files/ImageFilesField.svelte +1 -1
  72. package/dist/indicator/IndicatorBoard.svelte +1 -1
  73. package/dist/inline-cell-editor/InvalidEditor.svelte +1 -0
  74. package/dist/input-options-select/InputOptionsSelect.svelte +6 -6
  75. package/dist/input-options-select/InputOptionsSelect.svelte.d.ts +1 -1
  76. package/dist/layout/flex/FlexBlock.svelte +1 -1
  77. package/dist/lookup-editor/LookupEditor.svelte +1 -1
  78. package/dist/memo-editor/MemoEditor.svelte +2 -2
  79. package/dist/message-box/IMessageBox.d.ts +2 -4
  80. package/dist/message-box/IMessageBox.js +4 -6
  81. package/dist/message-box/MessageBoxBoard.svelte +1 -3
  82. package/dist/options-multi-select/OptionsMultiSelect.svelte +1 -1
  83. package/dist/prompts-text-editor/PromptsTextEditor.svelte +2 -2
  84. package/dist/prompts-text-editor/PromptsTextEditor.svelte.d.ts +1 -1
  85. package/dist/property-editor/PropertyEditor.svelte +3 -3
  86. package/dist/text-editor/PasswordEditor.svelte +1 -1
  87. package/dist/transfer/Transfer.svelte +17 -12
  88. package/dist/transfer/Transfer.svelte.d.ts +0 -1
  89. package/dist/unit-number-editor/UnitNumberEditor.svelte +2 -2
  90. package/docs/buttons/cn/ActionBar.md +459 -0
  91. package/docs/buttons/cn/Button.md +228 -0
  92. package/docs/buttons/cn/IconButton.md +294 -0
  93. package/docs/buttons/cn/README.md +600 -0
  94. package/docs/buttons/cn/TextButton.md +163 -0
  95. package/docs/buttons/en/ActionBar.md +459 -0
  96. package/docs/buttons/en/Button.md +228 -0
  97. package/docs/buttons/en/IconButton.md +294 -0
  98. package/docs/buttons/en/README.md +600 -0
  99. package/docs/buttons/en/TextButton.md +163 -0
  100. package/docs/containers/cn/accordion.md +622 -0
  101. package/docs/containers/cn/box.md +445 -0
  102. package/docs/containers/cn/card.md +404 -0
  103. package/docs/containers/cn/page.md +576 -0
  104. package/docs/containers/cn/tabs.md +686 -0
  105. package/docs/containers/en/accordion.md +622 -0
  106. package/docs/containers/en/box.md +445 -0
  107. package/docs/containers/en/card.md +404 -0
  108. package/docs/containers/en/page.md +576 -0
  109. package/docs/containers/en/tabs.md +686 -0
  110. package/docs/data-display/cn/concise-data-table.md +731 -0
  111. package/docs/data-display/cn/listbox.md +786 -0
  112. package/docs/data-display/cn/treeview.md +863 -0
  113. package/docs/data-display/en/concise-data-table.md +732 -0
  114. package/docs/data-display/en/listbox.md +786 -0
  115. package/docs/data-display/en/treeview.md +863 -0
  116. package/docs/data-table/cn/README.md +596 -0
  117. package/docs/data-table/en/README.md +596 -0
  118. package/docs/dialog/cn/README.md +767 -0
  119. package/docs/dialog/en/README.md +767 -0
  120. package/docs/form/cn/README.md +847 -0
  121. package/docs/form/en/README.md +847 -0
  122. package/docs/form-components/cn/README.md +495 -0
  123. package/docs/form-components/cn/attachmentfilesfield.md +757 -0
  124. package/docs/form-components/cn/cascade-options-select.md +374 -0
  125. package/docs/form-components/cn/checkbox.md +58 -0
  126. package/docs/form-components/cn/date-picker.md +138 -0
  127. package/docs/form-components/cn/datetime-picker.md +228 -0
  128. package/docs/form-components/cn/group-checkbox.md +405 -0
  129. package/docs/form-components/cn/group-radiobox.md +525 -0
  130. package/docs/form-components/cn/imagefilesfield.md +651 -0
  131. package/docs/form-components/cn/input-options-select.md +250 -0
  132. package/docs/form-components/cn/lookup-editor.md +406 -0
  133. package/docs/form-components/cn/memoeditor.md +409 -0
  134. package/docs/form-components/cn/number-editor.md +261 -0
  135. package/docs/form-components/cn/options-multi-select.md +177 -0
  136. package/docs/form-components/cn/options-select.md +83 -0
  137. package/docs/form-components/cn/passwordeditor.md +538 -0
  138. package/docs/form-components/cn/prompts-text-editor.md +241 -0
  139. package/docs/form-components/cn/radiobutton.md +84 -0
  140. package/docs/form-components/cn/switch.md +178 -0
  141. package/docs/form-components/cn/text-editor.md +142 -0
  142. package/docs/form-components/cn/time-editor.md +159 -0
  143. package/docs/form-components/cn/unit-number-editor.md +164 -0
  144. package/docs/form-components/en/README.md +463 -0
  145. package/docs/form-components/en/attachmentfilesfield.md +757 -0
  146. package/docs/form-components/en/cascade-options-select.md +358 -0
  147. package/docs/form-components/en/checkbox.md +59 -0
  148. package/docs/form-components/en/date-picker.md +138 -0
  149. package/docs/form-components/en/datetime-picker.md +228 -0
  150. package/docs/form-components/en/group-checkbox.md +395 -0
  151. package/docs/form-components/en/group-radiobox.md +498 -0
  152. package/docs/form-components/en/imagefilesfield.md +651 -0
  153. package/docs/form-components/en/input-options-select.md +239 -0
  154. package/docs/form-components/en/lookup-editor.md +389 -0
  155. package/docs/form-components/en/memoeditor.md +409 -0
  156. package/docs/form-components/en/number-editor.md +249 -0
  157. package/docs/form-components/en/options-multi-select.md +177 -0
  158. package/docs/form-components/en/options-select.md +83 -0
  159. package/docs/form-components/en/passwordeditor.md +538 -0
  160. package/docs/form-components/en/prompts-text-editor.md +241 -0
  161. package/docs/form-components/en/radiobutton.md +84 -0
  162. package/docs/form-components/en/switch.md +180 -0
  163. package/docs/form-components/en/text-editor.md +142 -0
  164. package/docs/form-components/en/time-editor.md +159 -0
  165. package/docs/form-components/en/unit-number-editor.md +164 -0
  166. package/docs/form-layout/cn/criteria-field.md +652 -0
  167. package/docs/form-layout/cn/flex-form.md +454 -0
  168. package/docs/form-layout/cn/flex-row-form.md +542 -0
  169. package/docs/form-layout/cn/form-field.md +923 -0
  170. package/docs/form-layout/cn/grid-form.md +676 -0
  171. package/docs/form-layout/en/criteria-field.md +652 -0
  172. package/docs/form-layout/en/flex-form.md +454 -0
  173. package/docs/form-layout/en/flex-row-form.md +542 -0
  174. package/docs/form-layout/en/form-field.md +923 -0
  175. package/docs/form-layout/en/grid-form.md +676 -0
  176. package/docs/global/cn/README.md +534 -0
  177. package/docs/global/en/README.md +534 -0
  178. package/docs/layout/cn/classiclayout.md +460 -0
  179. package/docs/layout/cn/headerlayout.md +524 -0
  180. package/docs/layout/cn/sidebarlayout.md +263 -0
  181. package/docs/layout/en/classiclayout.md +951 -0
  182. package/docs/layout/en/headerlayout.md +524 -0
  183. package/docs/layout/en/sidebarlayout.md +262 -0
  184. package/docs/misc/cn/README.md +1217 -0
  185. package/docs/misc/en/README.md +1217 -0
  186. package/docs/navigation/cn/breadcrumb.md +291 -0
  187. package/docs/navigation/cn/navigator.md +322 -0
  188. package/docs/navigation/cn/navigatormenu.md +512 -0
  189. package/docs/navigation/cn/progressbar.md +199 -0
  190. package/docs/navigation/cn/progressstepbar.md +299 -0
  191. package/docs/navigation/en/breadcrumb.md +291 -0
  192. package/docs/navigation/en/navigator.md +322 -0
  193. package/docs/navigation/en/navigatormenu.md +512 -0
  194. package/docs/navigation/en/progressbar.md +199 -0
  195. package/docs/navigation/en/progressstepbar.md +299 -0
  196. package/docs/search-filters/cn/daterange.md +1156 -0
  197. package/docs/search-filters/cn/numberrange.md +688 -0
  198. package/docs/search-filters/cn/searchbox.md +1582 -0
  199. package/docs/search-filters/en/daterange.md +1156 -0
  200. package/docs/search-filters/en/numberrange.md +688 -0
  201. package/docs/search-filters/en/searchbox.md +1582 -0
  202. package/package.json +53 -13
  203. package/dist/common-editor/DateInput.svelte +0 -27
  204. package/dist/i18n_resources/index.d.ts +0 -3
  205. package/dist/i18n_resources/index.js +0 -3
  206. /package/dist/{i18n_resources → i18nRes}/uniface_cn_resource.d.ts +0 -0
  207. /package/dist/{i18n_resources → i18nRes}/uniface_cn_resource.js +0 -0
  208. /package/dist/{i18n_resources → i18nRes}/uniface_en_resource.d.ts +0 -0
  209. /package/dist/{i18n_resources → i18nRes}/uniface_en_resource.js +0 -0
@@ -0,0 +1,1582 @@
1
+ # SearchBox 搜索框组件
2
+
3
+ 一个多功能搜索输入组件,为基于文本的查询和过滤提供清洁的界面,具有内置搜索图标、清除功能和对国际文本输入方法的支持。
4
+
5
+ ## 概述
6
+
7
+ SearchBox 组件通过 Google Material Design 图标、自动清除功能和对国际用户的全面输入方法支持,提供精美的搜索体验。它为在应用程序中实现搜索和过滤功能提供一致的界面,具有内置的焦点管理和多字节字符输入的合成事件处理。
8
+
9
+ ## 安装
10
+
11
+ ```typescript
12
+ import SearchBox from '@ticatec/uniface-element/SearchBox';
13
+ import type { OnChangeHandler } from '@ticatec/uniface-element/SearchBox';
14
+ ```
15
+
16
+ ## 基本用法
17
+
18
+ ```svelte
19
+ <script lang="ts">
20
+ import SearchBox from '@ticatec/uniface-element/SearchBox';
21
+
22
+ let searchQuery: string | null = null;
23
+
24
+ function handleSearch(query: string | null) {
25
+ console.log('搜索查询:', query);
26
+ }
27
+ </script>
28
+
29
+ <SearchBox
30
+ bind:value={searchQuery}
31
+ onChange={handleSearch}
32
+ placeholder="搜索..."
33
+ />
34
+ ```
35
+
36
+ ## 属性
37
+
38
+ | 属性 | 类型 | 默认值 | 描述 |
39
+ |------|------|--------|------|
40
+ | `variant` | `'' \| 'plain' \| 'outlined' \| 'filled'` | `''` | 视觉样式变体 |
41
+ | `compact` | `boolean` | `false` | 启用紧凑布局以适应密集表单 |
42
+ | `style` | `string` | `''` | 自定义内联样式 |
43
+ | `value` | `string \| null` | `null` | 当前搜索文本值(可绑定) |
44
+ | `disabled` | `boolean` | `false` | 禁用输入交互 |
45
+ | `onChange` | `OnChangeHandler<string \| null>` | `null` | 变更事件处理器 |
46
+ | `placeholder` | `string \| null` | `null` | 占位符文本 |
47
+
48
+ ## 方法
49
+
50
+ | 方法 | 描述 |
51
+ |------|------|
52
+ | `focus()` | 以编程方式聚焦搜索输入框 |
53
+
54
+ ## 类型定义
55
+
56
+ ```typescript
57
+ /**
58
+ * 变更事件处理器类型
59
+ */
60
+ type OnChangeHandler<T> = (value: T) => void;
61
+ ```
62
+
63
+ ## 功能特性
64
+
65
+ - **内置搜索图标**:Google Material Design 搜索图标提供清晰的视觉指示
66
+ - **自动清除功能**:输入文本时出现清除按钮
67
+ - **国际化支持**:多字节字符的输入法合成支持
68
+ - **焦点管理**:以编程方式进行焦点控制以获得更好的用户体验
69
+ - **一致样式**:通过变体支持与其他表单组件匹配
70
+ - **可访问性**:适当的 ARIA 标签和键盘导航
71
+
72
+ ## 使用示例
73
+
74
+ ### 产品搜索界面
75
+
76
+ ```svelte
77
+ <script lang="ts">
78
+ import SearchBox from '@ticatec/uniface-element/SearchBox';
79
+
80
+ let searchQuery: string | null = null;
81
+
82
+ // 模拟产品数据
83
+ let allProducts = [
84
+ { id: 1, name: '无线耳机', category: '电子产品', price: 899.99, brand: '音质科技' },
85
+ { id: 2, name: '智能手表', category: '电子产品', price: 2199.99, brand: '时间科技' },
86
+ { id: 3, name: '笔记本支架', category: '配件', price: 299.99, brand: '办公专家' },
87
+ { id: 4, name: 'USB-C 数据线', category: '配件', price: 99.99, brand: '连接助手' },
88
+ { id: 5, name: '蓝牙音箱', category: '电子产品', price: 499.99, brand: '声波制造' },
89
+ { id: 6, name: '手机保护套', category: '配件', price: 149.99, brand: '全方位保护' },
90
+ { id: 7, name: '无线充电器', category: '电子产品', price: 199.99, brand: '能量传输' }
91
+ ];
92
+
93
+ let filteredProducts = [];
94
+ let isSearching = false;
95
+
96
+ // 防抖搜索函数
97
+ let searchTimeout: number;
98
+
99
+ function performSearch(query: string | null) {
100
+ clearTimeout(searchTimeout);
101
+ isSearching = true;
102
+
103
+ searchTimeout = setTimeout(() => {
104
+ if (!query || query.trim() === '') {
105
+ filteredProducts = allProducts;
106
+ } else {
107
+ const searchTerm = query.toLowerCase().trim();
108
+ filteredProducts = allProducts.filter(product =>
109
+ product.name.toLowerCase().includes(searchTerm) ||
110
+ product.category.toLowerCase().includes(searchTerm) ||
111
+ product.brand.toLowerCase().includes(searchTerm)
112
+ );
113
+ }
114
+ isSearching = false;
115
+ }, 300); // 300ms 防抖延迟
116
+ }
117
+
118
+ // 初始加载
119
+ filteredProducts = allProducts;
120
+
121
+ // 响应式搜索
122
+ $: performSearch(searchQuery);
123
+
124
+ function clearSearch() {
125
+ searchQuery = null;
126
+ }
127
+
128
+ function handleProductClick(product) {
129
+ console.log('选中产品:', product);
130
+ alert(`选中:${product.name} - ¥${product.price}`);
131
+ }
132
+ </script>
133
+
134
+ <div class="product-search">
135
+ <header class="search-header">
136
+ <h2>产品目录</h2>
137
+ <p>搜索我们的产品库存</p>
138
+ </header>
139
+
140
+ <div class="search-controls">
141
+ <SearchBox
142
+ bind:value={searchQuery}
143
+ placeholder="搜索产品、分类或品牌..."
144
+ variant="outlined"
145
+ style="width: 100%; max-width: 500px;"
146
+ />
147
+
148
+ {#if searchQuery}
149
+ <div class="search-info">
150
+ <span class="search-results">
151
+ {isSearching ? '搜索中...' : `找到 ${filteredProducts.length} 个产品`}
152
+ </span>
153
+ <button class="clear-search-btn" on:click={clearSearch}>
154
+ 清除搜索
155
+ </button>
156
+ </div>
157
+ {/if}
158
+ </div>
159
+
160
+ <div class="products-grid">
161
+ {#if isSearching}
162
+ <div class="loading-state">
163
+ <div class="loading-spinner">🔍</div>
164
+ <p>正在搜索产品...</p>
165
+ </div>
166
+ {:else if filteredProducts.length > 0}
167
+ {#each filteredProducts as product}
168
+ <div
169
+ class="product-card"
170
+ on:click={() => handleProductClick(product)}
171
+ tabindex="0"
172
+ role="button"
173
+ >
174
+ <div class="product-header">
175
+ <h3 class="product-name">{product.name}</h3>
176
+ <span class="product-price">¥{product.price}</span>
177
+ </div>
178
+ <div class="product-details">
179
+ <span class="product-category">{product.category}</span>
180
+ <span class="product-brand">{product.brand}</span>
181
+ </div>
182
+ </div>
183
+ {/each}
184
+ {:else}
185
+ <div class="no-results">
186
+ <div class="no-results-icon">🔍</div>
187
+ <h3>未找到产品</h3>
188
+ <p>无法找到与"{searchQuery}"匹配的产品</p>
189
+ <button class="reset-search-btn" on:click={clearSearch}>
190
+ 查看所有产品
191
+ </button>
192
+ </div>
193
+ {/if}
194
+ </div>
195
+ </div>
196
+
197
+ <style>
198
+ .product-search {
199
+ max-width: 1200px;
200
+ margin: 0 auto;
201
+ padding: 20px;
202
+ }
203
+
204
+ .search-header {
205
+ text-align: center;
206
+ margin-bottom: 30px;
207
+ }
208
+
209
+ .search-header h2 {
210
+ color: #333;
211
+ margin: 0 0 8px 0;
212
+ }
213
+
214
+ .search-header p {
215
+ color: #666;
216
+ margin: 0;
217
+ }
218
+
219
+ .search-controls {
220
+ display: flex;
221
+ flex-direction: column;
222
+ align-items: center;
223
+ gap: 16px;
224
+ margin-bottom: 30px;
225
+ }
226
+
227
+ .search-info {
228
+ display: flex;
229
+ align-items: center;
230
+ gap: 16px;
231
+ font-size: 0.9rem;
232
+ }
233
+
234
+ .search-results {
235
+ color: #666;
236
+ }
237
+
238
+ .clear-search-btn {
239
+ padding: 4px 8px;
240
+ background: #f8f9fa;
241
+ border: 1px solid #dee2e6;
242
+ border-radius: 4px;
243
+ cursor: pointer;
244
+ font-size: 0.8rem;
245
+ }
246
+
247
+ .clear-search-btn:hover {
248
+ background: #e9ecef;
249
+ }
250
+
251
+ .products-grid {
252
+ display: grid;
253
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
254
+ gap: 20px;
255
+ min-height: 200px;
256
+ }
257
+
258
+ .product-card {
259
+ background: white;
260
+ border: 1px solid #e9ecef;
261
+ border-radius: 8px;
262
+ padding: 20px;
263
+ cursor: pointer;
264
+ transition: all 0.2s;
265
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
266
+ }
267
+
268
+ .product-card:hover,
269
+ .product-card:focus {
270
+ border-color: #007bff;
271
+ transform: translateY(-2px);
272
+ box-shadow: 0 4px 8px rgba(0,123,255,0.15);
273
+ outline: none;
274
+ }
275
+
276
+ .product-header {
277
+ display: flex;
278
+ justify-content: space-between;
279
+ align-items: flex-start;
280
+ margin-bottom: 12px;
281
+ }
282
+
283
+ .product-name {
284
+ margin: 0;
285
+ color: #333;
286
+ font-size: 1.2rem;
287
+ font-weight: 600;
288
+ }
289
+
290
+ .product-price {
291
+ color: #007bff;
292
+ font-weight: 700;
293
+ font-size: 1.1rem;
294
+ }
295
+
296
+ .product-details {
297
+ display: flex;
298
+ justify-content: space-between;
299
+ align-items: center;
300
+ }
301
+
302
+ .product-category {
303
+ background: #e9ecef;
304
+ color: #495057;
305
+ padding: 4px 8px;
306
+ border-radius: 12px;
307
+ font-size: 0.75rem;
308
+ font-weight: 500;
309
+ }
310
+
311
+ .product-brand {
312
+ color: #666;
313
+ font-size: 0.9rem;
314
+ font-style: italic;
315
+ }
316
+
317
+ .loading-state,
318
+ .no-results {
319
+ grid-column: 1 / -1;
320
+ text-align: center;
321
+ padding: 60px 20px;
322
+ color: #666;
323
+ }
324
+
325
+ .loading-spinner {
326
+ font-size: 3rem;
327
+ animation: pulse 1.5s infinite;
328
+ }
329
+
330
+ .no-results-icon {
331
+ font-size: 4rem;
332
+ opacity: 0.5;
333
+ margin-bottom: 16px;
334
+ }
335
+
336
+ .no-results h3 {
337
+ color: #333;
338
+ margin: 0 0 8px 0;
339
+ }
340
+
341
+ .no-results p {
342
+ margin: 0 0 20px 0;
343
+ }
344
+
345
+ .reset-search-btn {
346
+ padding: 8px 16px;
347
+ background: #007bff;
348
+ color: white;
349
+ border: none;
350
+ border-radius: 4px;
351
+ cursor: pointer;
352
+ font-size: 0.9rem;
353
+ }
354
+
355
+ .reset-search-btn:hover {
356
+ background: #0056b3;
357
+ }
358
+
359
+ @keyframes pulse {
360
+ 0%, 100% { opacity: 1; }
361
+ 50% { opacity: 0.5; }
362
+ }
363
+
364
+ @media (max-width: 768px) {
365
+ .products-grid {
366
+ grid-template-columns: 1fr;
367
+ }
368
+
369
+ .product-header {
370
+ flex-direction: column;
371
+ align-items: flex-start;
372
+ gap: 8px;
373
+ }
374
+
375
+ .product-details {
376
+ flex-direction: column;
377
+ align-items: flex-start;
378
+ gap: 8px;
379
+ }
380
+ }
381
+ </style>
382
+ ```
383
+
384
+ ### 用户目录搜索
385
+
386
+ ```svelte
387
+ <script lang="ts">
388
+ import SearchBox from '@ticatec/uniface-element/SearchBox';
389
+
390
+ let searchQuery: string | null = null;
391
+ let searchBoxRef: any;
392
+
393
+ // 模拟用户数据
394
+ let allUsers = [
395
+ { id: 1, name: '张小明', email: 'zhangxiaoming@company.com', department: '工程部', role: '高级开发工程师', status: '在职' },
396
+ { id: 2, name: '李小红', email: 'lixiaohong@company.com', department: '市场部', role: '市场经理', status: '在职' },
397
+ { id: 3, name: '王小刚', email: 'wangxiaogang@company.com', department: '销售部', role: '销售代表', status: '请假' },
398
+ { id: 4, name: '赵小丽', email: 'zhaoxiaoli@company.com', department: '人事部', role: '人事专员', status: '在职' },
399
+ { id: 5, name: '陈小华', email: 'chenxiaohua@company.com', department: '工程部', role: '技术主管', status: '远程' },
400
+ { id: 6, name: '孙小强', email: 'sunxiaoqiang@company.com', department: '财务部', role: '会计', status: '在职' }
401
+ ];
402
+
403
+ let filteredUsers = [];
404
+ let selectedUser = null;
405
+ let searchMode = 'all'; // 'all', 'name', 'email', 'department'
406
+
407
+ function performUserSearch(query: string | null) {
408
+ if (!query || query.trim() === '') {
409
+ filteredUsers = allUsers;
410
+ return;
411
+ }
412
+
413
+ const searchTerm = query.toLowerCase().trim();
414
+ filteredUsers = allUsers.filter(user => {
415
+ switch (searchMode) {
416
+ case 'name':
417
+ return user.name.toLowerCase().includes(searchTerm);
418
+ case 'email':
419
+ return user.email.toLowerCase().includes(searchTerm);
420
+ case 'department':
421
+ return user.department.toLowerCase().includes(searchTerm);
422
+ default: // 'all'
423
+ return user.name.toLowerCase().includes(searchTerm) ||
424
+ user.email.toLowerCase().includes(searchTerm) ||
425
+ user.department.toLowerCase().includes(searchTerm) ||
426
+ user.role.toLowerCase().includes(searchTerm);
427
+ }
428
+ });
429
+ }
430
+
431
+ // 初始加载
432
+ filteredUsers = allUsers;
433
+
434
+ // 响应式搜索
435
+ $: performUserSearch(searchQuery);
436
+
437
+ function selectUser(user) {
438
+ selectedUser = user;
439
+ }
440
+
441
+ function focusSearch() {
442
+ searchBoxRef?.focus();
443
+ }
444
+
445
+ function resetSearch() {
446
+ searchQuery = null;
447
+ selectedUser = null;
448
+ searchMode = 'all';
449
+ }
450
+
451
+ // 搜索键盘快捷键
452
+ function handleKeydown(event) {
453
+ if (event.ctrlKey && event.key === 'k') {
454
+ event.preventDefault();
455
+ focusSearch();
456
+ }
457
+ }
458
+ </script>
459
+
460
+ <svelte:window on:keydown={handleKeydown} />
461
+
462
+ <div class="user-directory">
463
+ <header class="directory-header">
464
+ <h2>员工目录</h2>
465
+ <p>搜索和浏览公司员工</p>
466
+ <div class="keyboard-hint">
467
+ <kbd>Ctrl</kbd> + <kbd>K</kbd> 聚焦搜索
468
+ </div>
469
+ </header>
470
+
471
+ <div class="search-section">
472
+ <div class="search-input-container">
473
+ <SearchBox
474
+ bind:this={searchBoxRef}
475
+ bind:value={searchQuery}
476
+ placeholder="搜索员工..."
477
+ variant="outlined"
478
+ style="width: 100%;"
479
+ />
480
+ </div>
481
+
482
+ <div class="search-options">
483
+ <label>搜索范围:</label>
484
+ <select bind:value={searchMode} class="search-mode-select">
485
+ <option value="all">所有字段</option>
486
+ <option value="name">仅姓名</option>
487
+ <option value="email">仅邮箱</option>
488
+ <option value="department">仅部门</option>
489
+ </select>
490
+
491
+ {#if searchQuery || selectedUser}
492
+ <button class="reset-btn" on:click={resetSearch}>重置</button>
493
+ {/if}
494
+ </div>
495
+ </div>
496
+
497
+ <div class="directory-content">
498
+ <div class="users-list">
499
+ <div class="list-header">
500
+ <h3>搜索结果</h3>
501
+ <span class="result-count">{filteredUsers.length} 名用户</span>
502
+ </div>
503
+
504
+ <div class="users-grid">
505
+ {#each filteredUsers as user}
506
+ <div
507
+ class="user-card"
508
+ class:selected={selectedUser?.id === user.id}
509
+ on:click={() => selectUser(user)}
510
+ tabindex="0"
511
+ role="button"
512
+ >
513
+ <div class="user-avatar">
514
+ {user.name.substring(0, 2)}
515
+ </div>
516
+ <div class="user-info">
517
+ <div class="user-name">{user.name}</div>
518
+ <div class="user-email">{user.email}</div>
519
+ <div class="user-role">{user.role}</div>
520
+ <div class="user-department">{user.department}</div>
521
+ </div>
522
+ <div class="user-status">
523
+ <span class="status-badge status-{user.status.replace(' ', '-')}">{user.status}</span>
524
+ </div>
525
+ </div>
526
+ {/each}
527
+
528
+ {#if filteredUsers.length === 0}
529
+ <div class="no-users-found">
530
+ <h4>未找到员工</h4>
531
+ <p>
532
+ {#if searchQuery}
533
+ 没有员工匹配您的搜索条件"{searchQuery}"
534
+ {:else}
535
+ 目录中没有员工
536
+ {/if}
537
+ </p>
538
+ {#if searchQuery}
539
+ <button class="clear-search-btn" on:click={() => searchQuery = null}>清除搜索</button>
540
+ {/if}
541
+ </div>
542
+ {/if}
543
+ </div>
544
+ </div>
545
+
546
+ {#if selectedUser}
547
+ <div class="user-details">
548
+ <div class="details-header">
549
+ <h3>员工详情</h3>
550
+ <button class="close-details-btn" on:click={() => selectedUser = null}>×</button>
551
+ </div>
552
+
553
+ <div class="details-content">
554
+ <div class="detail-avatar">
555
+ {selectedUser.name.substring(0, 2)}
556
+ </div>
557
+
558
+ <div class="detail-info">
559
+ <h4>{selectedUser.name}</h4>
560
+ <div class="detail-row">
561
+ <span class="detail-label">邮箱:</span>
562
+ <span class="detail-value">
563
+ <a href="mailto:{selectedUser.email}">{selectedUser.email}</a>
564
+ </span>
565
+ </div>
566
+ <div class="detail-row">
567
+ <span class="detail-label">职位:</span>
568
+ <span class="detail-value">{selectedUser.role}</span>
569
+ </div>
570
+ <div class="detail-row">
571
+ <span class="detail-label">部门:</span>
572
+ <span class="detail-value">{selectedUser.department}</span>
573
+ </div>
574
+ <div class="detail-row">
575
+ <span class="detail-label">状态:</span>
576
+ <span class="detail-value">
577
+ <span class="status-badge status-{selectedUser.status.replace(' ', '-')}">{selectedUser.status}</span>
578
+ </span>
579
+ </div>
580
+ </div>
581
+
582
+ <div class="detail-actions">
583
+ <button class="action-btn primary">发送消息</button>
584
+ <button class="action-btn secondary">查看资料</button>
585
+ <button class="action-btn secondary">安排会议</button>
586
+ </div>
587
+ </div>
588
+ </div>
589
+ {/if}
590
+ </div>
591
+ </div>
592
+
593
+ <style>
594
+ .user-directory {
595
+ max-width: 1400px;
596
+ margin: 0 auto;
597
+ padding: 20px;
598
+ height: 100vh;
599
+ display: flex;
600
+ flex-direction: column;
601
+ }
602
+
603
+ .directory-header {
604
+ text-align: center;
605
+ margin-bottom: 30px;
606
+ }
607
+
608
+ .directory-header h2 {
609
+ color: #333;
610
+ margin: 0 0 8px 0;
611
+ }
612
+
613
+ .directory-header p {
614
+ color: #666;
615
+ margin: 0 0 12px 0;
616
+ }
617
+
618
+ .keyboard-hint {
619
+ font-size: 0.8rem;
620
+ color: #888;
621
+ }
622
+
623
+ .keyboard-hint kbd {
624
+ background: #f8f9fa;
625
+ border: 1px solid #dee2e6;
626
+ border-radius: 3px;
627
+ padding: 2px 6px;
628
+ font-size: 0.7rem;
629
+ }
630
+
631
+ .search-section {
632
+ margin-bottom: 30px;
633
+ }
634
+
635
+ .search-input-container {
636
+ max-width: 600px;
637
+ margin: 0 auto 16px;
638
+ }
639
+
640
+ .search-options {
641
+ display: flex;
642
+ align-items: center;
643
+ justify-content: center;
644
+ gap: 16px;
645
+ font-size: 0.9rem;
646
+ }
647
+
648
+ .search-mode-select {
649
+ padding: 4px 8px;
650
+ border: 1px solid #dee2e6;
651
+ border-radius: 4px;
652
+ background: white;
653
+ }
654
+
655
+ .reset-btn {
656
+ padding: 4px 12px;
657
+ background: #6c757d;
658
+ color: white;
659
+ border: none;
660
+ border-radius: 4px;
661
+ cursor: pointer;
662
+ font-size: 0.8rem;
663
+ }
664
+
665
+ .reset-btn:hover {
666
+ background: #5a6268;
667
+ }
668
+
669
+ .directory-content {
670
+ flex: 1;
671
+ display: flex;
672
+ gap: 20px;
673
+ min-height: 0;
674
+ }
675
+
676
+ .users-list {
677
+ flex: 1;
678
+ display: flex;
679
+ flex-direction: column;
680
+ min-height: 0;
681
+ }
682
+
683
+ .list-header {
684
+ display: flex;
685
+ justify-content: space-between;
686
+ align-items: center;
687
+ margin-bottom: 16px;
688
+ padding-bottom: 8px;
689
+ border-bottom: 2px solid #e9ecef;
690
+ }
691
+
692
+ .list-header h3 {
693
+ margin: 0;
694
+ color: #333;
695
+ }
696
+
697
+ .result-count {
698
+ color: #666;
699
+ font-size: 0.9rem;
700
+ }
701
+
702
+ .users-grid {
703
+ flex: 1;
704
+ overflow-y: auto;
705
+ display: grid;
706
+ grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
707
+ gap: 16px;
708
+ padding-right: 8px;
709
+ }
710
+
711
+ .user-card {
712
+ background: white;
713
+ border: 2px solid #e9ecef;
714
+ border-radius: 8px;
715
+ padding: 16px;
716
+ cursor: pointer;
717
+ transition: all 0.2s;
718
+ display: flex;
719
+ align-items: center;
720
+ gap: 16px;
721
+ height: fit-content;
722
+ }
723
+
724
+ .user-card:hover,
725
+ .user-card:focus {
726
+ border-color: #007bff;
727
+ box-shadow: 0 2px 4px rgba(0,123,255,0.15);
728
+ outline: none;
729
+ }
730
+
731
+ .user-card.selected {
732
+ border-color: #007bff;
733
+ background: #f0f8ff;
734
+ }
735
+
736
+ .user-avatar {
737
+ width: 48px;
738
+ height: 48px;
739
+ border-radius: 50%;
740
+ background: #007bff;
741
+ color: white;
742
+ display: flex;
743
+ align-items: center;
744
+ justify-content: center;
745
+ font-weight: 600;
746
+ font-size: 1.1rem;
747
+ flex-shrink: 0;
748
+ }
749
+
750
+ .user-info {
751
+ flex: 1;
752
+ min-width: 0;
753
+ }
754
+
755
+ .user-name {
756
+ font-weight: 600;
757
+ color: #333;
758
+ margin-bottom: 4px;
759
+ }
760
+
761
+ .user-email {
762
+ color: #666;
763
+ font-size: 0.9rem;
764
+ margin-bottom: 4px;
765
+ overflow: hidden;
766
+ text-overflow: ellipsis;
767
+ white-space: nowrap;
768
+ }
769
+
770
+ .user-role {
771
+ color: #495057;
772
+ font-size: 0.85rem;
773
+ font-weight: 500;
774
+ }
775
+
776
+ .user-department {
777
+ color: #6c757d;
778
+ font-size: 0.8rem;
779
+ }
780
+
781
+ .status-badge {
782
+ padding: 3px 8px;
783
+ border-radius: 12px;
784
+ font-size: 0.7rem;
785
+ font-weight: 500;
786
+ text-transform: uppercase;
787
+ }
788
+
789
+ .status-在职 {
790
+ background: #d4edda;
791
+ color: #155724;
792
+ }
793
+
794
+ .status-远程 {
795
+ background: #cce5ff;
796
+ color: #004085;
797
+ }
798
+
799
+ .status-请假 {
800
+ background: #fff3cd;
801
+ color: #856404;
802
+ }
803
+
804
+ .no-users-found {
805
+ grid-column: 1 / -1;
806
+ text-align: center;
807
+ padding: 60px 20px;
808
+ color: #666;
809
+ }
810
+
811
+ .no-users-found h4 {
812
+ color: #333;
813
+ margin: 0 0 8px 0;
814
+ }
815
+
816
+ .clear-search-btn {
817
+ margin-top: 16px;
818
+ padding: 8px 16px;
819
+ background: #007bff;
820
+ color: white;
821
+ border: none;
822
+ border-radius: 4px;
823
+ cursor: pointer;
824
+ }
825
+
826
+ .clear-search-btn:hover {
827
+ background: #0056b3;
828
+ }
829
+
830
+ .user-details {
831
+ width: 400px;
832
+ background: white;
833
+ border-radius: 8px;
834
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
835
+ display: flex;
836
+ flex-direction: column;
837
+ max-height: 600px;
838
+ }
839
+
840
+ .details-header {
841
+ display: flex;
842
+ justify-content: space-between;
843
+ align-items: center;
844
+ padding: 20px 20px 0;
845
+ }
846
+
847
+ .details-header h3 {
848
+ margin: 0;
849
+ color: #333;
850
+ }
851
+
852
+ .close-details-btn {
853
+ background: none;
854
+ border: none;
855
+ font-size: 24px;
856
+ cursor: pointer;
857
+ color: #666;
858
+ padding: 0;
859
+ width: 30px;
860
+ height: 30px;
861
+ display: flex;
862
+ align-items: center;
863
+ justify-content: center;
864
+ }
865
+
866
+ .close-details-btn:hover {
867
+ color: #333;
868
+ background: #f8f9fa;
869
+ border-radius: 50%;
870
+ }
871
+
872
+ .details-content {
873
+ padding: 20px;
874
+ flex: 1;
875
+ overflow-y: auto;
876
+ }
877
+
878
+ .detail-avatar {
879
+ width: 80px;
880
+ height: 80px;
881
+ border-radius: 50%;
882
+ background: #007bff;
883
+ color: white;
884
+ display: flex;
885
+ align-items: center;
886
+ justify-content: center;
887
+ font-weight: 600;
888
+ font-size: 1.8rem;
889
+ margin: 0 auto 20px;
890
+ }
891
+
892
+ .detail-info h4 {
893
+ text-align: center;
894
+ color: #333;
895
+ margin: 0 0 20px 0;
896
+ font-size: 1.3rem;
897
+ }
898
+
899
+ .detail-row {
900
+ display: flex;
901
+ justify-content: space-between;
902
+ align-items: center;
903
+ padding: 12px 0;
904
+ border-bottom: 1px solid #f0f0f0;
905
+ }
906
+
907
+ .detail-label {
908
+ font-weight: 500;
909
+ color: #555;
910
+ }
911
+
912
+ .detail-value a {
913
+ color: #007bff;
914
+ text-decoration: none;
915
+ }
916
+
917
+ .detail-value a:hover {
918
+ text-decoration: underline;
919
+ }
920
+
921
+ .detail-actions {
922
+ margin-top: 20px;
923
+ display: flex;
924
+ flex-direction: column;
925
+ gap: 8px;
926
+ }
927
+
928
+ .action-btn {
929
+ padding: 8px 16px;
930
+ border: none;
931
+ border-radius: 4px;
932
+ cursor: pointer;
933
+ font-size: 0.9rem;
934
+ font-weight: 500;
935
+ }
936
+
937
+ .action-btn.primary {
938
+ background: #007bff;
939
+ color: white;
940
+ }
941
+
942
+ .action-btn.primary:hover {
943
+ background: #0056b3;
944
+ }
945
+
946
+ .action-btn.secondary {
947
+ background: #f8f9fa;
948
+ color: #495057;
949
+ border: 1px solid #dee2e6;
950
+ }
951
+
952
+ .action-btn.secondary:hover {
953
+ background: #e9ecef;
954
+ }
955
+
956
+ @media (max-width: 768px) {
957
+ .directory-content {
958
+ flex-direction: column;
959
+ }
960
+
961
+ .user-details {
962
+ width: 100%;
963
+ }
964
+
965
+ .users-grid {
966
+ grid-template-columns: 1fr;
967
+ }
968
+
969
+ .search-options {
970
+ flex-direction: column;
971
+ gap: 8px;
972
+ }
973
+ }
974
+ </style>
975
+ ```
976
+
977
+ ### 多搜索框高级过滤
978
+
979
+ ```svelte
980
+ <script lang="ts">
981
+ import SearchBox from '@ticatec/uniface-element/SearchBox';
982
+
983
+ // 多个搜索条件
984
+ let titleSearch: string | null = null;
985
+ let authorSearch: string | null = null;
986
+ let tagSearch: string | null = null;
987
+
988
+ // 模拟文章数据
989
+ let allArticles = [
990
+ {
991
+ id: 1,
992
+ title: 'Svelte 入门指南',
993
+ author: '张开发',
994
+ category: '教程',
995
+ tags: ['svelte', 'javascript', '前端'],
996
+ publishDate: '2024-03-01',
997
+ readTime: 8,
998
+ excerpt: '学习 Svelte 框架的基础知识并构建您的第一个应用程序。'
999
+ },
1000
+ {
1001
+ id: 2,
1002
+ title: '高级 TypeScript 模式',
1003
+ author: '李程序员',
1004
+ category: '高级',
1005
+ tags: ['typescript', '模式', '高级'],
1006
+ publishDate: '2024-02-28',
1007
+ readTime: 12,
1008
+ excerpt: '探索高级 TypeScript 模式以更好地组织代码。'
1009
+ },
1010
+ {
1011
+ id: 3,
1012
+ title: 'CSS 网格布局指南',
1013
+ author: '王设计师',
1014
+ category: 'CSS',
1015
+ tags: ['css', '网格', '布局', '响应式'],
1016
+ publishDate: '2024-03-05',
1017
+ readTime: 6,
1018
+ excerpt: '掌握 CSS 网格布局以实现现代响应式布局。'
1019
+ },
1020
+ {
1021
+ id: 4,
1022
+ title: '数据库优化技巧',
1023
+ author: '赵管理员',
1024
+ category: '后端',
1025
+ tags: ['数据库', '优化', '性能'],
1026
+ publishDate: '2024-02-25',
1027
+ readTime: 15,
1028
+ excerpt: '使用这些优化技术提高数据库性能。'
1029
+ },
1030
+ {
1031
+ id: 5,
1032
+ title: 'React vs Svelte 对比',
1033
+ author: '陈框架师',
1034
+ category: '对比',
1035
+ tags: ['react', 'svelte', '对比', '前端'],
1036
+ publishDate: '2024-03-03',
1037
+ readTime: 10,
1038
+ excerpt: 'React 和 Svelte 框架之间的详细对比。'
1039
+ }
1040
+ ];
1041
+
1042
+ let filteredArticles = [];
1043
+ let activeFiltersCount = 0;
1044
+
1045
+ function performSearch() {
1046
+ let results = allArticles;
1047
+
1048
+ // 按标题过滤
1049
+ if (titleSearch && titleSearch.trim()) {
1050
+ const titleTerm = titleSearch.toLowerCase().trim();
1051
+ results = results.filter(article =>
1052
+ article.title.toLowerCase().includes(titleTerm)
1053
+ );
1054
+ }
1055
+
1056
+ // 按作者过滤
1057
+ if (authorSearch && authorSearch.trim()) {
1058
+ const authorTerm = authorSearch.toLowerCase().trim();
1059
+ results = results.filter(article =>
1060
+ article.author.toLowerCase().includes(authorTerm)
1061
+ );
1062
+ }
1063
+
1064
+ // 按标签过滤
1065
+ if (tagSearch && tagSearch.trim()) {
1066
+ const tagTerm = tagSearch.toLowerCase().trim();
1067
+ results = results.filter(article =>
1068
+ article.tags.some(tag => tag.toLowerCase().includes(tagTerm)) ||
1069
+ article.category.toLowerCase().includes(tagTerm)
1070
+ );
1071
+ }
1072
+
1073
+ filteredArticles = results;
1074
+
1075
+ // 统计活跃过滤器
1076
+ activeFiltersCount = [titleSearch, authorSearch, tagSearch]
1077
+ .filter(search => search && search.trim()).length;
1078
+ }
1079
+
1080
+ // 初始加载
1081
+ filteredArticles = allArticles;
1082
+
1083
+ // 响应式搜索
1084
+ $: {
1085
+ titleSearch, authorSearch, tagSearch;
1086
+ performSearch();
1087
+ }
1088
+
1089
+ function clearAllFilters() {
1090
+ titleSearch = null;
1091
+ authorSearch = null;
1092
+ tagSearch = null;
1093
+ }
1094
+
1095
+ function readArticle(article) {
1096
+ console.log('阅读文章:', article.title);
1097
+ alert(`打开:${article.title}\n作者 ${article.author} • ${article.readTime} 分钟阅读`);
1098
+ }
1099
+
1100
+ // 获取独特的作者和标签以提供建议
1101
+ const allAuthors = [...new Set(allArticles.map(a => a.author))];
1102
+ const allTags = [...new Set(allArticles.flatMap(a => a.tags))];
1103
+ const allCategories = [...new Set(allArticles.map(a => a.category))];
1104
+ </script>
1105
+
1106
+ <div class="article-search">
1107
+ <header class="search-header">
1108
+ <h2>文章库</h2>
1109
+ <p>搜索和过滤我们的知识库</p>
1110
+ </header>
1111
+
1112
+ <div class="search-filters">
1113
+ <div class="filter-row">
1114
+ <div class="filter-group">
1115
+ <label>按标题搜索</label>
1116
+ <SearchBox
1117
+ bind:value={titleSearch}
1118
+ placeholder="输入文章标题..."
1119
+ variant="outlined"
1120
+ compact={true}
1121
+ />
1122
+ </div>
1123
+
1124
+ <div class="filter-group">
1125
+ <label>按作者搜索</label>
1126
+ <SearchBox
1127
+ bind:value={authorSearch}
1128
+ placeholder="输入作者姓名..."
1129
+ variant="outlined"
1130
+ compact={true}
1131
+ />
1132
+ </div>
1133
+
1134
+ <div class="filter-group">
1135
+ <label>按标签/分类搜索</label>
1136
+ <SearchBox
1137
+ bind:value={tagSearch}
1138
+ placeholder="输入标签或分类..."
1139
+ variant="outlined"
1140
+ compact={true}
1141
+ />
1142
+ </div>
1143
+ </div>
1144
+
1145
+ <div class="filter-actions">
1146
+ <div class="filter-info">
1147
+ {#if activeFiltersCount > 0}
1148
+ <span class="active-filters">{activeFiltersCount} 个过滤器生效</span>
1149
+ <button class="clear-all-btn" on:click={clearAllFilters}>清除所有</button>
1150
+ {:else}
1151
+ <span class="no-filters">未应用过滤器</span>
1152
+ {/if}
1153
+ </div>
1154
+
1155
+ <div class="results-info">
1156
+ <span class="result-count">找到 {filteredArticles.length} 篇文章</span>
1157
+ </div>
1158
+ </div>
1159
+
1160
+ <!-- 快速过滤建议 -->
1161
+ <div class="quick-suggestions">
1162
+ <div class="suggestion-group">
1163
+ <span class="suggestion-label">热门作者:</span>
1164
+ {#each allAuthors.slice(0, 3) as author}
1165
+ <button
1166
+ class="suggestion-btn"
1167
+ on:click={() => authorSearch = author}
1168
+ class:active={authorSearch === author}
1169
+ >
1170
+ {author}
1171
+ </button>
1172
+ {/each}
1173
+ </div>
1174
+
1175
+ <div class="suggestion-group">
1176
+ <span class="suggestion-label">热门标签:</span>
1177
+ {#each allTags.slice(0, 5) as tag}
1178
+ <button
1179
+ class="suggestion-btn"
1180
+ on:click={() => tagSearch = tag}
1181
+ class:active={tagSearch === tag}
1182
+ >
1183
+ #{tag}
1184
+ </button>
1185
+ {/each}
1186
+ </div>
1187
+ </div>
1188
+ </div>
1189
+
1190
+ <div class="articles-section">
1191
+ {#if filteredArticles.length > 0}
1192
+ <div class="articles-grid">
1193
+ {#each filteredArticles as article}
1194
+ <div class="article-card" on:click={() => readArticle(article)}>
1195
+ <div class="article-header">
1196
+ <h3 class="article-title">{article.title}</h3>
1197
+ <span class="article-category">{article.category}</span>
1198
+ </div>
1199
+
1200
+ <div class="article-meta">
1201
+ <span class="article-author">作者 {article.author}</span>
1202
+ <span class="article-date">{new Date(article.publishDate).toLocaleDateString()}</span>
1203
+ <span class="article-read-time">{article.readTime} 分钟阅读</span>
1204
+ </div>
1205
+
1206
+ <p class="article-excerpt">{article.excerpt}</p>
1207
+
1208
+ <div class="article-tags">
1209
+ {#each article.tags as tag}
1210
+ <span class="tag">#{tag}</span>
1211
+ {/each}
1212
+ </div>
1213
+ </div>
1214
+ {/each}
1215
+ </div>
1216
+ {:else}
1217
+ <div class="no-articles">
1218
+ <div class="no-articles-icon">📚</div>
1219
+ <h3>未找到文章</h3>
1220
+ <p>
1221
+ {#if activeFiltersCount > 0}
1222
+ 没有文章匹配您当前的搜索条件。
1223
+ <br>尝试调整您的过滤器或清除一些搜索条件。
1224
+ {:else}
1225
+ 库中没有可用的文章。
1226
+ {/if}
1227
+ </p>
1228
+ {#if activeFiltersCount > 0}
1229
+ <button class="reset-btn" on:click={clearAllFilters}>清除所有过滤器</button>
1230
+ {/if}
1231
+ </div>
1232
+ {/if}
1233
+ </div>
1234
+ </div>
1235
+
1236
+ <style>
1237
+ .article-search {
1238
+ max-width: 1200px;
1239
+ margin: 0 auto;
1240
+ padding: 20px;
1241
+ }
1242
+
1243
+ .search-header {
1244
+ text-align: center;
1245
+ margin-bottom: 30px;
1246
+ }
1247
+
1248
+ .search-header h2 {
1249
+ color: #333;
1250
+ margin: 0 0 8px 0;
1251
+ }
1252
+
1253
+ .search-header p {
1254
+ color: #666;
1255
+ margin: 0;
1256
+ }
1257
+
1258
+ .search-filters {
1259
+ background: #f8f9fa;
1260
+ padding: 25px;
1261
+ border-radius: 8px;
1262
+ margin-bottom: 30px;
1263
+ }
1264
+
1265
+ .filter-row {
1266
+ display: grid;
1267
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
1268
+ gap: 20px;
1269
+ margin-bottom: 20px;
1270
+ }
1271
+
1272
+ .filter-group {
1273
+ display: flex;
1274
+ flex-direction: column;
1275
+ gap: 8px;
1276
+ }
1277
+
1278
+ .filter-group label {
1279
+ font-weight: 500;
1280
+ color: #333;
1281
+ font-size: 0.9rem;
1282
+ }
1283
+
1284
+ .filter-actions {
1285
+ display: flex;
1286
+ justify-content: space-between;
1287
+ align-items: center;
1288
+ margin-bottom: 20px;
1289
+ padding-bottom: 15px;
1290
+ border-bottom: 1px solid #dee2e6;
1291
+ }
1292
+
1293
+ .filter-info {
1294
+ display: flex;
1295
+ align-items: center;
1296
+ gap: 12px;
1297
+ }
1298
+
1299
+ .active-filters {
1300
+ color: #007bff;
1301
+ font-weight: 500;
1302
+ }
1303
+
1304
+ .no-filters {
1305
+ color: #666;
1306
+ }
1307
+
1308
+ .clear-all-btn {
1309
+ padding: 4px 8px;
1310
+ background: #dc3545;
1311
+ color: white;
1312
+ border: none;
1313
+ border-radius: 4px;
1314
+ cursor: pointer;
1315
+ font-size: 0.8rem;
1316
+ }
1317
+
1318
+ .clear-all-btn:hover {
1319
+ background: #c82333;
1320
+ }
1321
+
1322
+ .result-count {
1323
+ color: #495057;
1324
+ font-weight: 500;
1325
+ }
1326
+
1327
+ .quick-suggestions {
1328
+ display: flex;
1329
+ flex-wrap: wrap;
1330
+ gap: 20px;
1331
+ }
1332
+
1333
+ .suggestion-group {
1334
+ display: flex;
1335
+ align-items: center;
1336
+ gap: 8px;
1337
+ flex-wrap: wrap;
1338
+ }
1339
+
1340
+ .suggestion-label {
1341
+ color: #666;
1342
+ font-size: 0.85rem;
1343
+ font-weight: 500;
1344
+ }
1345
+
1346
+ .suggestion-btn {
1347
+ padding: 4px 8px;
1348
+ background: white;
1349
+ border: 1px solid #dee2e6;
1350
+ border-radius: 12px;
1351
+ cursor: pointer;
1352
+ font-size: 0.75rem;
1353
+ transition: all 0.2s;
1354
+ }
1355
+
1356
+ .suggestion-btn:hover {
1357
+ border-color: #007bff;
1358
+ color: #007bff;
1359
+ }
1360
+
1361
+ .suggestion-btn.active {
1362
+ background: #007bff;
1363
+ color: white;
1364
+ border-color: #007bff;
1365
+ }
1366
+
1367
+ .articles-grid {
1368
+ display: grid;
1369
+ grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
1370
+ gap: 24px;
1371
+ }
1372
+
1373
+ .article-card {
1374
+ background: white;
1375
+ border: 1px solid #e9ecef;
1376
+ border-radius: 8px;
1377
+ padding: 20px;
1378
+ cursor: pointer;
1379
+ transition: all 0.2s;
1380
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
1381
+ }
1382
+
1383
+ .article-card:hover {
1384
+ border-color: #007bff;
1385
+ transform: translateY(-2px);
1386
+ box-shadow: 0 4px 8px rgba(0,123,255,0.15);
1387
+ }
1388
+
1389
+ .article-header {
1390
+ display: flex;
1391
+ justify-content: space-between;
1392
+ align-items: flex-start;
1393
+ margin-bottom: 12px;
1394
+ }
1395
+
1396
+ .article-title {
1397
+ margin: 0;
1398
+ color: #333;
1399
+ font-size: 1.2rem;
1400
+ font-weight: 600;
1401
+ line-height: 1.3;
1402
+ }
1403
+
1404
+ .article-category {
1405
+ background: #e9ecef;
1406
+ color: #495057;
1407
+ padding: 4px 8px;
1408
+ border-radius: 12px;
1409
+ font-size: 0.7rem;
1410
+ font-weight: 500;
1411
+ text-transform: uppercase;
1412
+ flex-shrink: 0;
1413
+ margin-left: 12px;
1414
+ }
1415
+
1416
+ .article-meta {
1417
+ display: flex;
1418
+ flex-wrap: wrap;
1419
+ gap: 12px;
1420
+ margin-bottom: 12px;
1421
+ font-size: 0.85rem;
1422
+ color: #666;
1423
+ }
1424
+
1425
+ .article-author {
1426
+ font-weight: 500;
1427
+ }
1428
+
1429
+ .article-excerpt {
1430
+ color: #555;
1431
+ line-height: 1.5;
1432
+ margin: 0 0 16px 0;
1433
+ }
1434
+
1435
+ .article-tags {
1436
+ display: flex;
1437
+ flex-wrap: wrap;
1438
+ gap: 6px;
1439
+ }
1440
+
1441
+ .tag {
1442
+ background: #f8f9fa;
1443
+ color: #495057;
1444
+ padding: 3px 8px;
1445
+ border-radius: 10px;
1446
+ font-size: 0.7rem;
1447
+ border: 1px solid #e9ecef;
1448
+ }
1449
+
1450
+ .no-articles {
1451
+ text-align: center;
1452
+ padding: 80px 20px;
1453
+ color: #666;
1454
+ }
1455
+
1456
+ .no-articles-icon {
1457
+ font-size: 4rem;
1458
+ opacity: 0.5;
1459
+ margin-bottom: 16px;
1460
+ }
1461
+
1462
+ .no-articles h3 {
1463
+ color: #333;
1464
+ margin: 0 0 12px 0;
1465
+ }
1466
+
1467
+ .no-articles p {
1468
+ margin: 0 0 20px 0;
1469
+ line-height: 1.6;
1470
+ }
1471
+
1472
+ .reset-btn {
1473
+ padding: 10px 20px;
1474
+ background: #007bff;
1475
+ color: white;
1476
+ border: none;
1477
+ border-radius: 4px;
1478
+ cursor: pointer;
1479
+ }
1480
+
1481
+ .reset-btn:hover {
1482
+ background: #0056b3;
1483
+ }
1484
+
1485
+ @media (max-width: 768px) {
1486
+ .filter-row {
1487
+ grid-template-columns: 1fr;
1488
+ }
1489
+
1490
+ .filter-actions {
1491
+ flex-direction: column;
1492
+ gap: 12px;
1493
+ align-items: stretch;
1494
+ }
1495
+
1496
+ .quick-suggestions {
1497
+ flex-direction: column;
1498
+ }
1499
+
1500
+ .articles-grid {
1501
+ grid-template-columns: 1fr;
1502
+ }
1503
+
1504
+ .article-header {
1505
+ flex-direction: column;
1506
+ gap: 8px;
1507
+ }
1508
+
1509
+ .article-category {
1510
+ align-self: flex-start;
1511
+ margin-left: 0;
1512
+ }
1513
+ }
1514
+ </style>
1515
+ ```
1516
+
1517
+ ## 样式
1518
+
1519
+ SearchBox 组件使用以下 CSS 类:
1520
+
1521
+ ```css
1522
+ .uniface-common-editor {
1523
+ /* 主输入容器 */
1524
+ position: relative;
1525
+ }
1526
+
1527
+ .uniface-common-editor .leading-icon {
1528
+ /* 搜索图标容器 */
1529
+ display: flex;
1530
+ align-items: center;
1531
+ padding: 0 8px;
1532
+ color: var(--uniface-editor-icon-color, #6c757d);
1533
+ }
1534
+
1535
+ .uniface-common-editor .action-icon {
1536
+ /* 清除按钮样式 */
1537
+ cursor: pointer;
1538
+ opacity: 0.6;
1539
+ transition: opacity 0.2s;
1540
+ }
1541
+
1542
+ .uniface-common-editor .action-icon:hover {
1543
+ opacity: 1;
1544
+ }
1545
+ ```
1546
+
1547
+ ## API 参考
1548
+
1549
+ ### SearchBox 组件属性
1550
+
1551
+ ```typescript
1552
+ interface SearchBoxProps {
1553
+ variant?: '' | 'plain' | 'outlined' | 'filled'; // 视觉样式变体
1554
+ compact?: boolean; // 紧凑布局模式
1555
+ style?: string; // 自定义内联样式
1556
+ value: string | null; // 搜索文本值
1557
+ disabled?: boolean; // 禁用输入
1558
+ onChange?: OnChangeHandler<string | null>; // 变更事件处理器
1559
+ placeholder?: string | null; // 占位符文本
1560
+ }
1561
+ ```
1562
+
1563
+ ### 方法
1564
+
1565
+ ```typescript
1566
+ interface SearchBox {
1567
+ focus(): void; // 以编程方式聚焦输入框
1568
+ }
1569
+ ```
1570
+
1571
+ ## 最佳实践
1572
+
1573
+ 1. **防抖处理** - 实现防抖搜索以避免过多的 API 调用
1574
+ 2. **加载状态** - 在搜索操作期间显示加载指示器
1575
+ 3. **空状态** - 在没有找到结果时提供有用的消息
1576
+ 4. **键盘导航** - 支持搜索焦点的键盘快捷键
1577
+ 5. **搜索范围** - 清楚地表明正在搜索哪些字段
1578
+ 6. **清除功能** - 始终提供清除搜索的简单方法
1579
+ 7. **移动端用户体验** - 确保在移动设备上的触摸友好交互
1580
+ 8. **性能** - 为大数据集优化搜索算法
1581
+ 9. **可访问性** - 提供适当的 ARIA 标签和屏幕阅读器支持
1582
+ 10. **视觉反馈** - 尽可能在搜索结果中突出显示匹配条件