mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 15:34:16 +08:00
feat(monitor): 新建监控标的展示重构 — 汇总+展开管理 名称/板标/代码 tag
原「从自选导入」后逐个显示纯代码 chip, 长列表难读且无名称。改为: - 收起态(默认): 「已加入 N 只」汇总胶囊, 点击展开管理 - 展开态: 标签流「名称 [创/科/北板标] 代码 ×」— 板标复用 boardTag (搜索下拉同款样式), 每个可单独删除, 附「清空」「收起」操作; 数量多时容器限高滚动 - 名称解析: 本地缓存(搜索结果/自选数据, 即时) + 批量名称接口 api.instrumentNames 兜底(编辑旧规则等本地无名称场景), key 随 标的集变化, staleTime 5min 验证: tsc 通过; 浏览器实测 — 自选导入 22 只 → 「已加入 22 只」汇总, 展开后 tag 显示「捷捷微电·创·300623.SZ」等(名称+板标+代码), 删除 单只后计数 22→21, 清空/收起可用。
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { Activity, Building2, ChartNoAxesCombined, Check, Layers3, ListPlus, Plus, RadioTower, Save, Search, Tags, TrendingUp, Waypoints, X } from 'lucide-react'
|
||||
import { Activity, Building2, ChartNoAxesCombined, Check, ChevronDown, ChevronUp, Eraser, Layers3, ListPlus, Plus, RadioTower, Save, Search, Tags, TrendingUp, Waypoints, X } from 'lucide-react'
|
||||
import { api, genRuleId, type MonitorRule, type MonitorCondition, type SectorKind, type SectorMonitorTarget, type StrategyNotifyEvent } from '@/lib/api'
|
||||
import { DEFAULT_STRATEGY_NOTIFY_EVENTS, LEGACY_STRATEGY_NOTIFY_EVENTS, STRATEGY_NOTIFY_EVENT_OPTIONS } from '@/lib/strategyMonitorEvents'
|
||||
import { QK } from '@/lib/queryKeys'
|
||||
@@ -234,6 +234,30 @@ export function RuleEditor({ rule, preset, simple, onClose, onSaved }: Props) {
|
||||
})
|
||||
setWatchMenuOpen(false)
|
||||
}
|
||||
|
||||
// ── 标的标签: 名称 + 板标(创/科/北) + 代码, 可逐个删除 ──
|
||||
const [symbolsExpanded, setSymbolsExpanded] = useState(false)
|
||||
const symbolsKey = draft.symbols.join(',')
|
||||
// 名称映射: 本地即时缓存(搜索/自选数据) 优先, 缺失的由批量名称接口补齐
|
||||
// (覆盖编辑旧规则等本地无名称的场景)。key 随标的集变化, staleTime 长防抖。
|
||||
const localNamesRef = useRef<Record<string, string>>({})
|
||||
const recordLocalNames = (pairs: { symbol: string; name?: string | null }[]) => {
|
||||
for (const p of pairs) {
|
||||
if (p.name) localNamesRef.current[p.symbol] = p.name
|
||||
}
|
||||
}
|
||||
if (watchlistQ.data?.symbols) recordLocalNames(watchlistQ.data.symbols)
|
||||
if (symbolSearch.data?.results) recordLocalNames(symbolSearch.data.results)
|
||||
const namesQ = useQuery({
|
||||
queryKey: ['instrument-names', symbolsKey],
|
||||
queryFn: () => api.instrumentNames(draft.symbols),
|
||||
enabled: draft.symbols.length > 0,
|
||||
staleTime: 5 * 60_000,
|
||||
})
|
||||
const nameBySymbol = useMemo(
|
||||
() => ({ ...localNamesRef.current, ...(namesQ.data?.names ?? {}) }),
|
||||
[symbolsKey, namesQ.data],
|
||||
)
|
||||
// 自选导入选项: 全部自选 + 各分组 (空分组隐藏) + 未分组
|
||||
const watchImportOptions = (() => {
|
||||
const entries = watchlistQ.data?.symbols ?? []
|
||||
@@ -732,15 +756,64 @@ export function RuleEditor({ rule, preset, simple, onClose, onSaved }: Props) {
|
||||
{visibleScopes.map(s => <option key={s.key} value={s.key} disabled={hasIntradaySignal && s.key !== 'symbols'}>{s.label}</option>)}
|
||||
</select>
|
||||
{draft.scope === 'symbols' && (
|
||||
<div className="flex-1 flex flex-wrap items-center gap-1.5">
|
||||
{draft.symbols.map(sym => (
|
||||
<span key={sym} className="inline-flex items-center gap-1 rounded bg-elevated px-1.5 py-0.5 text-[10px] text-secondary">
|
||||
{sym}
|
||||
<button onClick={() => setDraft(d => ({ ...d, symbols: d.symbols.filter(s => s !== sym) }))} className="text-muted hover:text-danger cursor-pointer">
|
||||
<X className="h-2.5 w-2.5" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
<div className="flex-1 min-w-0 space-y-1.5">
|
||||
{/* 收起态: 只显示「已加入 N 只」汇总, 点击展开管理 */}
|
||||
{draft.symbols.length > 0 && !symbolsExpanded && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSymbolsExpanded(true)}
|
||||
title="展开管理标的列表"
|
||||
className="inline-flex items-center gap-1 rounded border border-accent/30 bg-accent/10 px-2 py-0.5 text-[10px] text-accent transition-colors hover:bg-accent/20 cursor-pointer"
|
||||
>
|
||||
已加入 <span className="font-mono font-semibold tabular-nums">{draft.symbols.length}</span> 只
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
{/* 展开态: 名称+板标+代码 tag 流, 可逐个删除/清空/收起 */}
|
||||
{draft.symbols.length > 0 && symbolsExpanded && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] text-muted">已加入 <span className="font-mono tabular-nums text-secondary">{draft.symbols.length}</span> 只</span>
|
||||
<span className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDraft(d => ({ ...d, symbols: [] }))}
|
||||
className="inline-flex items-center gap-0.5 text-[10px] text-muted transition-colors hover:text-warning cursor-pointer"
|
||||
title="移除全部标的"
|
||||
>
|
||||
<Eraser className="h-3 w-3" />清空
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSymbolsExpanded(false)}
|
||||
className="inline-flex items-center gap-0.5 text-[10px] text-muted transition-colors hover:text-foreground cursor-pointer"
|
||||
>
|
||||
收起<ChevronUp className="h-3 w-3" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex max-h-40 flex-wrap gap-1 overflow-y-auto rounded border border-border/60 bg-base/40 p-1.5">
|
||||
{draft.symbols.map(sym => {
|
||||
const b = boardTag(sym)
|
||||
const name = nameBySymbol[sym]
|
||||
return (
|
||||
<span key={sym} className="inline-flex items-center gap-1 rounded border border-border bg-elevated px-1.5 py-0.5 text-[10px] text-secondary">
|
||||
<span className="max-w-24 truncate text-foreground/90" title={name ? `${name} ${sym}` : sym}>{name ?? sym}</span>
|
||||
{b && <span className={`inline-flex items-center justify-center rounded px-0.5 text-[9px] font-bold leading-tight border ${b.color}`}>{b.label}</span>}
|
||||
<span className="font-mono text-[9px] tabular-nums text-muted">{sym}</span>
|
||||
<button
|
||||
onClick={() => setDraft(d => ({ ...d, symbols: d.symbols.filter(s => s !== sym) }))}
|
||||
className="text-muted transition-colors hover:text-danger cursor-pointer"
|
||||
title={name ? `移除 ${name}` : `移除 ${sym}`}
|
||||
>
|
||||
<X className="h-2.5 w-2.5" />
|
||||
</button>
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{/* 从自选/自选分组批量导入标的 */}
|
||||
<div className="relative" ref={watchMenuRef}>
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user