@quantabit/search-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1581 @@
1
+ 'use strict';
2
+
3
+ var sdkConfig = require('@quantabit/sdk-config');
4
+ var React = require('react');
5
+
6
+ /**
7
+ * Search SDK - API 客户端
8
+ * 搜索系统后端接口封装
9
+ *
10
+ * 使用 BaseApiClient 基类简化代码
11
+ */
12
+
13
+
14
+ /**
15
+ * 搜索 API 客户端
16
+ */
17
+ class SearchApiClient extends sdkConfig.BaseApiClient {
18
+ constructor(config = {}) {
19
+ super('/search', config);
20
+ }
21
+
22
+ // ============ 搜索 ============
23
+
24
+ /**
25
+ * 全局搜索
26
+ * @param {string} query - 搜索关键词
27
+ * @param {Object} options - 搜索选项
28
+ */
29
+ async search(query, options = {}) {
30
+ return this.get('/', {
31
+ q: query,
32
+ ...options
33
+ });
34
+ }
35
+
36
+ /**
37
+ * 搜索用户
38
+ * @param {string} query - 搜索关键词
39
+ * @param {Object} options - 搜索选项
40
+ */
41
+ async searchUsers(query, options = {}) {
42
+ return this.get('/users', {
43
+ q: query,
44
+ ...options
45
+ });
46
+ }
47
+
48
+ /**
49
+ * 搜索内容
50
+ * @param {string} query - 搜索关键词
51
+ * @param {Object} options - 搜索选项
52
+ */
53
+ async searchContent(query, options = {}) {
54
+ return this.get('/content', {
55
+ q: query,
56
+ ...options
57
+ });
58
+ }
59
+
60
+ /**
61
+ * 搜索商品
62
+ * @param {string} query - 搜索关键词
63
+ * @param {Object} options - 搜索选项
64
+ */
65
+ async searchProducts(query, options = {}) {
66
+ return this.get('/products', {
67
+ q: query,
68
+ ...options
69
+ });
70
+ }
71
+
72
+ /**
73
+ * 搜索话题
74
+ * @param {string} query - 搜索关键词
75
+ * @param {Object} options - 搜索选项
76
+ */
77
+ async searchTopics(query, options = {}) {
78
+ return this.get('/topics', {
79
+ q: query,
80
+ ...options
81
+ });
82
+ }
83
+
84
+ // ============ 搜索建议 ============
85
+
86
+ /**
87
+ * 获取搜索建议
88
+ * @param {string} query - 输入内容
89
+ * @param {number} limit - 返回数量
90
+ */
91
+ async getSuggestions(query, limit = 10) {
92
+ return this.get('/suggestions', {
93
+ q: query,
94
+ limit
95
+ });
96
+ }
97
+
98
+ /**
99
+ * 获取热门搜索
100
+ * @param {number} limit - 返回数量
101
+ */
102
+ async getHotSearches(limit = 10) {
103
+ return this.get('/hot', {
104
+ limit
105
+ });
106
+ }
107
+
108
+ /**
109
+ * 获取搜索发现
110
+ */
111
+ async getDiscovery() {
112
+ return this.get('/discovery');
113
+ }
114
+
115
+ // ============ 搜索历史 ============
116
+
117
+ /**
118
+ * 获取搜索历史
119
+ * @param {number} limit - 返回数量
120
+ */
121
+ async getHistory(limit = 20) {
122
+ return this.get('/history', {
123
+ limit
124
+ });
125
+ }
126
+
127
+ /**
128
+ * 清除搜索历史
129
+ */
130
+ async clearHistory() {
131
+ return this.delete('/history');
132
+ }
133
+
134
+ /**
135
+ * 删除单条历史
136
+ * @param {string} historyId - 历史记录 ID
137
+ */
138
+ async deleteHistoryItem(historyId) {
139
+ return this.delete(`/history/${historyId}`);
140
+ }
141
+
142
+ // ============ 高级搜索 ============
143
+
144
+ /**
145
+ * 高级搜索
146
+ * @param {Object} filters - 过滤条件
147
+ */
148
+ async advancedSearch(filters) {
149
+ return this.post('/advanced', filters);
150
+ }
151
+
152
+ /**
153
+ * 筛选搜索
154
+ * @param {string} query - 搜索关键词
155
+ * @param {Object} filters - 过滤条件
156
+ */
157
+ async filterSearch(query, filters = {}) {
158
+ return this.post('/filter', {
159
+ q: query,
160
+ filters
161
+ });
162
+ }
163
+
164
+ // ============ 索引管理(管理员)============
165
+
166
+ /**
167
+ * 获取索引状态
168
+ */
169
+ async getIndexStatus() {
170
+ return this.get('/admin/index/status');
171
+ }
172
+
173
+ /**
174
+ * 重建索引
175
+ * @param {string} indexType - 索引类型
176
+ */
177
+ async rebuildIndex(indexType) {
178
+ return this.post(`/admin/index/${indexType}/rebuild`);
179
+ }
180
+
181
+ /**
182
+ * 获取搜索统计
183
+ * @param {Object} params - 统计参数
184
+ */
185
+ async getStats(params = {}) {
186
+ return this.get('/admin/stats', params);
187
+ }
188
+
189
+ // ============ 隐私合规 ============
190
+
191
+ /**
192
+ * 清除所有搜索数据 — GDPR 第17条
193
+ * 删除搜索历史 + 个性化搜索偏好
194
+ * @returns {Promise<object>} 清除结果
195
+ */
196
+ async clearAllUserData() {
197
+ const results = {};
198
+ try {
199
+ results.history = await this.clearHistory();
200
+ } catch (e) {
201
+ results.historyError = e.message;
202
+ }
203
+ try {
204
+ results.preferences = await this.delete('/preferences');
205
+ } catch (e) {
206
+ results.preferencesError = e.message;
207
+ }
208
+ return {
209
+ cleared: true,
210
+ timestamp: new Date().toISOString(),
211
+ ...results
212
+ };
213
+ }
214
+
215
+ /**
216
+ * 导出搜索数据 — GDPR 第20条
217
+ */
218
+ async exportSearchData() {
219
+ try {
220
+ const history = await this.getHistory(100);
221
+ return {
222
+ exportDate: new Date().toISOString(),
223
+ format: 'QBit Search Export (GDPR Art. 20)',
224
+ searchHistory: history?.data || []
225
+ };
226
+ } catch (e) {
227
+ return {
228
+ error: e.message,
229
+ exportDate: new Date().toISOString()
230
+ };
231
+ }
232
+ }
233
+
234
+ /**
235
+ * 获取隐私数据声明
236
+ */
237
+ getDataDisclosure() {
238
+ return {
239
+ sdk: '@quantabit/search-sdk',
240
+ privacyLevel: 'functional',
241
+ consentRequired: false,
242
+ collected: [{
243
+ type: 'search_queries',
244
+ description: 'User search keywords',
245
+ retention: '90 days',
246
+ anonymizedAfter: '30 days'
247
+ }, {
248
+ type: 'search_history',
249
+ description: 'Search history records',
250
+ retention: 'Until cleared by user'
251
+ }, {
252
+ type: 'search_preferences',
253
+ description: 'Personalized search settings',
254
+ retention: 'Until cleared'
255
+ }],
256
+ gdprCapabilities: ['delete', 'export'],
257
+ note: 'Search queries are anonymized after 30 days. Users can clear history anytime.'
258
+ };
259
+ }
260
+ }
261
+
262
+ // 创建默认实例
263
+ const searchApi = new SearchApiClient();
264
+
265
+ function SearchInput({
266
+ placeholder = 'Search...',
267
+ suggestions = [],
268
+ onSearch,
269
+ onSelect,
270
+ debounce = 300,
271
+ shortcut = 'mod+k',
272
+ showIcon = true,
273
+ size = 'medium',
274
+ className = ''
275
+ }) {
276
+ const [query, setQuery] = React.useState('');
277
+ const [open, setOpen] = React.useState(false);
278
+ const [active, setActive] = React.useState(-1);
279
+ const inputRef = React.useRef(null);
280
+ const timerRef = React.useRef(null);
281
+ const sz = {
282
+ small: {
283
+ pad: '6px 10px',
284
+ font: 13
285
+ },
286
+ medium: {
287
+ pad: '8px 14px',
288
+ font: 14
289
+ },
290
+ large: {
291
+ pad: '10px 16px',
292
+ font: 15
293
+ }
294
+ };
295
+ const s = sz[size] || sz.medium;
296
+ const filtered = query ? suggestions.filter(s => {
297
+ const label = typeof s === 'string' ? s : s.label;
298
+ return label.toLowerCase().includes(query.toLowerCase());
299
+ }) : [];
300
+ const handleChange = e => {
301
+ const v = e.target.value;
302
+ setQuery(v);
303
+ setActive(-1);
304
+ clearTimeout(timerRef.current);
305
+ timerRef.current = setTimeout(() => onSearch?.(v), debounce);
306
+ setOpen(!!v);
307
+ };
308
+ const handleSelect = item => {
309
+ const label = typeof item === 'string' ? item : item.label;
310
+ setQuery(label);
311
+ setOpen(false);
312
+ onSelect?.(item);
313
+ };
314
+ const handleKey = e => {
315
+ if (e.key === 'ArrowDown') {
316
+ e.preventDefault();
317
+ setActive(a => Math.min(a + 1, filtered.length - 1));
318
+ } else if (e.key === 'ArrowUp') {
319
+ e.preventDefault();
320
+ setActive(a => Math.max(a - 1, -1));
321
+ } else if (e.key === 'Enter' && active >= 0) {
322
+ handleSelect(filtered[active]);
323
+ } else if (e.key === 'Escape') {
324
+ setOpen(false);
325
+ }
326
+ };
327
+ return /*#__PURE__*/React.createElement("div", {
328
+ className: `qsr-search ${className}`,
329
+ style: {
330
+ position: 'relative',
331
+ width: '100%'
332
+ }
333
+ }, /*#__PURE__*/React.createElement("div", {
334
+ style: {
335
+ display: 'flex',
336
+ alignItems: 'center',
337
+ gap: 8,
338
+ padding: s.pad,
339
+ border: '1px solid #e4e4e7',
340
+ borderRadius: 10,
341
+ background: '#fff',
342
+ transition: 'all 0.2s',
343
+ fontSize: s.font
344
+ },
345
+ className: "qsr-input-wrap"
346
+ }, showIcon && /*#__PURE__*/React.createElement("svg", {
347
+ width: "16",
348
+ height: "16",
349
+ viewBox: "0 0 24 24",
350
+ fill: "none",
351
+ stroke: "#a1a1aa",
352
+ strokeWidth: "2"
353
+ }, /*#__PURE__*/React.createElement("circle", {
354
+ cx: "11",
355
+ cy: "11",
356
+ r: "8"
357
+ }), /*#__PURE__*/React.createElement("path", {
358
+ d: "m21 21-4.3-4.3"
359
+ })), /*#__PURE__*/React.createElement("input", {
360
+ ref: inputRef,
361
+ value: query,
362
+ onChange: handleChange,
363
+ onKeyDown: handleKey,
364
+ onFocus: () => query && setOpen(true),
365
+ placeholder: placeholder,
366
+ style: {
367
+ border: 'none',
368
+ outline: 'none',
369
+ flex: 1,
370
+ fontSize: s.font,
371
+ background: 'transparent'
372
+ }
373
+ }), query && /*#__PURE__*/React.createElement("button", {
374
+ onClick: () => {
375
+ setQuery('');
376
+ setOpen(false);
377
+ onSearch?.('');
378
+ },
379
+ style: {
380
+ border: 'none',
381
+ background: 'none',
382
+ cursor: 'pointer',
383
+ color: '#a1a1aa',
384
+ fontSize: 16,
385
+ padding: 0
386
+ }
387
+ }, "\u2715")), open && filtered.length > 0 && /*#__PURE__*/React.createElement("div", {
388
+ className: "qsr-dropdown",
389
+ style: {
390
+ position: 'absolute',
391
+ top: '100%',
392
+ left: 0,
393
+ right: 0,
394
+ marginTop: 4,
395
+ background: '#fff',
396
+ borderRadius: 10,
397
+ border: '1px solid #e4e4e7',
398
+ boxShadow: '0 8px 32px rgba(0,0,0,0.1)',
399
+ maxHeight: 280,
400
+ overflowY: 'auto',
401
+ padding: 4,
402
+ zIndex: 9999
403
+ }
404
+ }, filtered.map((item, i) => {
405
+ const label = typeof item === 'string' ? item : item.label;
406
+ const desc = item.description;
407
+ return /*#__PURE__*/React.createElement("button", {
408
+ key: i,
409
+ onClick: () => handleSelect(item),
410
+ className: `qsr-option ${i === active ? 'qsr-active' : ''}`,
411
+ style: {
412
+ display: 'block',
413
+ width: '100%',
414
+ padding: '8px 12px',
415
+ borderRadius: 6,
416
+ border: 'none',
417
+ background: i === active ? 'rgba(59,130,246,0.06)' : 'transparent',
418
+ cursor: 'pointer',
419
+ textAlign: 'left',
420
+ fontSize: 13,
421
+ color: '#18181b',
422
+ transition: 'all 0.1s'
423
+ }
424
+ }, /*#__PURE__*/React.createElement("div", {
425
+ style: {
426
+ fontWeight: i === active ? 600 : 400
427
+ }
428
+ }, label), desc && /*#__PURE__*/React.createElement("div", {
429
+ style: {
430
+ fontSize: 11,
431
+ color: '#a1a1aa',
432
+ marginTop: 2
433
+ }
434
+ }, desc));
435
+ })));
436
+ }
437
+
438
+ function CommandPalette({
439
+ visible,
440
+ onClose,
441
+ commands = [],
442
+ placeholder = 'Type a command...',
443
+ className = ''
444
+ }) {
445
+ const [query, setQuery] = React.useState('');
446
+ const [active, setActive] = React.useState(0);
447
+ const inputRef = React.useRef(null);
448
+ const filtered = commands.filter(c => c.label.toLowerCase().includes(query.toLowerCase()));
449
+ React.useEffect(() => {
450
+ if (visible) {
451
+ setQuery('');
452
+ setActive(0);
453
+ setTimeout(() => inputRef.current?.focus(), 50);
454
+ }
455
+ }, [visible]);
456
+ React.useEffect(() => {
457
+ if (!visible) return;
458
+ const h = e => {
459
+ if (e.key === 'Escape') onClose?.();
460
+ };
461
+ document.addEventListener('keydown', h);
462
+ return () => document.removeEventListener('keydown', h);
463
+ }, [visible, onClose]);
464
+ const handleKey = e => {
465
+ if (e.key === 'ArrowDown') {
466
+ e.preventDefault();
467
+ setActive(a => (a + 1) % filtered.length);
468
+ } else if (e.key === 'ArrowUp') {
469
+ e.preventDefault();
470
+ setActive(a => (a - 1 + filtered.length) % filtered.length);
471
+ } else if (e.key === 'Enter' && filtered[active]) {
472
+ filtered[active].action?.();
473
+ onClose?.();
474
+ }
475
+ };
476
+ if (!visible) return null;
477
+ return /*#__PURE__*/React.createElement("div", {
478
+ onClick: onClose,
479
+ style: {
480
+ position: 'fixed',
481
+ inset: 0,
482
+ zIndex: 99999,
483
+ background: 'rgba(0,0,0,0.5)',
484
+ backdropFilter: 'blur(4px)',
485
+ display: 'flex',
486
+ justifyContent: 'center',
487
+ paddingTop: '20vh'
488
+ }
489
+ }, /*#__PURE__*/React.createElement("div", {
490
+ onClick: e => e.stopPropagation(),
491
+ className: `qsr-palette ${className}`,
492
+ style: {
493
+ width: 520,
494
+ maxHeight: 400,
495
+ background: '#fff',
496
+ borderRadius: 16,
497
+ boxShadow: '0 24px 80px rgba(0,0,0,0.2)',
498
+ overflow: 'hidden',
499
+ animation: 'qsr-scale-in 0.15s ease-out'
500
+ }
501
+ }, /*#__PURE__*/React.createElement("div", {
502
+ style: {
503
+ display: 'flex',
504
+ alignItems: 'center',
505
+ gap: 8,
506
+ padding: '12px 16px',
507
+ borderBottom: '1px solid #f4f4f5'
508
+ }
509
+ }, /*#__PURE__*/React.createElement("svg", {
510
+ width: "18",
511
+ height: "18",
512
+ viewBox: "0 0 24 24",
513
+ fill: "none",
514
+ stroke: "#a1a1aa",
515
+ strokeWidth: "2"
516
+ }, /*#__PURE__*/React.createElement("circle", {
517
+ cx: "11",
518
+ cy: "11",
519
+ r: "8"
520
+ }), /*#__PURE__*/React.createElement("path", {
521
+ d: "m21 21-4.3-4.3"
522
+ })), /*#__PURE__*/React.createElement("input", {
523
+ ref: inputRef,
524
+ value: query,
525
+ onChange: e => {
526
+ setQuery(e.target.value);
527
+ setActive(0);
528
+ },
529
+ onKeyDown: handleKey,
530
+ placeholder: placeholder,
531
+ style: {
532
+ border: 'none',
533
+ outline: 'none',
534
+ flex: 1,
535
+ fontSize: 15,
536
+ background: 'transparent'
537
+ }
538
+ })), /*#__PURE__*/React.createElement("div", {
539
+ style: {
540
+ maxHeight: 300,
541
+ overflowY: 'auto',
542
+ padding: 4
543
+ }
544
+ }, filtered.map((cmd, i) => /*#__PURE__*/React.createElement("button", {
545
+ key: i,
546
+ onClick: () => {
547
+ cmd.action?.();
548
+ onClose?.();
549
+ },
550
+ style: {
551
+ display: 'flex',
552
+ alignItems: 'center',
553
+ gap: 10,
554
+ width: '100%',
555
+ padding: '10px 12px',
556
+ borderRadius: 8,
557
+ border: 'none',
558
+ background: i === active ? 'rgba(59,130,246,0.06)' : 'transparent',
559
+ cursor: 'pointer',
560
+ textAlign: 'left',
561
+ fontSize: 14,
562
+ color: '#18181b',
563
+ transition: 'all 0.1s'
564
+ }
565
+ }, cmd.icon && /*#__PURE__*/React.createElement("span", {
566
+ style: {
567
+ width: 20,
568
+ height: 20,
569
+ display: 'flex',
570
+ flexShrink: 0,
571
+ color: '#71717a'
572
+ }
573
+ }, cmd.icon), /*#__PURE__*/React.createElement("span", {
574
+ style: {
575
+ flex: 1,
576
+ fontWeight: i === active ? 600 : 400
577
+ }
578
+ }, cmd.label), cmd.shortcut && /*#__PURE__*/React.createElement("kbd", {
579
+ style: {
580
+ fontSize: 11,
581
+ padding: '2px 6px',
582
+ borderRadius: 4,
583
+ background: 'rgba(128,128,128,0.08)',
584
+ color: '#a1a1aa',
585
+ border: '1px solid rgba(128,128,128,0.1)'
586
+ }
587
+ }, cmd.shortcut))))));
588
+ }
589
+
590
+ /**
591
+ * Search SDK - React Hooks
592
+ * 搜索系统相关的状态管理
593
+ */
594
+
595
+
596
+ /**
597
+ * 搜索功能
598
+ */
599
+ function useSearch(options = {}) {
600
+ const [query, setQuery] = React.useState('');
601
+ const [results, setResults] = React.useState([]);
602
+ const [loading, setLoading] = React.useState(false);
603
+ const [error, setError] = React.useState(null);
604
+ const [pagination, setPagination] = React.useState({
605
+ page: 1,
606
+ total: 0,
607
+ hasMore: false
608
+ });
609
+
610
+ // Handle options with fallback to old opts naming/keys
611
+ const type = options.type || 'all';
612
+ const sort = options.sort || 'relevance';
613
+ const limit = options.limit || 20;
614
+ const debounce = options.debounce !== undefined ? options.debounce : options.debounceMs !== undefined ? options.debounceMs : 300;
615
+ const debounceRef = React.useRef(null);
616
+ const search = React.useCallback(async (searchQuery, page = 1) => {
617
+ const q = searchQuery !== undefined ? searchQuery : query;
618
+ if (!q || !q.trim()) {
619
+ setResults([]);
620
+ setLoading(false);
621
+ return;
622
+ }
623
+ try {
624
+ setLoading(true);
625
+ setError(null);
626
+ const response = await searchApi.search(q, {
627
+ type,
628
+ sort,
629
+ page,
630
+ limit
631
+ });
632
+ const items = response?.data || response?.items || response || [];
633
+ if (page === 1) {
634
+ setResults(items);
635
+ } else {
636
+ setResults(prev => [...prev, ...items]);
637
+ }
638
+ setPagination({
639
+ page,
640
+ total: response?.total || items.length,
641
+ hasMore: !!response?.hasMore
642
+ });
643
+ } catch (err) {
644
+ setError(err?.message || String(err));
645
+ } finally {
646
+ setLoading(false);
647
+ }
648
+ }, [type, sort, limit, query]);
649
+
650
+ // 带防抖的搜索
651
+ const debouncedSearch = React.useCallback(value => {
652
+ setQuery(value);
653
+ if (debounceRef.current) {
654
+ clearTimeout(debounceRef.current);
655
+ }
656
+ debounceRef.current = setTimeout(() => {
657
+ search(value, 1);
658
+ }, debounce);
659
+ }, [search, debounce]);
660
+
661
+ // 清理
662
+ React.useEffect(() => {
663
+ return () => {
664
+ if (debounceRef.current) {
665
+ clearTimeout(debounceRef.current);
666
+ }
667
+ };
668
+ }, []);
669
+ const loadMore = React.useCallback(() => {
670
+ if (!loading && pagination.hasMore) {
671
+ search(query, pagination.page + 1);
672
+ }
673
+ }, [loading, pagination, search, query]);
674
+ const clear = React.useCallback(() => {
675
+ setQuery('');
676
+ setResults([]);
677
+ setPagination({
678
+ page: 1,
679
+ total: 0,
680
+ hasMore: false
681
+ });
682
+ setLoading(false);
683
+ }, []);
684
+ return {
685
+ query,
686
+ setQuery: debouncedSearch,
687
+ results,
688
+ loading,
689
+ error,
690
+ pagination,
691
+ search: q => search(q !== undefined ? q : query, 1),
692
+ loadMore,
693
+ clear
694
+ };
695
+ }
696
+
697
+ /**
698
+ * 搜索建议
699
+ */
700
+ function useSuggestions(query, limit = 10) {
701
+ const [suggestions, setSuggestions] = React.useState([]);
702
+ const [loading, setLoading] = React.useState(false);
703
+ const debounceRef = React.useRef(null);
704
+ React.useEffect(() => {
705
+ if (!query || query.length < 2) {
706
+ setSuggestions([]);
707
+ return;
708
+ }
709
+ if (debounceRef.current) {
710
+ clearTimeout(debounceRef.current);
711
+ }
712
+ debounceRef.current = setTimeout(async () => {
713
+ try {
714
+ setLoading(true);
715
+ const response = await searchApi.getSuggestions(query, limit);
716
+ setSuggestions(response?.data || response?.suggestions || response || []);
717
+ } catch (err) {
718
+ console.error('Get suggestions error:', err);
719
+ } finally {
720
+ setLoading(false);
721
+ }
722
+ }, 200);
723
+ return () => {
724
+ if (debounceRef.current) {
725
+ clearTimeout(debounceRef.current);
726
+ }
727
+ };
728
+ }, [query, limit]);
729
+ return {
730
+ suggestions,
731
+ loading
732
+ };
733
+ }
734
+
735
+ /**
736
+ * 带输入状态的搜索建议
737
+ */
738
+ function useSearchSuggestions(limit = 10) {
739
+ const [query, setQuery] = React.useState('');
740
+ const {
741
+ suggestions,
742
+ loading
743
+ } = useSuggestions(query, limit);
744
+ return {
745
+ query,
746
+ setQuery,
747
+ suggestions,
748
+ loading
749
+ };
750
+ }
751
+
752
+ /**
753
+ * 搜索历史
754
+ */
755
+ function useSearchHistory(storageKey = 'eco-search-history') {
756
+ const [history, setHistory] = React.useState([]);
757
+ const maxItems = 10;
758
+
759
+ // 初始化
760
+ React.useEffect(() => {
761
+ if (typeof localStorage === 'undefined') return;
762
+ const saved = localStorage.getItem(storageKey);
763
+ if (saved) {
764
+ try {
765
+ const parsed = JSON.parse(saved);
766
+ // Ensure items are normalized as objects { query }
767
+ const normalized = parsed.map(item => typeof item === 'string' ? {
768
+ query: item
769
+ } : item);
770
+ setHistory(normalized);
771
+ } catch {
772
+ setHistory([]);
773
+ }
774
+ }
775
+ }, [storageKey]);
776
+ const addToHistory = React.useCallback(item => {
777
+ const queryStr = typeof item === 'string' ? item : item?.query || '';
778
+ if (!queryStr || !queryStr.trim()) return;
779
+ setHistory(prev => {
780
+ const filtered = prev.filter(entry => (typeof entry === 'string' ? entry : entry.query) !== queryStr);
781
+ const newHistory = [{
782
+ query: queryStr
783
+ }, ...filtered].slice(0, maxItems);
784
+ if (typeof localStorage !== 'undefined') {
785
+ localStorage.setItem(storageKey, JSON.stringify(newHistory));
786
+ }
787
+ return newHistory;
788
+ });
789
+ }, [storageKey]);
790
+ const removeFromHistory = React.useCallback(item => {
791
+ const queryStr = typeof item === 'string' ? item : item?.query || '';
792
+ setHistory(prev => {
793
+ const filtered = prev.filter(entry => (typeof entry === 'string' ? entry : entry.query) !== queryStr);
794
+ if (typeof localStorage !== 'undefined') {
795
+ localStorage.setItem(storageKey, JSON.stringify(filtered));
796
+ }
797
+ return filtered;
798
+ });
799
+ }, [storageKey]);
800
+ const clearHistory = React.useCallback(() => {
801
+ setHistory([]);
802
+ if (typeof localStorage !== 'undefined') {
803
+ localStorage.removeItem(storageKey);
804
+ }
805
+ }, [storageKey]);
806
+ return {
807
+ history,
808
+ addToHistory,
809
+ removeFromHistory,
810
+ clearHistory
811
+ };
812
+ }
813
+
814
+ /**
815
+ * 热门搜索
816
+ */
817
+ function useTrendingSearches(limit = 10) {
818
+ const [trending, setTrending] = React.useState([]);
819
+ const [loading, setLoading] = React.useState(true);
820
+ const fetchTrending = React.useCallback(async () => {
821
+ try {
822
+ setLoading(true);
823
+ const response = await searchApi.getHotSearches(limit);
824
+ setTrending(response?.data || response?.keywords || response || []);
825
+ } catch (err) {
826
+ console.error('Get trending error:', err);
827
+ } finally {
828
+ setLoading(false);
829
+ }
830
+ }, [limit]);
831
+ React.useEffect(() => {
832
+ fetchTrending();
833
+ }, [fetchTrending]);
834
+ return {
835
+ trending,
836
+ loading,
837
+ refresh: fetchTrending
838
+ };
839
+ }
840
+
841
+ /**
842
+ * 筛选器
843
+ */
844
+ function useSearchFilters(initialFilters = {}) {
845
+ const [filters, setFilters] = React.useState(initialFilters);
846
+ const updateFilter = React.useCallback((key, value) => {
847
+ setFilters(prev => ({
848
+ ...prev,
849
+ [key]: value
850
+ }));
851
+ }, []);
852
+ const setFilter = updateFilter;
853
+ const clearFilters = React.useCallback(() => {
854
+ setFilters({});
855
+ }, []);
856
+ const hasFilters = React.useMemo(() => {
857
+ return Object.values(filters).some(v => v !== null && v !== undefined && v !== '');
858
+ }, [filters]);
859
+ const activeCount = React.useMemo(() => {
860
+ return Object.values(filters).filter(v => v !== null && v !== undefined && v !== '').length;
861
+ }, [filters]);
862
+ return {
863
+ filters,
864
+ updateFilter,
865
+ setFilter,
866
+ clearFilters,
867
+ hasFilters,
868
+ activeCount
869
+ };
870
+ }
871
+
872
+ /**
873
+ * 语音搜索
874
+ */
875
+ function useVoiceSearch(onResult) {
876
+ const [isListening, setIsListening] = React.useState(false);
877
+ const [isSupported, setIsSupported] = React.useState(false);
878
+ const recognitionRef = React.useRef(null);
879
+ React.useEffect(() => {
880
+ if (typeof window === 'undefined') return;
881
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
882
+ if (SpeechRecognition) {
883
+ setIsSupported(true);
884
+ const rec = new SpeechRecognition();
885
+ rec.continuous = false;
886
+ rec.interimResults = false;
887
+ rec.onresult = event => {
888
+ const transcript = event.results?.[0]?.[0]?.transcript || '';
889
+ onResult?.(transcript);
890
+ };
891
+ rec.onend = () => {
892
+ setIsListening(false);
893
+ };
894
+ recognitionRef.current = rec;
895
+ }
896
+ }, [onResult]);
897
+ const startListening = React.useCallback(() => {
898
+ if (recognitionRef.current && !isListening) {
899
+ try {
900
+ recognitionRef.current.start();
901
+ setIsListening(true);
902
+ } catch (err) {
903
+ console.error('Speech recognition start error:', err);
904
+ }
905
+ }
906
+ }, [isListening]);
907
+ const stopListening = React.useCallback(() => {
908
+ if (recognitionRef.current && isListening) {
909
+ try {
910
+ recognitionRef.current.stop();
911
+ } catch (err) {
912
+ console.error('Speech recognition stop error:', err);
913
+ }
914
+ setIsListening(false);
915
+ }
916
+ }, [isListening]);
917
+ return {
918
+ isListening,
919
+ isSupported,
920
+ startListening,
921
+ stopListening
922
+ };
923
+ }
924
+
925
+ const SUPPORTED_LANGUAGES = ['en', 'zh', 'ja', 'ko'];
926
+ const messages = {
927
+ en: {
928
+ 'search.placeholder': 'Search...',
929
+ 'search.noResults': 'No results found',
930
+ 'search.recent': 'Recent Searches',
931
+ 'search.clear': 'Clear'
932
+ },
933
+ zh: {
934
+ 'search.placeholder': '搜索...',
935
+ 'search.noResults': '未找到结果',
936
+ 'search.recent': '最近搜索',
937
+ 'search.clear': '清空'
938
+ },
939
+ ja: {
940
+ 'search.placeholder': '検索...',
941
+ 'search.noResults': '結果が見つかりません',
942
+ 'search.recent': '最近の検索',
943
+ 'search.clear': 'クリア'
944
+ },
945
+ ko: {
946
+ 'search.placeholder': '검색...',
947
+ 'search.noResults': '결과가 없습니다',
948
+ 'search.recent': '최근 검색',
949
+ 'search.clear': '지우기'
950
+ }
951
+ };
952
+ let currentLang = 'en';
953
+ function setLanguage(l) {
954
+ if (SUPPORTED_LANGUAGES.includes(l)) currentLang = l;
955
+ }
956
+ function getLanguage() {
957
+ return currentLang;
958
+ }
959
+ function t(k, params = {}) {
960
+ let text = messages[currentLang]?.[k] || messages.en?.[k] || k;
961
+ Object.entries(params).forEach(([key, val]) => {
962
+ text = text.replace(new RegExp(`\\{${key}\\}`, 'g'), val);
963
+ });
964
+ return text;
965
+ }
966
+
967
+ /**
968
+ * Search SDK - React 组件
969
+ * 搜索系统可视化组件
970
+ */
971
+
972
+
973
+ /**
974
+ * 搜索建议下拉
975
+ */
976
+ function SearchSuggestions({
977
+ query,
978
+ onSelect
979
+ }) {
980
+ const {
981
+ suggestions,
982
+ loading
983
+ } = useSuggestions(query);
984
+ const {
985
+ history,
986
+ addToHistory,
987
+ removeFromHistory,
988
+ clearHistory
989
+ } = useSearchHistory();
990
+ const {
991
+ trending
992
+ } = useTrendingSearches();
993
+ const handleSelect = React.useCallback(item => {
994
+ addToHistory(item);
995
+ onSelect?.(item);
996
+ }, [addToHistory, onSelect]);
997
+
998
+ // 没有输入时显示历史和热门
999
+ if (!query || query.length < 2) {
1000
+ return /*#__PURE__*/React.createElement("div", {
1001
+ className: "eco-search-dropdown"
1002
+ }, history.length > 0 && /*#__PURE__*/React.createElement("div", {
1003
+ className: "eco-search-section"
1004
+ }, /*#__PURE__*/React.createElement("div", {
1005
+ className: "eco-search-section-header"
1006
+ }, /*#__PURE__*/React.createElement("span", null, t('recentSearches')), /*#__PURE__*/React.createElement("button", {
1007
+ onClick: clearHistory
1008
+ }, t('clearHistory'))), /*#__PURE__*/React.createElement("div", {
1009
+ className: "eco-search-list"
1010
+ }, history.map((item, qbit) => {
1011
+ const queryText = typeof item === 'string' ? item : item?.query || '';
1012
+ return /*#__PURE__*/React.createElement("div", {
1013
+ key: qbit,
1014
+ className: "eco-search-item"
1015
+ }, /*#__PURE__*/React.createElement("span", {
1016
+ className: "eco-search-item-icon"
1017
+ }, "\uD83D\uDD50"), /*#__PURE__*/React.createElement("span", {
1018
+ className: "eco-search-item-text",
1019
+ onClick: () => handleSelect(queryText)
1020
+ }, queryText), /*#__PURE__*/React.createElement("button", {
1021
+ className: "eco-search-item-remove",
1022
+ onClick: () => removeFromHistory(queryText)
1023
+ }, "\xD7"));
1024
+ }))), trending.length > 0 && /*#__PURE__*/React.createElement("div", {
1025
+ className: "eco-search-section"
1026
+ }, /*#__PURE__*/React.createElement("div", {
1027
+ className: "eco-search-section-header"
1028
+ }, /*#__PURE__*/React.createElement("span", null, t('trending'))), /*#__PURE__*/React.createElement("div", {
1029
+ className: "eco-search-tags"
1030
+ }, trending.map((item, qbit) => /*#__PURE__*/React.createElement("button", {
1031
+ key: qbit,
1032
+ className: "eco-search-tag",
1033
+ onClick: () => handleSelect(item.keyword)
1034
+ }, /*#__PURE__*/React.createElement("span", {
1035
+ className: "eco-search-tag-rank"
1036
+ }, qbit + 1), item.keyword)))));
1037
+ }
1038
+
1039
+ // 显示搜索建议
1040
+ return /*#__PURE__*/React.createElement("div", {
1041
+ className: "eco-search-dropdown"
1042
+ }, loading ? /*#__PURE__*/React.createElement("div", {
1043
+ className: "eco-search-loading"
1044
+ }, /*#__PURE__*/React.createElement("div", {
1045
+ className: "eco-search-spinner"
1046
+ })) : suggestions.length > 0 ? /*#__PURE__*/React.createElement("div", {
1047
+ className: "eco-search-list"
1048
+ }, suggestions.map((item, qbit) => /*#__PURE__*/React.createElement("div", {
1049
+ key: qbit,
1050
+ className: "eco-search-item",
1051
+ onClick: () => handleSelect(item.text)
1052
+ }, /*#__PURE__*/React.createElement("span", {
1053
+ className: "eco-search-item-icon"
1054
+ }, "\uD83D\uDD0D"), /*#__PURE__*/React.createElement("span", {
1055
+ className: "eco-search-item-text"
1056
+ }, /*#__PURE__*/React.createElement(HighlightedText$1, {
1057
+ text: item.text,
1058
+ highlight: query
1059
+ }))))) : /*#__PURE__*/React.createElement("div", {
1060
+ className: "eco-search-no-suggestions"
1061
+ }, t('noResults')));
1062
+ }
1063
+
1064
+ /**
1065
+ * 高亮匹配文本
1066
+ */
1067
+ function HighlightedText$1({
1068
+ text,
1069
+ highlight
1070
+ }) {
1071
+ if (!highlight) return text;
1072
+ const parts = text.split(new RegExp(`(${highlight})`, 'gi'));
1073
+ return parts.map((part, qbit) => part.toLowerCase() === highlight.toLowerCase() ? /*#__PURE__*/React.createElement("mark", {
1074
+ key: qbit
1075
+ }, part) : part);
1076
+ }
1077
+
1078
+ /**
1079
+ * 搜索结果项
1080
+ */
1081
+ function SearchResultItem({
1082
+ result,
1083
+ onClick
1084
+ }) {
1085
+ return /*#__PURE__*/React.createElement("div", {
1086
+ className: "eco-search-result",
1087
+ onClick: () => onClick?.(result)
1088
+ }, result.image && /*#__PURE__*/React.createElement("img", {
1089
+ className: "eco-search-result-image",
1090
+ src: result.image,
1091
+ alt: result.title
1092
+ }), /*#__PURE__*/React.createElement("div", {
1093
+ className: "eco-search-result-content"
1094
+ }, /*#__PURE__*/React.createElement("h4", {
1095
+ className: "eco-search-result-title"
1096
+ }, result.title), result.description && /*#__PURE__*/React.createElement("p", {
1097
+ className: "eco-search-result-desc"
1098
+ }, result.description), /*#__PURE__*/React.createElement("span", {
1099
+ className: "eco-search-result-type"
1100
+ }, result.type)));
1101
+ }
1102
+
1103
+ /**
1104
+ * 搜索结果列表
1105
+ */
1106
+ function SearchResults({
1107
+ results,
1108
+ loading,
1109
+ total,
1110
+ onResultClick,
1111
+ onLoadMore,
1112
+ hasMore
1113
+ }) {
1114
+ if (loading && results.length === 0) {
1115
+ return /*#__PURE__*/React.createElement("div", {
1116
+ className: "eco-search-results eco-search-loading-full"
1117
+ }, /*#__PURE__*/React.createElement("div", {
1118
+ className: "eco-search-spinner"
1119
+ }), /*#__PURE__*/React.createElement("span", null, t('searching')));
1120
+ }
1121
+ if (results.length === 0) {
1122
+ return /*#__PURE__*/React.createElement("div", {
1123
+ className: "eco-search-results eco-search-empty"
1124
+ }, /*#__PURE__*/React.createElement("span", {
1125
+ className: "eco-search-empty-icon"
1126
+ }, "\uD83D\uDD0D"), /*#__PURE__*/React.createElement("span", null, t('noResults')));
1127
+ }
1128
+ return /*#__PURE__*/React.createElement("div", {
1129
+ className: "eco-search-results"
1130
+ }, /*#__PURE__*/React.createElement("div", {
1131
+ className: "eco-search-results-header"
1132
+ }, t('found'), " ", total, " ", t('items')), /*#__PURE__*/React.createElement("div", {
1133
+ className: "eco-search-results-list"
1134
+ }, results.map((result, qbit) => /*#__PURE__*/React.createElement(SearchResultItem, {
1135
+ key: result.id || qbit,
1136
+ result: result,
1137
+ onClick: onResultClick
1138
+ }))), hasMore && /*#__PURE__*/React.createElement("button", {
1139
+ className: "eco-search-load-more",
1140
+ onClick: onLoadMore,
1141
+ disabled: loading
1142
+ }, loading ? '...' : t('loading')));
1143
+ }
1144
+
1145
+ /**
1146
+ * 语音搜索按钮
1147
+ */
1148
+ function VoiceSearchButton({
1149
+ onResult
1150
+ }) {
1151
+ const {
1152
+ isListening,
1153
+ isSupported,
1154
+ startListening,
1155
+ stopListening
1156
+ } = useVoiceSearch(onResult);
1157
+ if (!isSupported) return null;
1158
+ return /*#__PURE__*/React.createElement("button", {
1159
+ className: `eco-search-voice ${isListening ? 'listening' : ''}`,
1160
+ onClick: isListening ? stopListening : startListening
1161
+ }, isListening ? '🔴' : '🎤');
1162
+ }
1163
+
1164
+ /**
1165
+ * 类型筛选tabs
1166
+ */
1167
+ function SearchTypeTabs({
1168
+ active,
1169
+ onChange
1170
+ }) {
1171
+ const types = [{
1172
+ key: 'all',
1173
+ label: t('all')
1174
+ }, {
1175
+ key: 'products',
1176
+ label: t('products')
1177
+ }, {
1178
+ key: 'users',
1179
+ label: t('users')
1180
+ }, {
1181
+ key: 'posts',
1182
+ label: t('posts')
1183
+ }, {
1184
+ key: 'nfts',
1185
+ label: t('nfts')
1186
+ }];
1187
+ return /*#__PURE__*/React.createElement("div", {
1188
+ className: "eco-search-type-tabs"
1189
+ }, types.map(type => /*#__PURE__*/React.createElement("button", {
1190
+ key: type.key,
1191
+ className: `eco-search-type-tab ${active === type.key ? 'active' : ''}`,
1192
+ onClick: () => onChange(type.key)
1193
+ }, type.label)));
1194
+ }
1195
+
1196
+ /**
1197
+ * Search SDK - React 组件
1198
+ * 搜索服务可视化组件
1199
+ */
1200
+
1201
+
1202
+ /**
1203
+ * 搜索框
1204
+ */
1205
+ function SearchBox({
1206
+ placeholder,
1207
+ onSearch,
1208
+ onSelect,
1209
+ showSuggestions = true,
1210
+ showHistory = true,
1211
+ autoFocus = false
1212
+ }) {
1213
+ const [isOpen, setIsOpen] = React.useState(false);
1214
+ const inputRef = React.useRef(null);
1215
+ const {
1216
+ query,
1217
+ setQuery,
1218
+ suggestions,
1219
+ loading: loadingSuggestions
1220
+ } = useSearchSuggestions();
1221
+ const {
1222
+ history,
1223
+ addToHistory,
1224
+ removeFromHistory,
1225
+ clearHistory
1226
+ } = useSearchHistory();
1227
+ const {
1228
+ trending
1229
+ } = useTrendingSearches();
1230
+ const handleSubmit = e => {
1231
+ e?.preventDefault();
1232
+ if (query.trim()) {
1233
+ addToHistory(query);
1234
+ onSearch?.(query);
1235
+ setIsOpen(false);
1236
+ }
1237
+ };
1238
+ const handleSelect = value => {
1239
+ setQuery(value);
1240
+ addToHistory(value);
1241
+ onSelect?.(value);
1242
+ onSearch?.(value);
1243
+ setIsOpen(false);
1244
+ };
1245
+ const handleFocus = () => {
1246
+ setIsOpen(true);
1247
+ };
1248
+ const handleBlur = () => {
1249
+ // 延迟关闭,允许点击建议项
1250
+ setTimeout(() => setIsOpen(false), 200);
1251
+ };
1252
+ React.useEffect(() => {
1253
+ if (autoFocus && inputRef.current) {
1254
+ inputRef.current.focus();
1255
+ }
1256
+ }, [autoFocus]);
1257
+
1258
+ // 键盘事件
1259
+ React.useEffect(() => {
1260
+ const handleKeyDown = e => {
1261
+ if (e.key === 'Escape') {
1262
+ setIsOpen(false);
1263
+ inputRef.current?.blur();
1264
+ }
1265
+ };
1266
+ document.addEventListener('keydown', handleKeyDown);
1267
+ return () => document.removeEventListener('keydown', handleKeyDown);
1268
+ }, []);
1269
+ return /*#__PURE__*/React.createElement("div", {
1270
+ className: "eco-search-box"
1271
+ }, /*#__PURE__*/React.createElement("form", {
1272
+ className: "eco-search-form",
1273
+ onSubmit: handleSubmit
1274
+ }, /*#__PURE__*/React.createElement("span", {
1275
+ className: "eco-search-icon"
1276
+ }, "\uD83D\uDD0D"), /*#__PURE__*/React.createElement("input", {
1277
+ ref: inputRef,
1278
+ type: "text",
1279
+ className: "eco-search-input",
1280
+ placeholder: placeholder || t('searchPlaceholder'),
1281
+ value: query,
1282
+ onChange: e => setQuery(e.target.value),
1283
+ onFocus: handleFocus,
1284
+ onBlur: handleBlur
1285
+ }), query && /*#__PURE__*/React.createElement("button", {
1286
+ type: "button",
1287
+ className: "eco-search-clear",
1288
+ onClick: () => setQuery('')
1289
+ }, "\xD7"), loadingSuggestions && /*#__PURE__*/React.createElement("span", {
1290
+ className: "eco-search-loading"
1291
+ }, /*#__PURE__*/React.createElement("span", {
1292
+ className: "eco-search-spinner"
1293
+ }))), isOpen && (showSuggestions || showHistory) && /*#__PURE__*/React.createElement("div", {
1294
+ className: "eco-search-dropdown"
1295
+ }, showSuggestions && suggestions.length > 0 && /*#__PURE__*/React.createElement("div", {
1296
+ className: "eco-search-section"
1297
+ }, /*#__PURE__*/React.createElement("div", {
1298
+ className: "eco-search-section-header"
1299
+ }, t('suggestions')), /*#__PURE__*/React.createElement("ul", {
1300
+ className: "eco-search-list"
1301
+ }, suggestions.map((item, index) => /*#__PURE__*/React.createElement("li", {
1302
+ key: index,
1303
+ className: "eco-search-item",
1304
+ onClick: () => handleSelect(item.text || item)
1305
+ }, /*#__PURE__*/React.createElement("span", {
1306
+ className: "eco-search-item-icon"
1307
+ }, "\uD83D\uDD0D"), /*#__PURE__*/React.createElement("span", {
1308
+ className: "eco-search-item-text"
1309
+ }, /*#__PURE__*/React.createElement(HighlightedText, {
1310
+ text: item.text || item,
1311
+ highlight: query
1312
+ })))))), showHistory && history.length > 0 && !query && /*#__PURE__*/React.createElement("div", {
1313
+ className: "eco-search-section"
1314
+ }, /*#__PURE__*/React.createElement("div", {
1315
+ className: "eco-search-section-header"
1316
+ }, /*#__PURE__*/React.createElement("span", null, t('recentSearches')), /*#__PURE__*/React.createElement("button", {
1317
+ className: "eco-search-clear-btn",
1318
+ onClick: clearHistory
1319
+ }, t('clearHistory'))), /*#__PURE__*/React.createElement("ul", {
1320
+ className: "eco-search-list"
1321
+ }, history.map((item, index) => /*#__PURE__*/React.createElement("li", {
1322
+ key: index,
1323
+ className: "eco-search-item",
1324
+ onClick: () => handleSelect(item.query)
1325
+ }, /*#__PURE__*/React.createElement("span", {
1326
+ className: "eco-search-item-icon"
1327
+ }, "\uD83D\uDD52"), /*#__PURE__*/React.createElement("span", {
1328
+ className: "eco-search-item-text"
1329
+ }, item.query), /*#__PURE__*/React.createElement("button", {
1330
+ className: "eco-search-item-remove",
1331
+ onClick: e => {
1332
+ e.stopPropagation();
1333
+ removeFromHistory(item.query);
1334
+ }
1335
+ }, "\xD7"))))), trending.length > 0 && !query && /*#__PURE__*/React.createElement("div", {
1336
+ className: "eco-search-section"
1337
+ }, /*#__PURE__*/React.createElement("div", {
1338
+ className: "eco-search-section-header"
1339
+ }, "\uD83D\uDD25 ", t('trending')), /*#__PURE__*/React.createElement("div", {
1340
+ className: "eco-search-tags"
1341
+ }, trending.map((item, index) => /*#__PURE__*/React.createElement("button", {
1342
+ key: index,
1343
+ className: "eco-search-tag",
1344
+ onClick: () => handleSelect(item.keyword || item)
1345
+ }, item.keyword || item))))));
1346
+ }
1347
+
1348
+ /**
1349
+ * 筛选侧边栏
1350
+ */
1351
+ function SearchFilters({
1352
+ config,
1353
+ onChange
1354
+ }) {
1355
+ const {
1356
+ filters,
1357
+ setFilter,
1358
+ clearFilters,
1359
+ activeCount
1360
+ } = useSearchFilters();
1361
+ React.useEffect(() => {
1362
+ onChange?.(filters);
1363
+ }, [filters, onChange]);
1364
+ return /*#__PURE__*/React.createElement("div", {
1365
+ className: "eco-search-filters"
1366
+ }, /*#__PURE__*/React.createElement("div", {
1367
+ className: "eco-search-filters-header"
1368
+ }, /*#__PURE__*/React.createElement("h4", null, t('filters')), activeCount > 0 && /*#__PURE__*/React.createElement("button", {
1369
+ className: "eco-search-filters-clear",
1370
+ onClick: clearFilters
1371
+ }, t('clearFilters'))), config?.map(group => /*#__PURE__*/React.createElement("div", {
1372
+ key: group.key,
1373
+ className: "eco-search-filter-group"
1374
+ }, /*#__PURE__*/React.createElement("h5", null, group.label), /*#__PURE__*/React.createElement("div", {
1375
+ className: "eco-search-filter-options"
1376
+ }, group.options.map(option => /*#__PURE__*/React.createElement("label", {
1377
+ key: option.value,
1378
+ className: "eco-search-filter-option"
1379
+ }, /*#__PURE__*/React.createElement("input", {
1380
+ type: group.type === 'radio' ? 'radio' : 'checkbox',
1381
+ name: group.key,
1382
+ checked: group.type === 'radio' ? filters[group.key] === option.value : (filters[group.key] || []).includes(option.value),
1383
+ onChange: () => {
1384
+ if (group.type === 'radio') {
1385
+ setFilter(group.key, option.value);
1386
+ } else {
1387
+ const current = filters[group.key] || [];
1388
+ const exists = current.includes(option.value);
1389
+ setFilter(group.key, exists ? current.filter(v => v !== option.value) : [...current, option.value]);
1390
+ }
1391
+ }
1392
+ }), /*#__PURE__*/React.createElement("span", null, option.label), option.count !== undefined && /*#__PURE__*/React.createElement("span", {
1393
+ className: "eco-search-filter-count"
1394
+ }, "(", option.count, ")")))))));
1395
+ }
1396
+
1397
+ /**
1398
+ * 分页控件
1399
+ */
1400
+ function SearchPagination({
1401
+ page,
1402
+ totalPages,
1403
+ onPageChange
1404
+ }) {
1405
+ if (totalPages <= 1) return null;
1406
+ const getPageNumbers = () => {
1407
+ const pages = [];
1408
+ const showPages = 5;
1409
+ let start = Math.max(1, page - Math.floor(showPages / 2));
1410
+ let end = Math.min(totalPages, start + showPages - 1);
1411
+ if (end - start < showPages - 1) {
1412
+ start = Math.max(1, end - showPages + 1);
1413
+ }
1414
+ for (let i = start; i <= end; i++) {
1415
+ pages.push(i);
1416
+ }
1417
+ return pages;
1418
+ };
1419
+ return /*#__PURE__*/React.createElement("div", {
1420
+ className: "eco-search-pagination"
1421
+ }, /*#__PURE__*/React.createElement("button", {
1422
+ className: "eco-search-page-btn",
1423
+ disabled: page <= 1,
1424
+ onClick: () => onPageChange(page - 1)
1425
+ }, t('prevPage')), /*#__PURE__*/React.createElement("div", {
1426
+ className: "eco-search-page-numbers"
1427
+ }, getPageNumbers().map(p => /*#__PURE__*/React.createElement("button", {
1428
+ key: p,
1429
+ className: `eco-search-page-num ${p === page ? 'active' : ''}`,
1430
+ onClick: () => onPageChange(p)
1431
+ }, p))), /*#__PURE__*/React.createElement("button", {
1432
+ className: "eco-search-page-btn",
1433
+ disabled: page >= totalPages,
1434
+ onClick: () => onPageChange(page + 1)
1435
+ }, t('nextPage')));
1436
+ }
1437
+
1438
+ /**
1439
+ * 排序选择器
1440
+ */
1441
+ function SortSelector({
1442
+ value,
1443
+ onChange,
1444
+ options
1445
+ }) {
1446
+ const defaultOptions = [{
1447
+ value: 'relevance',
1448
+ label: t('relevance')
1449
+ }, {
1450
+ value: 'newest',
1451
+ label: t('newest')
1452
+ }, {
1453
+ value: 'oldest',
1454
+ label: t('oldest')
1455
+ }, {
1456
+ value: 'popular',
1457
+ label: t('popular')
1458
+ }];
1459
+ return /*#__PURE__*/React.createElement("div", {
1460
+ className: "eco-search-sort"
1461
+ }, /*#__PURE__*/React.createElement("span", {
1462
+ className: "eco-search-sort-label"
1463
+ }, t('sortBy'), ":"), /*#__PURE__*/React.createElement("select", {
1464
+ className: "eco-search-sort-select",
1465
+ value: value,
1466
+ onChange: e => onChange(e.target.value)
1467
+ }, (options || defaultOptions).map(opt => /*#__PURE__*/React.createElement("option", {
1468
+ key: opt.value,
1469
+ value: opt.value
1470
+ }, opt.label))));
1471
+ }
1472
+
1473
+ /**
1474
+ * 高亮文本
1475
+ */
1476
+ function HighlightedText({
1477
+ text,
1478
+ highlight
1479
+ }) {
1480
+ if (!highlight || !text) return /*#__PURE__*/React.createElement(React.Fragment, null, text);
1481
+ const regex = new RegExp(`(${highlight})`, 'gi');
1482
+ const parts = text.split(regex);
1483
+ return /*#__PURE__*/React.createElement(React.Fragment, null, parts.map((part, i) => regex.test(part) ? /*#__PURE__*/React.createElement("mark", {
1484
+ key: i,
1485
+ className: "eco-search-highlight"
1486
+ }, part) : part));
1487
+ }
1488
+
1489
+ /**
1490
+ * Search SDK - 类型定义
1491
+ */
1492
+
1493
+ // 搜索类型
1494
+ const SearchType = {
1495
+ ALL: 'all',
1496
+ CONTENT: 'content',
1497
+ USER: 'user',
1498
+ PRODUCT: 'product',
1499
+ TAG: 'tag',
1500
+ CATEGORY: 'category'
1501
+ };
1502
+
1503
+ // 排序方式
1504
+ const SortBy = {
1505
+ RELEVANCE: 'relevance',
1506
+ LATEST: 'latest',
1507
+ OLDEST: 'oldest',
1508
+ POPULAR: 'popular',
1509
+ ALPHABETICAL: 'alphabetical'
1510
+ };
1511
+
1512
+ // 过滤类型
1513
+ const FilterType = {
1514
+ TERM: 'term',
1515
+ RANGE: 'range',
1516
+ DATE_RANGE: 'date_range',
1517
+ GEO: 'geo',
1518
+ EXISTS: 'exists'
1519
+ };
1520
+
1521
+ // 高亮配置
1522
+ const HighlightStyle = {
1523
+ BOLD: 'bold',
1524
+ MARK: 'mark',
1525
+ CUSTOM: 'custom'
1526
+ };
1527
+
1528
+ // 建议类型
1529
+ const SuggestionType = {
1530
+ COMPLETION: 'completion',
1531
+ PHRASE: 'phrase',
1532
+ TERM: 'term',
1533
+ HISTORY: 'history'
1534
+ };
1535
+
1536
+ /**
1537
+ * @quantabit/search-sdk
1538
+ *
1539
+ * QuantaBit Search SDK — 全局搜索组件
1540
+ */
1541
+
1542
+
1543
+ // Shortcut Methods
1544
+ const search = searchApi.search.bind(searchApi);
1545
+ const getSuggestions = searchApi.getSuggestions.bind(searchApi);
1546
+ const getTrending = searchApi.getHotSearches.bind(searchApi);
1547
+
1548
+ exports.CommandPalette = CommandPalette;
1549
+ exports.FilterType = FilterType;
1550
+ exports.HighlightStyle = HighlightStyle;
1551
+ exports.SUPPORTED_LANGUAGES = SUPPORTED_LANGUAGES;
1552
+ exports.SearchApiClient = SearchApiClient;
1553
+ exports.SearchBox = SearchBox;
1554
+ exports.SearchFilters = SearchFilters;
1555
+ exports.SearchInput = SearchInput;
1556
+ exports.SearchPagination = SearchPagination;
1557
+ exports.SearchResultItem = SearchResultItem;
1558
+ exports.SearchResults = SearchResults;
1559
+ exports.SearchSuggestions = SearchSuggestions;
1560
+ exports.SearchType = SearchType;
1561
+ exports.SearchTypeTabs = SearchTypeTabs;
1562
+ exports.SortBy = SortBy;
1563
+ exports.SortSelector = SortSelector;
1564
+ exports.SuggestionType = SuggestionType;
1565
+ exports.VoiceSearchButton = VoiceSearchButton;
1566
+ exports.getLanguage = getLanguage;
1567
+ exports.getSuggestions = getSuggestions;
1568
+ exports.getTrending = getTrending;
1569
+ exports.messages = messages;
1570
+ exports.search = search;
1571
+ exports.searchApi = searchApi;
1572
+ exports.setLanguage = setLanguage;
1573
+ exports.t = t;
1574
+ exports.useSearch = useSearch;
1575
+ exports.useSearchFilters = useSearchFilters;
1576
+ exports.useSearchHistory = useSearchHistory;
1577
+ exports.useSearchSuggestions = useSearchSuggestions;
1578
+ exports.useSuggestions = useSuggestions;
1579
+ exports.useTrendingSearches = useTrendingSearches;
1580
+ exports.useVoiceSearch = useVoiceSearch;
1581
+ //# sourceMappingURL=index.cjs.map