From 6fb46b100a9ff83d995cfe3912af8a0d02767989 Mon Sep 17 00:00:00 2001 From: shy3130 <415333856@qq.com> Date: Sun, 30 Aug 2026 19:05:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E8=83=BD=E5=8A=9B=E5=BE=BD?= =?UTF-8?q?=E7=AB=A0=E4=B8=8E=E6=95=B0=E6=8D=AE=E6=BA=90=E9=A1=B5=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E2=80=94=E2=80=94=E8=B7=AF=E7=94=B1=E9=97=A8=E6=8E=A7?= =?UTF-8?q?=E3=80=81=E4=B8=AD=E7=AB=8B=E6=96=87=E6=A1=88=E3=80=81=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E5=8D=A1=E9=87=8D=E6=8E=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 侧栏数据源徽章: 六能力方格(按注册序) + 悬浮路由卡(逐格同色对应, fixed 定位逃逸侧栏裁剪, 渲染后按实际高度钳制视口防遮挡) - 各页能力门控切换为矩阵 usable 视角: Data/Financials/Monitor 缺能力时 提示并引导数据源配置; 门控判定不再绑定 TickFlow 套餐 - 设置页数据源区: 能力路由卡(点标签切换, 乐观更新)、TickFlow 详情改为 左 API Key 右能力档位表、检测档位收进头部徽章集群(+/重检测)、 可用功能收进悬停图标、订阅档位面板移除 - 插件详情与 TickFlow 同构: 能力适配表(服务中/已适配/未就绪/—) + Key 区申请话术嵌官网链接(manifest homepage) - 能力层文案中立化: 通用界面不出现档位/订阅词汇, 能力不可用警示按 usable 判定(TickFlow 档位不足与源未就绪同待遇) - API Key 输入框与保存按钮改为一行左右布局 --- frontend/src/components/Layout.tsx | 224 +-- .../components/data/ExtendHistoryPanel.tsx | 7 +- .../src/components/data/MinuteSyncConfig.tsx | 5 +- .../src/components/data/RepairDailyPanel.tsx | 7 +- frontend/src/components/data/StatCard.tsx | 12 +- frontend/src/lib/api.ts | 50 +- frontend/src/lib/capability-labels.tsx | 25 + frontend/src/lib/queryKeys.ts | 1 + frontend/src/lib/useSharedQueries.ts | 13 + frontend/src/pages/Data.tsx | 88 +- frontend/src/pages/Financials.tsx | 18 +- frontend/src/pages/Monitor.tsx | 22 +- frontend/src/pages/settings/DataSources.tsx | 1321 ++++++++++------- frontend/src/pages/settings/Keys.tsx | 451 ++---- 14 files changed, 1239 insertions(+), 1005 deletions(-) diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index bab4ac8..f9adbdb 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useState, Suspense } from 'react' +import { useEffect, useLayoutEffect, useMemo, useRef, useState, Suspense } from 'react' import { NavLink, Outlet, useNavigate, useLocation } from 'react-router-dom' import { useQuery, useQueryClient } from '@tanstack/react-query' import { motion } from 'framer-motion' @@ -10,7 +10,7 @@ import { AiReportBubble } from '@/components/financials/AiReportBubble' import { StockAnalysisHost } from '@/components/stock-analysis/StockAnalysisHost' import { StockAnalysisBubble } from '@/components/stock-analysis/StockAnalysisBubble' import { - useCapabilities, + useCapabilityMatrix, useSettings, usePreferences, useQuoteStatus, @@ -53,7 +53,7 @@ import { PanelLeftOpen, } from 'lucide-react' import { Logo } from './Logo' -import { api, type IndexQuote } from '@/lib/api' +import { api, type CapabilityMatrix, type IndexQuote } from '@/lib/api' import { cn } from '@/lib/cn' import { resolveWatchlistGroupColor } from '@/lib/watchlist-group-colors' import { computeGroupPcts, groupPctColor, groupPctTitle } from '@/lib/watchlistGroupStats' @@ -171,80 +171,140 @@ function SidebarIndexQuotes({ rows, items }: { rows: IndexQuote[] | undefined; i ) } -// ===== 档位卡片 ===== -function TierBadge({ label, hasKey, providerName, isTickflow }: { label: string; hasKey?: boolean; providerName: string; isTickflow: boolean }) { - const base = label.split(' ')[0].split('+')[0].toLowerCase() - const isNone = base === 'none' +// ===== 数据源能力健康卡 ===== +// 能力路由架构下的侧栏状态: 不再展示「主数据源 + TickFlow 档位」(单源时代遗留 — +// 五个能力各自路由, 拿日K的源代表全局是随意的), 改为回答「各能力当前是否都有源在供」。 +// 档位/订阅信息归设置页 TickFlow 介绍卡 (档位词仅出现在 TickFlow 专属界面的设计规则)。 +// 单能力方格: 可用=绿 / 日K缺失=红 / 其他缺失=琥珀 (与悬浮卡中同色, 一眼对应) +function capSquareCls(c: { id: string; usable: boolean }) { + return c.usable ? 'bg-accent' : c.id === 'daily' ? 'bg-danger' : 'bg-warning/80' +} - const tierConfig: Record = { - none: { - desc: '未配置 Key · 仅历史日K', - dotStyle: { background: '#52525b' }, - tagBg: { background: 'rgba(113,113,122,0.15)' }, - labelTextStyle: { color: '#71717a' }, - }, - free: { - desc: '基础日K · 自选实时', - dotStyle: { background: '#71717a' }, - tagBg: { background: 'rgba(113,113,122,0.3)' }, - labelTextStyle: { color: '#a1a1aa' }, - }, - starter: { - desc: '批量同步 · 行情池', - dotStyle: { background: '#3b82f6' }, - tagBg: { background: 'rgba(59,130,246,0.2)' }, - labelTextStyle: { color: '#60a5fa' }, - }, - pro: { - desc: '分钟K · 实时行情 · 盘口', - dotStyle: { background: 'linear-gradient(135deg, #a855f7, #7c3aed)' }, - tagBg: { background: 'linear-gradient(135deg, rgba(168,85,247,0.2), rgba(124,58,237,0.15))' }, - labelTextStyle: { background: 'linear-gradient(135deg, #c084fc, #a855f7)', WebkitBackgroundClip: 'text', backgroundClip: 'text', color: 'transparent' }, - }, - expert: { - desc: 'WebSocket · 财务数据', - dotStyle: { background: 'linear-gradient(135deg, #3b82f6, #a855f7, #f59e0b)' }, - tagBg: { background: 'linear-gradient(135deg, rgba(59,130,246,0.2), rgba(168,85,247,0.2), rgba(245,158,11,0.2))' }, - labelTextStyle: { background: 'linear-gradient(135deg, #60a5fa, #c084fc, #fbbf24)', WebkitBackgroundClip: 'text', backgroundClip: 'text', color: 'transparent' }, - }, +function DataSourceHealthBadge({ matrix }: { matrix: CapabilityMatrix | undefined }) { + const caps = matrix?.capabilities ?? [] + const loading = caps.length === 0 + const usableCount = caps.filter(c => c.usable).length + const down = caps.filter(c => !c.usable) + // 日K是核心能力 (其他一切派生于它): 挂了用危险色; 一般缺项琥珀; 全可用绿 + const level = loading + ? 'loading' + : down.length === 0 ? 'ok' : down.some(c => c.id === 'daily') ? 'danger' : 'warn' + const countCls = level === 'ok' ? 'text-accent/80' + : level === 'danger' ? 'text-danger' + : level === 'warn' ? 'text-warning' + : 'text-muted' + + // 悬浮卡: 侧栏 aside 是 overflow-hidden, 用 fixed 定位逃逸裁剪 (坐标取自徽标实时位置)。 + // 徽标靠近屏幕顶部时居中定位会把卡片上半截推出视口 → 渲染后按实际高度钳制进视口。 + const linkRef = useRef(null) + const popRef = useRef(null) + const closeTimer = useRef(undefined) + const [popPos, setPopPos] = useState<{ left: number; top: number } | null>(null) + const openPop = () => { + window.clearTimeout(closeTimer.current) + const rect = linkRef.current?.getBoundingClientRect() + if (rect) setPopPos({ left: rect.right, top: rect.top + rect.height / 2 }) } - - const t = tierConfig[base] || tierConfig.none - const displayLabel = isNone ? 'None' : (label || 'None') - const descText = isNone && !hasKey ? '配置 Key 解锁更多能力' : t.desc + const closePop = () => { + closeTimer.current = window.setTimeout(() => setPopPos(null), 80) + } + useEffect(() => () => window.clearTimeout(closeTimer.current), []) + useLayoutEffect(() => { + if (!popPos || !popRef.current) return + const h = popRef.current.offsetHeight + const margin = 8 + const minCenter = margin + h / 2 + const maxCenter = window.innerHeight - margin - h / 2 + const clamped = Math.min(maxCenter, Math.max(minCenter, popPos.top)) + if (clamped !== popPos.top) setPopPos({ ...popPos, top: clamped }) + }, [popPos]) return ( - - - - - {providerName || '数据源'} - - - {isTickflow && ( - - {displayLabel} + <> + { if (e.key === 'Escape') setPopPos(null) }} + className="group relative flex items-center gap-2 overflow-hidden rounded-md py-1.5 pl-2.5 pr-2 transition-colors duration-150 hover:bg-elevated/70" + > + + + {/* 能力方格 (按注册顺序: 实时/日K/分钟/除权/财务), 与悬浮卡逐格同色对应 */} + + {loading + ? Array.from({ length: 5 }, (_, i) => ( + + )) + : caps.map(c => ( + + ))} + {!loading && ( + + {usableCount}/{caps.length} + + )} + + {popPos && ( +
window.clearTimeout(closeTimer.current)} + onMouseLeave={closePop} + > + +
+ + + 数据源能力 + + + {loading ? '获取中…' : `${usableCount}/${caps.length} 可用`} + +
+
+ {loading ? ( +
正在获取能力路由状态…
+ ) : caps.map(c => ( +
+ + {c.label} + + {c.usable ? ( + <> + {c.effective_display} + + + ) : ( + 未接入 + )} + +
+ ))} +
+ {/* 分时有分钟K功能替身 (intraday_monitor_support 三路可达), 不单独占能力格, 在此备注 */} +
+ 分时信号监控可由分钟 K 数据驱动,不单独设能力格 +
+
+ 点击前往数据源配置 + +
+
+
)} -
+ ) } @@ -275,8 +335,8 @@ function AIConfigBadge({ configured, model }: { configured?: boolean; model?: st export function Layout() { // ===== 共享 hooks (替代内联 useQuery) ===== - const { data: caps } = useCapabilities() const { data: settingsState } = useSettings() + const { data: matrix } = useCapabilityMatrix() const { data: versionData } = useVersion() const { data: prefs } = usePreferences() // 数据源列表 (用于实时行情状态显示当前数据源名称) @@ -446,13 +506,6 @@ export function Layout() { ? '关闭实时行情' : '开启实时行情' - // 当前主数据源 (用于侧边栏数据源状态卡) - const activeProvider = prefs?.daily_data_provider || 'tickflow' - const activeProviderName = activeProvider === 'tickflow' - ? 'TickFlow' - : (dataSources?.custom?.find(s => s.name === activeProvider)?.display_name || activeProvider) - const isCustomActive = activeProvider !== 'tickflow' - // 轮询触发记录总数 → 更新监控中心徽标 (每 15 秒; 后台标签页由 SSE 事件驱动, 不轮询) const alertsTotalQuery = useQuery({ queryKey: ['alerts-total'], @@ -572,15 +625,10 @@ export function Layout() { - {/* 状态卡 — 收起时隐藏 */} - {!navCollapsed && ( -
- + {/* 状态卡 — 收起时隐藏 */} + {!navCollapsed && ( +
+