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