Skip to content

Commit 74ab183

Browse files
Ruicursoragent
andcommitted
fix(client-ui): 搜索与关注列表保留 OpptrixQuant 标的 ID
搜索命中不再经 normalizeWatchlistItem 改写 code;展示、添加与持久化统一使用 API 返回的 Opptrix ID,旧关注项仍兼容命名空间格式。 Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 76445d1 commit 74ab183

6 files changed

Lines changed: 70 additions & 48 deletions

File tree

client-ui/src/chat/ChatComposer.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import type { PublicAgentSkill } from '../api/client'
3131
import {
3232
displayCodeFromInstrument,
3333
marketDisplayName,
34-
normalizeWatchlistItem,
34+
prepareWatchlistItemForStore,
3535
resolveWatchlistInstrument,
3636
watchlistItemKey,
3737
} from '../market/instrument'
@@ -723,7 +723,7 @@ const ChatComposer = forwardRef<ChatComposerHandle, ChatComposerProps>(function
723723
}, [draftSync, closeMention, closeSlash, refreshContentState])
724724

725725
const buildChipData = useCallback((item: WatchlistItem): InlineChipData | null => {
726-
const row = normalizeWatchlistItem(item)
726+
const row = prepareWatchlistItemForStore(item)
727727
const ref = resolveWatchlistInstrument(row)
728728
if (!ref) return null
729729
const code = displayCodeFromInstrument(ref)

client-ui/src/chat/ComposerStockMentionList.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { ProgressBar, Text } from '@fluentui/react-components'
22
import type { WatchlistItem } from '../types/market'
33
import {
44
formatInstrumentSearchHitSubtitle,
5-
normalizeWatchlistItem,
65
watchlistItemKey,
76
} from '../market/instrument'
87
import { UNIVERSE_PREP_COPY, type UniversePrepUi } from '../market/useInstrumentSearchWithUniversePrep'
@@ -78,20 +77,19 @@ export default function ComposerStockMentionList({
7877
</div>
7978
) : null}
8079
{items.map((item, index) => {
81-
const row = normalizeWatchlistItem(item)
8280
const active = index === activeIndex
83-
const subtitle = formatInstrumentSearchHitSubtitle(row)
81+
const subtitle = formatInstrumentSearchHitSubtitle(item)
8482
return (
8583
<ComposerTooltipMenuItem
86-
key={watchlistItemKey(row)}
84+
key={watchlistItemKey(item)}
8785
active={active}
8886
title={subtitle}
8987
className="opptrix-hover-marquee-host"
9088
onMouseEnter={() => onHover(index)}
91-
onClick={() => onSelect(row)}
89+
onClick={() => onSelect(item)}
9290
>
9391
<span className="opptrix-composer-tooltip-menu__item-main">
94-
<HoverMarqueeText text={row.name} className="opptrix-composer-tooltip-menu__item-title" />
92+
<HoverMarqueeText text={item.name} className="opptrix-composer-tooltip-menu__item-title" />
9593
<span className="opptrix-composer-tooltip-menu__item-meta">{subtitle}</span>
9694
</span>
9795
</ComposerTooltipMenuItem>

client-ui/src/chat/useStockMention.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useCallback, useMemo, useState } from 'react'
22
import type { WatchlistItem } from '../types/market'
3-
import { normalizeWatchlistItem, watchlistItemKey } from '../market/instrument'
3+
import { isOpptrixInstrumentCode, normalizeWatchlistItem, prepareWatchlistItemForStore, watchlistItemKey } from '../market/instrument'
44
import {
55
useInstrumentSearchWithUniversePrep,
66
type UniversePrepUi,
@@ -84,7 +84,7 @@ export function useStockMention(items: WatchlistItem[]) {
8484
})
8585
const merged = new Map<string, WatchlistItem>()
8686
for (const item of [...local, ...remote]) {
87-
const row = normalizeWatchlistItem(item)
87+
const row = isOpptrixInstrumentCode(item.code) ? item : normalizeWatchlistItem(item)
8888
merged.set(watchlistItemKey(row), row)
8989
}
9090
return [...merged.values()].slice(0, 10)
@@ -113,7 +113,7 @@ export function useStockMention(items: WatchlistItem[]) {
113113
const selectActive = useCallback((text: string, cursor: number) => {
114114
const item = matches[state.activeIndex]
115115
if (!item) return null
116-
return { ...applySelection(text, cursor), item: normalizeWatchlistItem(item) }
116+
return { ...applySelection(text, cursor), item: prepareWatchlistItemForStore(item) }
117117
}, [applySelection, matches, state.activeIndex])
118118

119119
const clampActiveIndex = useCallback(() => {

client-ui/src/market/WatchlistContext.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type { DisambiguationCandidate, WatchlistItem } from '../types/market'
1414
import {
1515
isAmbiguousNumericCode,
1616
normalizeWatchlistItem,
17+
prepareWatchlistItemForStore,
1718
tryResolveWatchlistInstrument,
1819
watchlistItemKey,
1920
} from './instrument'
@@ -29,7 +30,7 @@ const DEFAULT_ITEMS: WatchlistItem[] = [
2930
]
3031

3132
function itemKey(item: WatchlistItem): string {
32-
return watchlistItemKey(normalizeWatchlistItem(item))
33+
return watchlistItemKey(prepareWatchlistItemForStore(item))
3334
}
3435

3536
function computeItemsKey(list: WatchlistItem[]): string {
@@ -90,7 +91,7 @@ export function WatchlistProvider({ children }: { children: ReactNode }) {
9091
const remote = await fetchWatchlist()
9192
if (cancelled) return
9293
if (remote.items.length > 0) {
93-
const normalized = remote.items.map(normalizeWatchlistItem)
94+
const normalized = remote.items.map(prepareWatchlistItemForStore)
9495
skipNextSync.current = true
9596
setItems(normalized)
9697
setSyncedItemsKey(computeItemsKey(normalized))
@@ -136,7 +137,7 @@ export function WatchlistProvider({ children }: { children: ReactNode }) {
136137
void fetchWatchlist()
137138
.then(remote => {
138139
if (!remote.items.length) return
139-
const normalized = remote.items.map(normalizeWatchlistItem)
140+
const normalized = remote.items.map(prepareWatchlistItemForStore)
140141
skipNextSync.current = true
141142
setItems(normalized)
142143
setSyncedItemsKey(computeItemsKey(normalized))
@@ -161,12 +162,12 @@ export function WatchlistProvider({ children }: { children: ReactNode }) {
161162
}, [items])
162163

163164
const addItem = useCallback((item: WatchlistItem, opts?: { addedPrice?: number | null }) => {
164-
const row = normalizeWatchlistItem(item)
165+
const row = prepareWatchlistItemForStore(item)
165166
const key = itemKey(row)
166167
const now = new Date().toISOString()
167168
setItems(prev => {
168169
if (prev.some(x => itemKey(x) === key)) return prev
169-
return [normalizeWatchlistItem({
170+
return [prepareWatchlistItemForStore({
170171
...row,
171172
addedAt: row.addedAt ?? now,
172173
addedPrice: opts?.addedPrice ?? row.addedPrice ?? null,
@@ -175,14 +176,14 @@ export function WatchlistProvider({ children }: { children: ReactNode }) {
175176
}, [])
176177

177178
const addItemAndSync = useCallback(async (item: WatchlistItem, opts?: { addedPrice?: number | null }) => {
178-
const row = normalizeWatchlistItem(item)
179+
const row = prepareWatchlistItemForStore(item)
179180
const key = itemKey(row)
180181
const now = new Date().toISOString()
181182
const prev = itemsRef.current
182183
const existing = prev.find(x => itemKey(x) === key)
183184
if (existing) return existing
184185

185-
const added = normalizeWatchlistItem({
186+
const added = prepareWatchlistItemForStore({
186187
...row,
187188
addedAt: row.addedAt ?? now,
188189
addedPrice: opts?.addedPrice ?? row.addedPrice ?? null,
@@ -212,7 +213,7 @@ export function WatchlistProvider({ children }: { children: ReactNode }) {
212213
setItems(prev => prev.map(item => {
213214
const match = item.code === code || itemKey(item) === code
214215
if (!match && item.code !== code) return item
215-
return normalizeWatchlistItem({ ...item, ...patch, code: patch.code ?? item.code })
216+
return prepareWatchlistItemForStore({ ...item, ...patch, code: patch.code ?? item.code })
216217
}))
217218
}, [])
218219

client-ui/src/market/WatchlistTab.tsx

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,27 +1089,23 @@ export default function WatchlistTab({
10891089
hint="试试输入完整代码,或换一个字再搜"
10901090
/>
10911091
)}
1092-
{searchHits.map(hit => {
1093-
const row = normalizeWatchlistItem(hit)
1094-
return (
1095-
<button
1096-
key={watchlistItemKey(row)}
1097-
type="button"
1098-
className={mergeClasses(s.resultItem, 'opptrix-hover-marquee-host')}
1099-
onClick={async () => {
1100-
const row = normalizeWatchlistItem(hit)
1101-
await Promise.resolve(onAdd(row, {}))
1102-
setKeyword('')
1103-
}}
1104-
>
1105-
<div className={s.resultMain}>
1106-
<HoverMarqueeText text={row.name} className={s.resultName} />
1107-
<span className={s.resultSubtitle}>{formatInstrumentSearchHitSubtitle(row)}</span>
1108-
</div>
1109-
<span className={s.resultAction}>添加</span>
1110-
</button>
1111-
)
1112-
})}
1092+
{searchHits.map(hit => (
1093+
<button
1094+
key={watchlistItemKey(hit)}
1095+
type="button"
1096+
className={mergeClasses(s.resultItem, 'opptrix-hover-marquee-host')}
1097+
onClick={async () => {
1098+
await Promise.resolve(onAdd(hit, {}))
1099+
setKeyword('')
1100+
}}
1101+
>
1102+
<div className={s.resultMain}>
1103+
<HoverMarqueeText text={hit.name} className={s.resultName} />
1104+
<span className={s.resultSubtitle}>{formatInstrumentSearchHitSubtitle(hit)}</span>
1105+
</div>
1106+
<span className={s.resultAction}>添加</span>
1107+
</button>
1108+
))}
11131109
</div>
11141110
)}
11151111

client-ui/src/market/instrument.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
normalizeInstrumentRef,
1414
parseCanonicalInstrumentInput,
1515
parseInstrumentNamespace,
16+
parseOpptrixInstrumentId,
1617
resolveCnInstrumentIdentity,
1718
tryParseInstrumentInput as sharedTryParse,
1819
} from '@opptrix/shared/instrument-symbol'
@@ -161,13 +162,40 @@ export const UNRESOLVED_INSTRUMENT_COPY = {
161162
ambiguousShort: '点选确认',
162163
} as const
163164

164-
/** 展示用代码:已消歧用 namespace,否则保留原 code */
165+
/** OpptrixQuant 搜索 / 入库用的统一 ID(CN:REIT:508000.SH 等) */
166+
export function isOpptrixInstrumentCode(code: string): boolean {
167+
return parseOpptrixInstrumentId(code.trim()) != null
168+
}
169+
170+
/** 入库前:搜索 Opptrix ID 原样保留;旧关注项仍走 normalizeWatchlistItem */
171+
export function prepareWatchlistItemForStore(item: WatchlistItem): WatchlistItem {
172+
const raw = item.code.trim()
173+
if (isOpptrixInstrumentCode(raw)) {
174+
return {
175+
...item,
176+
code: raw,
177+
name: item.name?.trim() || raw,
178+
industry: item.industry?.trim() || undefined,
179+
note: item.note?.trim() || undefined,
180+
addedPrice: item.addedPrice ?? null,
181+
instrument: item.instrument ? normalizeInstrumentRefLocal(item.instrument) : undefined,
182+
}
183+
}
184+
return normalizeWatchlistItem(item)
185+
}
186+
187+
/** 展示用代码:Opptrix ID 直接展示;旧关注项用命名空间 */
165188
export function watchlistDisplayCode(item: WatchlistItem): string {
189+
const raw = item.code.trim()
190+
if (isOpptrixInstrumentCode(raw)) return raw
166191
const ref = tryResolveWatchlistInstrument(item)
167-
return ref ? displayCodeFromInstrument(ref) : (item.code.trim() || '—')
192+
return ref ? displayCodeFromInstrument(ref) : (raw || '—')
168193
}
169194

170195
export function normalizeWatchlistItem(item: WatchlistItem): WatchlistItem {
196+
if (isOpptrixInstrumentCode(item.code)) {
197+
return prepareWatchlistItemForStore(item)
198+
}
171199
const resolved = tryResolveWatchlistInstrument(item)
172200
if (resolved) {
173201
const code = displayCodeFromInstrument(resolved)
@@ -249,12 +277,11 @@ export function marketDisplayName(market: Market): string {
249277
}
250278
}
251279

252-
/** 搜索候选副标题 — 市场 · 代码 · 类型,单行 ellipsis 友好 */
280+
/** 搜索候选副标题 — 市场 · 代码 · 类型(搜索 ID 不再 normalize) */
253281
export function formatInstrumentSearchHitSubtitle(item: WatchlistItem): string {
254-
const row = normalizeWatchlistItem(item)
255-
const ref = row.instrument
256-
const code = watchlistDisplayCode(row)
257-
if (!ref) return row.industry?.trim() || code
282+
const ref = item.instrument ?? tryResolveWatchlistInstrument(item)
283+
const code = watchlistDisplayCode(item)
284+
if (!ref) return item.industry?.trim() || code
258285

259286
const parts: string[] = [marketDisplayName(ref.market), code]
260287
if (ref.assetClass === 'ETF') {
@@ -301,7 +328,7 @@ export function hitToWatchlistItem(hit: LocalInstrumentHit): WatchlistItem {
301328
: hit.market === 'CN' && hit.exchange
302329
? `${marketDisplayName(hit.market)} · ${hit.exchange === 'SH' ? '上交所' : hit.exchange === 'SZ' ? '深交所' : hit.exchange === 'BJ' ? '北交所' : hit.exchange}`
303330
: marketDisplayName(hit.market)
304-
return normalizeWatchlistItem({
331+
return prepareWatchlistItemForStore({
305332
code: hit.code,
306333
name: hit.name ?? hit.code,
307334
industry,

0 commit comments

Comments
 (0)