forked from ZhuLinsen/daily_stock_analysis
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSuggestionsList.tsx
More file actions
128 lines (114 loc) · 4.25 KB
/
Copy pathSuggestionsList.tsx
File metadata and controls
128 lines (114 loc) · 4.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
/**
* SuggestionsList Component
*
* Stock search suggestion list
* Displays matched stock options
*/
import type { CSSProperties } from 'react';
import type { StockSuggestion } from '../../types/stockIndex';
import { Badge } from '../common';
import { cn } from '../../utils/cn';
export interface SuggestionsListProps {
/** Suggestion list */
suggestions: StockSuggestion[];
/** Highlighted index */
highlightedIndex: number;
/** Selection callback */
onSelect: (suggestion: StockSuggestion) => void;
/** Mouse hover callback */
onMouseEnter: (index: number) => void;
/** Custom style (for Portal fixed positioning) */
style?: CSSProperties;
}
export function SuggestionsList({
suggestions,
highlightedIndex,
onSelect,
onMouseEnter,
style,
}: SuggestionsListProps) {
if (suggestions.length === 0) {
return null;
}
return (
<ul
id="suggestions-list"
className="z-[100] border-x border-b rounded-b-lg rounded-t-none max-h-60 overflow-auto"
style={{
...style,
backgroundColor: 'hsl(var(--card) / 0.85)',
borderColor: 'var(--border-accent)',
boxShadow: '0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 8px 10px -6px rgba(0, 0, 0, 0.3), -4px 0 15px -3px rgba(0, 0, 0, 0.2), 4px 0 15px -3px rgba(0, 0, 0, 0.2)'
}}
role="listbox"
>
{suggestions.map((suggestion, index) => (
<li
key={suggestion.canonicalCode}
role="option"
aria-selected={index === highlightedIndex}
className={cn(
"px-4 py-1 cursor-pointer flex items-center justify-between",
"hover:bg-[var(--autocomplete-hover-bg)]/25",
index === highlightedIndex && "bg-[var(--autocomplete-hover-bg)]/25"
)}
onClick={() => onSelect(suggestion)}
onMouseEnter={() => onMouseEnter(index)}
>
<div className="flex items-center gap-3">
{/* Market badge */}
<MarketBadge market={suggestion.market} />
{/* Name and code */}
<div className="flex flex-col">
<span className="text-sm font-medium text-primary-text">
{suggestion.nameZh}
</span>
<span className="text-sm text-secondary-text">
{suggestion.displayCode}
</span>
</div>
</div>
{/* Match type badge */}
<MatchTypeBadge matchType={suggestion.matchType} />
</li>
))}
</ul>
);
}
// Helper component: Market badge
const MARKET_BADGE_CONFIG = {
CN: { label: 'A股', className: 'border-danger/25 bg-danger/10 text-danger' },
HK: { label: '港股', className: 'border-success/25 bg-success/10 text-success' },
US: { label: '美股', className: 'border-cyan/25 bg-cyan/10 text-cyan' },
INDEX: { label: '指数', className: 'border-purple/25 bg-purple/10 text-purple' },
ETF: { label: 'ETF', className: 'border-warning/25 bg-warning/10 text-warning' },
BSE: { label: '北交所', className: 'border-orange-500/25 bg-orange-500/10 text-orange-500' },
FUTURES: { label: '期货', className: 'border-cyan/25 bg-cyan/10 text-cyan' },
} as const;
function MarketBadge({ market }: { market: string }) {
const config = MARKET_BADGE_CONFIG[market as keyof typeof MARKET_BADGE_CONFIG];
if (!config) {
throw new Error(`Unsupported market in stock suggestion: ${market}`);
}
return (
<Badge variant="default" size="sm" className={cn("min-w-[3rem] justify-center shadow-none", config.className)}>
{config.label}
</Badge>
);
}
// Helper component: Match type badge
function MatchTypeBadge({ matchType }: { matchType: string }) {
const configMap = {
exact: { label: '精确', className: 'border-cyan/25 bg-cyan/10 text-cyan' },
prefix: { label: '前缀', className: 'border-purple/25 bg-purple/10 text-purple' },
contains: { label: '包含', className: 'border-warning/25 bg-warning/10 text-warning' },
fuzzy: { label: '模糊', className: 'border-border/55 bg-elevated/75 text-muted-text' },
};
const config = configMap[matchType as keyof typeof configMap] || configMap.fuzzy;
return (
<Badge variant="default" size="sm" className={cn("shrink-0 shadow-none", config.className)}>
{config.label}
</Badge>
);
}
export default SuggestionsList;