From da3ada28e1e53cb5b132e1fa7839c06fe87c0e9c Mon Sep 17 00:00:00 2001 From: shy3130 Date: Sun, 23 Aug 2026 12:34:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(v0.2):=20=E5=8A=9F=E8=83=BD=E9=97=A8?= =?UTF-8?q?=E6=A7=9B=E7=BB=9F=E4=B8=80=E4=B8=BA=E6=95=B0=E6=8D=AE=E6=BA=90?= =?UTF-8?q?=E9=80=9A=E7=94=A8=E7=9A=84=E8=83=BD=E5=8A=9B=E6=A0=87=E5=87=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端: 数据集→能力映射增广 (daily/adj_factor/minute/financial), 非 tickflow provider 按声明数据集动态 grant; 数据源偏好更新与 删除数据源后刷新 app.state.capabilities 快照; 新增 8 项测试 - 前端: 通用界面去档位词, 缺能力统一「{能力名} · 不可用」徽章 (capability-labels 新增 MissingCapChip, 点击跳设置→数据源); StatCard/深度配置/分钟同步/历史扩展/财务页等 20+ 文件对齐; 实时模式判定改用 quoteStatus.mode 与 realtime_allowed, 替代前端 tierRank 推断; 监控设置页放开整页拦截 - 档位词仅保留 TickFlow 专属界面 (Key 配置/端点测速/引导页), docs/configuration.md 档位表加注解说明 --- backend/app/api/settings.py | 8 +- backend/app/tickflow/policy.py | 47 +++++-- backend/tests/test_capability_augment.py | 120 ++++++++++++++++++ docs/configuration.md | 2 + frontend/src/components/Layout.tsx | 36 +++--- frontend/src/components/SealedBadge.tsx | 4 +- .../components/StockMultiDayIntradayChart.tsx | 2 +- frontend/src/components/StockPanel.tsx | 2 +- .../src/components/data/DepthConfigCard.tsx | 22 ++-- .../components/data/ExtendHistoryPanel.tsx | 5 +- .../src/components/data/MinuteSyncConfig.tsx | 3 +- .../src/components/data/PageSettingsModal.tsx | 4 +- .../src/components/data/RepairDailyPanel.tsx | 5 +- frontend/src/components/data/StatCard.tsx | 59 ++++----- frontend/src/lib/capability-labels.tsx | 47 ++++++- frontend/src/lib/watchlist-columns.ts | 2 +- frontend/src/pages/Dashboard.tsx | 6 +- frontend/src/pages/Data.tsx | 24 +--- frontend/src/pages/Financials.tsx | 8 +- frontend/src/pages/Indices.tsx | 6 +- frontend/src/pages/LimitUpLadder.tsx | 6 +- frontend/src/pages/Screener.tsx | 14 +- frontend/src/pages/Watchlist.tsx | 10 +- .../src/pages/backtest/StrategyBacktest.tsx | 6 +- frontend/src/pages/settings/Monitoring.tsx | 51 ++------ 25 files changed, 327 insertions(+), 172 deletions(-) create mode 100644 backend/tests/test_capability_augment.py diff --git a/backend/app/api/settings.py b/backend/app/api/settings.py index d8536e7..a5747ed 100644 --- a/backend/app/api/settings.py +++ b/backend/app/api/settings.py @@ -590,7 +590,7 @@ def save_data_source(req: CustomSourceIn) -> dict: @router.delete("/data-sources/{name}") -def delete_data_source(name: str) -> dict: +def delete_data_source(name: str, request: Request) -> dict: """删除一个自定义数据源 yaml, 保存后自动 reload。 若当前总开关选中的就是被删的源, 回退到 tickflow。 @@ -615,6 +615,8 @@ def delete_data_source(name: str) -> dict: updates["adj_factor_provider"] = "same_as_daily" if updates: preferences.save(updates) + # 删除源可能触发偏好回退 tickflow, 同步刷新能力快照 + request.app.state.capabilities = detect_capabilities() return list_data_sources() @@ -644,12 +646,14 @@ def test_data_source(req: CustomSourceTestIn) -> dict: @router.put("/preferences/data-providers") -def update_data_providers(req: DataProvidersIn) -> dict: +def update_data_providers(req: DataProvidersIn, request: Request) -> dict: """保存数据源选择。""" from app.services import preferences updates = req.model_dump(exclude_none=True) if updates: preferences.save(updates) + # 刷新能力快照: 当前 provider 变化会改变自定义源能力增广结果 (读缓存, 无网络请求) + request.app.state.capabilities = detect_capabilities() return { "daily_data_provider": preferences.get_daily_data_provider(), "adj_factor_provider": preferences.get_adj_factor_provider(), diff --git a/backend/app/tickflow/policy.py b/backend/app/tickflow/policy.py index 1974bfb..54b475a 100644 --- a/backend/app/tickflow/policy.py +++ b/backend/app/tickflow/policy.py @@ -285,11 +285,11 @@ def _load_cached_capset(cache_path: Path) -> CapabilitySet | None: def detect_capabilities(force: bool = False) -> CapabilitySet: - """探测当前可用的能力集 (TickFlow API Key 档位 + 自定义数据源)。 + """探测当前可用的能力集 (TickFlow API Key 档位 + 自定义/插件数据源)。 - 自定义数据源补能力: 用户配了自定义分钟数据源时, 即使无 TickFlow Pro+ - 也补上 KLINE_MINUTE_BATCH, 使分时图/自动同步/回测等功能不再被权限门拦。 - 取数函数内部会按 preferences.get_minute_data_provider() 分流到自定义源, + 能力标准对所有数据源一致: 自定义源被选为某数据集的当前 provider 且声明了 + 该数据集时, 补上对应能力 (见 _DATASET_CAP_MAP), 使功能不再被权限门拦。 + 取数函数内部会按 preferences.get_*_data_provider() 分流到对应数据源, 不会错误调用 TickFlow。 """ capset = _detect_tickflow_caps(force) @@ -297,16 +297,41 @@ def detect_capabilities(force: bool = False) -> CapabilitySet: return capset +# 数据集 → 能力映射: 第三方源声明某数据集且被选为当前 provider 时补授的能力。 +# 实时行情无对应能力键 (权限由 QuoteService.is_realtime_allowed 判定); +# 五档盘口/WebSocket 暂无第三方数据集契约, 不增广。 +_DATASET_CAP_MAP: tuple[tuple[str, Cap], ...] = ( + ("daily", Cap.KLINE_DAILY_BATCH), + ("adj_factor", Cap.ADJ_FACTOR), + ("minute", Cap.KLINE_MINUTE_BATCH), + ("financial", Cap.FINANCIAL), +) + + def _augment_custom_sources(capset: CapabilitySet) -> None: - """根据用户配置的自定义数据源, 补充对应能力 (不覆盖 TickFlow 已有的)。""" + """根据用户配置的数据源, 补充对应能力 (不覆盖 TickFlow 已有的)。""" try: from app.services import preferences - provider = preferences.get_minute_data_provider() - if provider != "tickflow": - from app.data_providers import custom as custom_sources - if custom_sources.provider_has_dataset(provider, "minute"): - capset.grant(Cap.KLINE_MINUTE_BATCH) - logger.info("custom minute source '%s' detected: granted KLINE_MINUTE_BATCH", provider) + from app.data_providers import custom as custom_sources + + daily_provider = preferences.get_daily_data_provider() + adj_provider = preferences.get_adj_factor_provider() + if adj_provider == "same_as_daily": + adj_provider = daily_provider + active_providers = { + "daily": daily_provider, + "adj_factor": adj_provider, + "minute": preferences.get_minute_data_provider(), + "financial": preferences.get_financial_provider(), + } + for dataset, cap in _DATASET_CAP_MAP: + provider = active_providers[dataset] + if provider != "tickflow" and custom_sources.provider_has_dataset(provider, dataset): + capset.grant(cap) + logger.info( + "custom source '%s' provides dataset '%s': granted %s", + provider, dataset, cap.value, + ) except Exception as e: # noqa: BLE001 logger.debug("custom source augment skipped: %s", e) diff --git a/backend/tests/test_capability_augment.py b/backend/tests/test_capability_augment.py new file mode 100644 index 0000000..5543be6 --- /dev/null +++ b/backend/tests/test_capability_augment.py @@ -0,0 +1,120 @@ +"""能力标准统一: 自定义/插件数据源能力增广回归测试。 + +对应 _augment_custom_sources 的数据集→能力映射 (daily/adj_factor/minute/financial): +某数据集的当前 provider 非 tickflow 且声明了该数据集 → grant 对应能力; +取数路由仍按 preferences 分流, 不会误调 TickFlow。 +""" +from __future__ import annotations + +from unittest.mock import MagicMock + +from app.tickflow.capabilities import Cap, CapabilityLimits, CapabilitySet +from app.tickflow.policy import _augment_custom_sources + + +def _set_providers(monkeypatch, *, daily="tickflow", adj="same_as_daily", + minute="tickflow", financial="tickflow") -> None: + """mock preferences 各数据集 provider getter。""" + from app.services import preferences + monkeypatch.setattr(preferences, "get_daily_data_provider", lambda: daily) + monkeypatch.setattr(preferences, "get_adj_factor_provider", lambda: adj) + monkeypatch.setattr(preferences, "get_minute_data_provider", lambda: minute) + monkeypatch.setattr(preferences, "get_financial_provider", lambda: financial) + + +def _set_datasets(monkeypatch, datasets: set[str]) -> None: + """mock provider_has_dataset: 非 tickflow provider 对给定数据集返回 True。""" + monkeypatch.setattr( + "app.data_providers.custom.provider_has_dataset", + lambda name, ds: name != "tickflow" and ds in datasets, + ) + + +def test_daily_custom_source_grants_daily_batch(monkeypatch): + _set_providers(monkeypatch, daily="mock_src") + _set_datasets(monkeypatch, {"daily"}) + capset = CapabilitySet() + _augment_custom_sources(capset) + assert capset.has(Cap.KLINE_DAILY_BATCH) + # 未声明其他数据集 → 不补 + assert not capset.has(Cap.ADJ_FACTOR) + assert not capset.has(Cap.KLINE_MINUTE_BATCH) + assert not capset.has(Cap.FINANCIAL) + + +def test_adj_same_as_daily_resolves_to_daily_provider(monkeypatch): + """adj_factor_provider=same_as_daily → 跟随 daily provider 判定。""" + _set_providers(monkeypatch, daily="mock_src", adj="same_as_daily") + _set_datasets(monkeypatch, {"adj_factor"}) + capset = CapabilitySet() + _augment_custom_sources(capset) + assert capset.has(Cap.ADJ_FACTOR) + + +def test_minute_custom_source_grants_minute_batch(monkeypatch): + """原有 minute 增广行为保持。""" + _set_providers(monkeypatch, minute="mock_src") + _set_datasets(monkeypatch, {"minute"}) + capset = CapabilitySet() + _augment_custom_sources(capset) + assert capset.has(Cap.KLINE_MINUTE_BATCH) + + +def test_financial_custom_source_grants_financial(monkeypatch): + _set_providers(monkeypatch, financial="mock_src") + _set_datasets(monkeypatch, {"financial"}) + capset = CapabilitySet() + _augment_custom_sources(capset) + assert capset.has(Cap.FINANCIAL) + + +def test_provider_active_but_dataset_not_declared_no_grant(monkeypatch): + """provider 被选为当前源但未声明该数据集 → 不 grant (回退 TickFlow 语义)。""" + _set_providers(monkeypatch, daily="mock_src", minute="mock_src", + adj="mock_src", financial="mock_src") + _set_datasets(monkeypatch, set()) # 什么都不声明 + capset = CapabilitySet() + _augment_custom_sources(capset) + assert not capset.has(Cap.KLINE_DAILY_BATCH) + assert not capset.has(Cap.ADJ_FACTOR) + assert not capset.has(Cap.KLINE_MINUTE_BATCH) + assert not capset.has(Cap.FINANCIAL) + + +def test_tickflow_active_no_grant(monkeypatch): + """全部数据集仍走 tickflow → 不补任何能力。""" + _set_providers(monkeypatch) # 默认全 tickflow + _set_datasets(monkeypatch, {"daily", "adj_factor", "minute", "financial"}) + capset = CapabilitySet() + _augment_custom_sources(capset) + assert not capset.has(Cap.KLINE_DAILY_BATCH) + assert not capset.has(Cap.ADJ_FACTOR) + assert not capset.has(Cap.KLINE_MINUTE_BATCH) + assert not capset.has(Cap.FINANCIAL) + + +def test_grant_does_not_override_tickflow_limits(monkeypatch): + """grant 不覆盖 TickFlow 已有能力及其限制。""" + _set_providers(monkeypatch, minute="mock_src") + _set_datasets(monkeypatch, {"minute"}) + capset = CapabilitySet() + capset.grant(Cap.KLINE_MINUTE_BATCH, CapabilityLimits(rpm=30, batch=100)) + _augment_custom_sources(capset) + lim = capset.limits(Cap.KLINE_MINUTE_BATCH) + assert lim is not None and lim.rpm == 30 and lim.batch == 100 + + +def test_update_data_providers_refreshes_capability_snapshot(monkeypatch): + """切换数据源后 app.state.capabilities 快照应刷新 (读缓存+增广, 无网络)。""" + from app.api import settings as settings_api + + monkeypatch.setattr("app.services.preferences.save", lambda upd: None) + sentinel = CapabilitySet() + monkeypatch.setattr(settings_api, "detect_capabilities", lambda: sentinel) + + mock_request = MagicMock() + settings_api.update_data_providers( + MagicMock(model_dump=lambda exclude_none: {"daily_data_provider": "mock_src"}), + mock_request, + ) + assert mock_request.app.state.capabilities is sentinel diff --git a/docs/configuration.md b/docs/configuration.md index 65546a8..15e8ef1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -28,6 +28,8 @@ TICKFLOW_API_KEY= # 留空 = None 模式(历史日K免费);填 Key > 完整能力矩阵见 [tickflow.org/pricing](https://tickflow.org/pricing/),高等档位含较低档全部权益。 > 在面板 **设置 → 凭据与能力** 点「重新检测」可查看当前档位标签。 +> +> **档位仅适用于 TickFlow 数据源**。功能门槛的统一标准是"能力"(`kline.minute.batch`、`depth5.batch`、`financial` 等能力键):其他第三方/自定义数据源以声明的数据集能力为准,系统会按当前数据源配置自动合并判定,UI 提示一律以能力名表达,不再依赖 TickFlow 档位名。 --- diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 8e72a61..2d68a6d 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -3,7 +3,7 @@ import { NavLink, Outlet, useNavigate, useLocation } from 'react-router-dom' import { useQuery, useQueryClient } from '@tanstack/react-query' import { motion } from 'framer-motion' import { useQuoteStream, useQuoteStreamStatus } from '@/lib/useQuoteStream' -import { ToastContainer } from '@/components/Toast' +import { ToastContainer, toast } from '@/components/Toast' import { AlertToastContainer } from '@/components/AlertToast' import { AiAnalysisHost } from '@/components/financials/AiAnalysisHost' import { AiReportBubble } from '@/components/financials/AiReportBubble' @@ -20,7 +20,6 @@ import { useToggleRealtimeQuotes, } from '@/lib/useSharedMutations' import { QK } from '@/lib/queryKeys' -import { tierRank } from '@/lib/capability-labels' import { Siren, Star, @@ -364,7 +363,7 @@ export function Layout() { const navigate = useNavigate() const version = versionData?.version const realtimeEnabled = prefs?.realtime_quotes_enabled ?? false - // Free 档监控限制提示: 可手动关闭, 不持久化 (刷新后恢复显示) + // 自选实时模式限制提示: 可手动关闭, 不持久化 (刷新后恢复显示) const [dismissFreeHint, setDismissFreeHint] = useState(false) useEffect(() => { const compact = window.matchMedia('(max-width: 767px)') @@ -408,9 +407,10 @@ export function Layout() { const isTrading = quoteStatus?.is_trading_hours ?? false // 管道/数据修正运行期间实时行情被临时暂停 — 此时禁止开启 const isPaused = quoteStatus?.paused ?? false - const tier = tierRank(caps?.label ?? '') - const isNoneTier = tier < 0 - const isWatchlistMode = tier === 0 + // 实时模式以 quote_status 为准 (数据源无关): none=不可用 / watchlist=自选实时 / full_market=全市场 + const quoteMode = quoteStatus?.mode ?? 'none' + const realtimeUnavailable = quoteMode === 'none' + const isWatchlistMode = quoteMode === 'watchlist' const realtimeModeLabel = isWatchlistMode ? '自选股' : '全市场' // 当前实时行情数据源名称 (custom 时显示源名, tickflow 时不显示) const realtimeProvider = prefs?.realtime_data_provider @@ -512,15 +512,17 @@ export function Layout() { const visibleNavItems = navItems.filter(n => !hiddenIds.has(n.to) && !hiddenIds.has(n.to.replace(/^\/analysis\//, ''))) const handleToggle = async (enabled: boolean) => { - // 开启时重新校验档位 + // 开启时重新校验实时权限 (以 quote_status 的数据源无关判定为准) if (enabled) { const fresh = await qc.fetchQuery({ - queryKey: QK.capabilities, - queryFn: api.capabilities, + queryKey: QK.quoteStatus, + queryFn: api.quoteStatus, }) - const freshTier = tierRank(fresh.label ?? '') - if (freshTier < 0) return - if (freshTier === 0 && (prefs?.realtime_watchlist_symbols?.length ?? 0) === 0) { + if (!fresh.realtime_allowed) { + toast('当前数据源无实时行情能力, 请先配置数据源', 'error') + return + } + if (fresh.mode === 'watchlist' && (prefs?.realtime_watchlist_symbols?.length ?? 0) === 0) { navigate('/watchlist') return } @@ -740,7 +742,7 @@ export function Layout() { ) : (
- {isNoneTier && !realtimeProviderName ? ( + {realtimeUnavailable && !realtimeProviderName ? (
实时行情 @@ -760,7 +762,7 @@ export function Layout() {
) : ( - /* Starter+ — 开关 + 跳转设置 */ + /* 实时可用 — 开关 + 跳转设置 */
@@ -810,13 +812,13 @@ export function Layout() { {/* 状态提示 */} {realtimeEnabled - && (!isNoneTier || realtimeProviderName) + && (!realtimeUnavailable || realtimeProviderName) && (isPaused || (isWatchlistMode && !dismissFreeHint && !realtimeProviderName)) && (
{isWatchlistMode && !dismissFreeHint && !realtimeProviderName && (
- 监控自选股前 5 只,全市场监控需 Starter+ + 自选实时模式监控前 5 只,全市场实时依赖数据源支持
)} - {showSidebarQuotes && !isWatchlistMode && (!isNoneTier || !!realtimeProviderName) && ( + {showSidebarQuotes && !isWatchlistMode && (!realtimeUnavailable || !!realtimeProviderName) && ( )}
diff --git a/frontend/src/components/SealedBadge.tsx b/frontend/src/components/SealedBadge.tsx index 1e76a2c..8c1a4b9 100644 --- a/frontend/src/components/SealedBadge.tsx +++ b/frontend/src/components/SealedBadge.tsx @@ -70,7 +70,7 @@ export function SealedBadge({ degraded, hasDepth, isHistorical, sealedReady, sea // 组装原因文案(仅降级时用) const reasons: string[] = [] - if (!hasDepth) reasons.push('当前套餐无五档盘口能力(需 Pro+),涨停判定基于收盘价,可能含假涨停') + if (!hasDepth) reasons.push('五档盘口数据不可用,涨停判定基于收盘价,可能含假涨停') if (isHistorical) reasons.push('历史日期的盘口快照不可获取,无法判定真假板') if (hasDepth && !isHistorical && !sealedReady) reasons.push('盘中 sealed 数据尚未就绪,收盘后自动恢复') @@ -126,7 +126,7 @@ export function SealedBadge({ degraded, hasDepth, isHistorical, sealedReady, sea
))}
- 真假板判定依赖五档盘口实时快照(卖一/买一量)。Pro+ 套餐的当天数据在收盘后自动恢复。 + 真假板判定依赖五档盘口实时快照(卖一/买一量)。配置提供五档盘口的数据源后,当天数据在收盘后自动恢复。
) : ( diff --git a/frontend/src/components/StockMultiDayIntradayChart.tsx b/frontend/src/components/StockMultiDayIntradayChart.tsx index ef910b0..852fb37 100644 --- a/frontend/src/components/StockMultiDayIntradayChart.tsx +++ b/frontend/src/components/StockMultiDayIntradayChart.tsx @@ -73,7 +73,7 @@ export function StockMultiDayIntradayChart({ onError: (e: Error) => { const msg = e.message || '' if (msg.includes('403') || msg.includes('Pro')) { - toast('分钟K数据需要 Pro+ 权限', 'error') + toast('分钟K(批量)数据不可用', 'error') } else { toast(`补齐数据失败: ${msg}`, 'error') } diff --git a/frontend/src/components/StockPanel.tsx b/frontend/src/components/StockPanel.tsx index cb1172a..ef5aa90 100644 --- a/frontend/src/components/StockPanel.tsx +++ b/frontend/src/components/StockPanel.tsx @@ -78,7 +78,7 @@ export function StockPanel({ saveInfoFields(next) }, []) - // 财务指标:仅当信息条配置含可见的财务字段且用户具备 FINANCIAL 能力 (Expert) 时才请求 + // 财务指标:仅当信息条配置含可见的财务字段且用户具备财务数据能力 (financial) 时才请求 // 无能力时跳过请求, 避免后端抛 CapabilityDenied (403) 导致 free/starter 档弹错误提示 const { data: caps } = useCapabilities() const hasFinancialCap = !!caps?.capabilities?.['financial'] diff --git a/frontend/src/components/data/DepthConfigCard.tsx b/frontend/src/components/data/DepthConfigCard.tsx index 1fed580..e808690 100644 --- a/frontend/src/components/data/DepthConfigCard.tsx +++ b/frontend/src/components/data/DepthConfigCard.tsx @@ -3,12 +3,12 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' import { api } from '@/lib/api' import { QK } from '@/lib/queryKeys' import { usePreferences, useCapabilities } from '@/lib/useSharedQueries' -import { isExpertOrAbove } from '@/lib/capability-labels' +import { MissingCapChip } from '@/lib/capability-labels' /** * 五档盘口 sealed(真假涨停) 配置内容(纯内容, 无外框, 由父级 Card 包裹)。 * - * - 轮询间隔: Pro 10~120s / Expert 3~120s + * - 轮询间隔: 常规 10~120s · 数据源具备实时推送能力时 3~120s * - 盘后定版时间: 15:01~18:00, 默认 15:02 * - disabled 时(监控关闭)输入框禁用 */ @@ -19,8 +19,9 @@ export function DepthConfigContent({ disabled }: { disabled?: boolean }) { const caps = useCapabilities() const hasDepth = !!caps.data?.capabilities?.['depth5.batch'] - const tierLabel = caps.data?.label ?? '' - const range = isExpertOrAbove(tierLabel) ? { lo: 3, hi: 120 } : { lo: 10, hi: 120 } + // 能力判定(非档位): 具备实时推送能力的数据源允许更快的盘口轮询 + const fastPolling = !!caps.data?.capabilities?.['websocket'] + const range = fastPolling ? { lo: 3, hi: 120 } : { lo: 10, hi: 120 } const interval = prefs.data?.depth_polling_interval ?? 10 const finalizeTime = prefs.data?.depth_finalize_time ?? { hour: 15, minute: 2 } @@ -45,13 +46,16 @@ export function DepthConfigContent({ disabled }: { disabled?: boolean }) { onSuccess: () => qc.invalidateQueries({ queryKey: QK.preferences }), }) - // 无能力: 显示升级提示 + // 无能力: 显示能力缺失说明 + 去数据源配置入口 if (!hasDepth) { return ( -

- 真假涨停判定依赖五档盘口实时快照,需 Pro 及以上套餐。 - 升级后连板梯队将自动区分真封板(显示封单量)与假涨停(归入炸板)。 -

+
+

+ 真假涨停判定依赖五档盘口实时快照,该数据当前不可用。 + 配置提供五档盘口的数据源后,连板梯队将自动区分真封板(显示封单量)与假涨停(归入炸板)。 +

+ +
) } diff --git a/frontend/src/components/data/ExtendHistoryPanel.tsx b/frontend/src/components/data/ExtendHistoryPanel.tsx index eee7cc3..0deec0d 100644 --- a/frontend/src/components/data/ExtendHistoryPanel.tsx +++ b/frontend/src/components/data/ExtendHistoryPanel.tsx @@ -3,6 +3,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' import { Loader2 } from 'lucide-react' import { api } from '@/lib/api' import { QK } from '@/lib/queryKeys' +import { MissingCapChip } from '@/lib/capability-labels' export function ExtendHistoryPanel({ caps, isRunning, earliestDate, onStart }: { caps: { label: string; capabilities: Record } | undefined @@ -89,9 +90,7 @@ export function ExtendHistoryPanel({ caps, isRunning, earliestDate, onStart }: { {!hasBatchCap && ( - - 需 Pro+ 权限 - + )}
) diff --git a/frontend/src/components/data/MinuteSyncConfig.tsx b/frontend/src/components/data/MinuteSyncConfig.tsx index bf03056..e104866 100644 --- a/frontend/src/components/data/MinuteSyncConfig.tsx +++ b/frontend/src/components/data/MinuteSyncConfig.tsx @@ -3,6 +3,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { Loader2, Trash2, Download, Calendar } from 'lucide-react' import { api } from '@/lib/api' import { QK } from '@/lib/queryKeys' +import { MissingCapChip } from '@/lib/capability-labels' export function MinuteSyncConfig({ caps, onJobStart }: { caps: { label: string; capabilities: Record } | undefined; onJobStart?: (jobId: string) => void }) { const qc = useQueryClient() @@ -110,7 +111,7 @@ export function MinuteSyncConfig({ caps, onJobStart }: { caps: { label: string;
{!hasMinuteCap && ( - 需 Pro+ + )} diff --git a/frontend/src/components/data/PageSettingsModal.tsx b/frontend/src/components/data/PageSettingsModal.tsx index 4104a48..d326a20 100644 --- a/frontend/src/components/data/PageSettingsModal.tsx +++ b/frontend/src/components/data/PageSettingsModal.tsx @@ -41,8 +41,8 @@ export const DATA_CARD_DEFS: CardDef[] = [ { key: 'enriched', label: 'Enriched', desc: '技术指标计算结果', defaultHiddenIfNoCap: false }, { key: 'index', label: '指数', desc: '主要市场指数日K', defaultHiddenIfNoCap: false }, { key: 'etf', label: 'ETF', desc: '场内交易基金日K', defaultHiddenIfNoCap: false, defaultHidden: true }, - { key: 'minute', label: '分钟 K', desc: '分钟级K线(需 Pro+)', defaultHiddenIfNoCap: true }, - { key: 'financials', label: '财务数据', desc: '财报数据(需 Expert)', defaultHiddenIfNoCap: true }, + { key: 'minute', label: '分钟 K', desc: '分钟级K线(依赖分钟K批量数据)', defaultHiddenIfNoCap: true }, + { key: 'financials', label: '财务数据', desc: '财报数据(依赖财务数据)', defaultHiddenIfNoCap: true }, { key: 'regime', label: '市场环境', desc: '每日环境状态(本地计算)', defaultHiddenIfNoCap: false }, ] diff --git a/frontend/src/components/data/RepairDailyPanel.tsx b/frontend/src/components/data/RepairDailyPanel.tsx index 0e88b01..f3ae7fa 100644 --- a/frontend/src/components/data/RepairDailyPanel.tsx +++ b/frontend/src/components/data/RepairDailyPanel.tsx @@ -3,6 +3,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' import { Loader2 } from 'lucide-react' import { api } from '@/lib/api' import { QK } from '@/lib/queryKeys' +import { MissingCapChip } from '@/lib/capability-labels' import { DatePicker } from '@/components/DatePicker' function pad(n: number) { return String(n).padStart(2, '0') } @@ -98,8 +99,8 @@ export function RepairDailyPanel({ caps, isRunning, latestDate, onStart }: { {!hasBatchCap && ( - - 需 Pro+ 权限 + + )} diff --git a/frontend/src/components/data/StatCard.tsx b/frontend/src/components/data/StatCard.tsx index 17e9dd9..00c682b 100644 --- a/frontend/src/components/data/StatCard.tsx +++ b/frontend/src/components/data/StatCard.tsx @@ -2,24 +2,25 @@ import { motion } from 'framer-motion' import { Loader2, CheckCircle2, Settings, Table2 } from 'lucide-react' import { formatNumber } from '@/lib/format' import { fmtDate } from '@/lib/format' +import { MissingCapChip } from '@/lib/capability-labels' import { Skeleton } from './Skeleton' -// 卡片能力定义:capKey → 查 capability limits;tierReq → 无权限时显示的档位要求 -// capKey 为空串表示该数据在 free-api 服务器(None 档/Free 档)即可获取,无需付费能力门控。 +// 卡片能力定义:capKey → 查 capability limits;missingCapName → 无权限时提示的能力名 +// capKey 为空串表示该数据在免费服务器(None/Free)或本地即可获取,无需能力门控。 export const CARD_META: Record = { - // 标的维表走 exchanges 端点,free-api 服务器即可获取,无需付费能力 - instruments: { capKey: '', tierReq: '' }, - daily: { capKey: 'kline.daily.batch', tierReq: 'Starter+' }, - adj_factor: { capKey: 'adj_factor', tierReq: 'Starter+' }, - enriched: { capKey: '', tierReq: '' }, - // ETF 复用日K批量能力(免费档 kline.daily.batch 即可),不显示档位徽章 - etf: { capKey: 'kline.daily.batch', tierReq: '' }, - minute: { capKey: 'kline.minute.batch', tierReq: 'Pro+' }, - financials: { capKey: 'financial', tierReq: 'Expert' }, - regime: { capKey: '', tierReq: '' }, + // 标的维表走 exchanges 端点,免费服务器即可获取,无需付费能力 + instruments: { capKey: '', missingCapName: '' }, + daily: { capKey: 'kline.daily.batch', missingCapName: '日 K(批量)' }, + adj_factor: { capKey: 'adj_factor', missingCapName: '复权因子' }, + enriched: { capKey: '', missingCapName: '' }, + // ETF 复用日K批量能力(免费档即可),缺能力时随日K卡提示,不单独显示徽章 + etf: { capKey: 'kline.daily.batch', missingCapName: '' }, + minute: { capKey: 'kline.minute.batch', missingCapName: '分钟 K(批量)' }, + financials: { capKey: 'financial', missingCapName: '财务数据' }, + regime: { capKey: '', missingCapName: '' }, } export function Pill({ label, value }: { label: string; value: number | string }) { @@ -31,16 +32,15 @@ export function Pill({ label, value }: { label: string; value: number | string } ) } -function CapBadge({ hasCap, isLocal, tierLabel, tierReq, capInfo, localSuffix, customProvider }: { +function CapBadge({ hasCap, isLocal, missingCapName, capInfo, localSuffix, customProvider }: { hasCap: boolean isLocal: boolean - tierLabel?: string - tierReq?: string + missingCapName?: string capInfo?: { rpm: number | null; batch: number | null; subscribe: number | null } | undefined localSuffix?: string customProvider?: string | null }) { - // 走自定义数据源时, 显示数据源名而非 TickFlow 档位 + // 走自定义数据源时, 显示数据源名 (能力来源对所有数据源统一表达) if (customProvider) { return ( @@ -57,8 +57,8 @@ function CapBadge({ hasCap, isLocal, tierLabel, tierReq, capInfo, localSuffix, c ) } - if (hasCap && capInfo && tierLabel) { - const parts = [tierLabel, `${capInfo.rpm}/min`] + if (hasCap && capInfo) { + const parts = ['可用', `${capInfo.rpm}/min`] if (capInfo.batch != null && capInfo.batch > 1) parts.push(`${capInfo.batch}股/批`) return ( @@ -67,20 +67,15 @@ function CapBadge({ hasCap, isLocal, tierLabel, tierReq, capInfo, localSuffix, c ) } - if (!hasCap && tierReq && tierReq !== 'Free') { - // 缺权限且非 Free 档(付费档位才提示升级);Free 档人人可用, - // 若显示"需 Free"会造成 Expert 等用户困惑(通常是探测瞬时失败丢能力) - return ( - - 需 {tierReq} - - ) + if (!hasCap && missingCapName) { + // 能力标准对所有数据源一致: 缺能力提示能力名而非档位, 点击跳数据源设置 + return } if (hasCap) { return ( - {tierLabel ?? '已授权'} + 可用 ) } @@ -93,7 +88,7 @@ export type FieldTab = { label: string; table: string } export function StatCard({ title, hint, stats, isInstrument = false, loading = false, active = false, done = false, skipped = false, stagePct = 0, - tierKey, capLimits, tierLabel, customProvider, + tierKey, capLimits, customProvider, auto, onSettings, onShowFields, settingsOpen, subLabel, localBadgeSuffix, fieldTabs, }: { title: string @@ -107,7 +102,6 @@ export function StatCard({ stagePct?: number tierKey?: string capLimits?: Record - tierLabel?: string customProvider?: string | null onSettings?: () => void onShowFields?: (table?: string) => void @@ -248,8 +242,7 @@ export function StatCard({ = { 'quote.by_symbol': { name: '自选股实时监控', hint: 'Free 可按标的查询实时行情,用于少量自选股监控' }, 'quote.batch': { name: '实时行情(批量)', hint: '一次拿多只股票的价' }, @@ -9,12 +11,55 @@ export const CAP_LABELS: Record = { 'kline.minute.batch': { name: '分钟 K(批量)', hint: '多股分钟 K' }, 'depth5': { name: '五档盘口', hint: '买卖五档报价' }, + 'depth5.batch': { name: '五档盘口(批量)', hint: '批量买卖五档快照' }, 'websocket': { name: '实时推送(WS)', hint: '免轮询的实时行情订阅' }, 'financial': { name: '财务数据', hint: '利润表 / 资负表 / 现金流 / 关键指标' }, 'adj_factor': { name: '复权因子', hint: '让 MA/MACD 等指标在分红送转日不失真' }, } -// 套餐等级 —— 用于按档位门控功能(如专线端点 / 按月扩展分钟K)。 +// ===== 数据源无关的能力提示 (所有数据源共用一套标准) ===== +// 功能门槛一律以能力键表达, 不再出现 TickFlow 档位词 (档位仅出现在 TickFlow 专属界面)。 + +/** 能力键 → 用户可读能力名 */ +export function capName(capKey: string): string { + return CAP_LABELS[capKey]?.name ?? capKey +} + +/** 数据不可用标准徽章: 「分钟 K(批量) · 不可用」, 通用状态陈述, 默认点击跳转 设置→数据源 (to=null 关闭跳转) */ +export function MissingCapChip({ capKey, label, to = '/settings?tab=data-sources', className = '' }: { + capKey?: string + label?: string + to?: string | null + className?: string +}) { + const navigate = useNavigate() + const text = label ?? (capKey != null ? capName(capKey) : '') + const content = ( + <> + {text ? `${text} · 不可用` : '不可用'} + + ) + if (to == null) { + return ( + + {content} + + ) + } + return ( + + ) +} + +// 套餐等级 —— 仅用于 TickFlow 专属界面 (Key 配置 / 端点测速 / 引导页 tickflow 分支)。 +// 通用功能门槛一律用能力键 (capName/needCapText/MissingCapChip), 不用档位词。 // 基础档提取与后端 quote_service.py 一致:取 label 第一个词("Pro +" → "pro")。 // none = None 档(无 key / 无效 key),低于 free,仅历史日K无实时行情。 export const TIER_RANK: Record = { none: -1, free: 0, starter: 1, pro: 2, expert: 3 } diff --git a/frontend/src/lib/watchlist-columns.ts b/frontend/src/lib/watchlist-columns.ts index 5e01ebf..0abd978 100644 --- a/frontend/src/lib/watchlist-columns.ts +++ b/frontend/src/lib/watchlist-columns.ts @@ -72,7 +72,7 @@ export const BUILTIN_COLUMNS: ColumnConfig[] = [ { id: 'builtin:signals', source: { type: 'builtin', key: 'signals' }, label: '信号', visible: true, align: 'center' }, { id: 'builtin:candle', source: { type: 'builtin', key: 'candle' }, label: '日k', visible: false, align: 'center' }, { id: 'builtin:intraday', source: { type: 'builtin', key: 'intraday' }, label: '分时', visible: false, align: 'center' }, - // 财务指标 (需 Expert 套餐 financial capability, 列默认隐藏) + // 财务指标 (需财务数据能力 financial, 列默认隐藏) { id: 'builtin:eps', source: { type: 'builtin', key: 'eps' }, label: 'EPS', visible: false, align: 'center' }, { id: 'builtin:bps', source: { type: 'builtin', key: 'bps' }, label: 'BPS', visible: false, align: 'center' }, { id: 'builtin:roe', source: { type: 'builtin', key: 'roe' }, label: 'ROE', visible: false, align: 'center' }, diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 5f5a7a7..8cbd3ad 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -668,7 +668,7 @@ export function Dashboard() { const currentDate = selectedDate ?? data.as_of ?? '' const quoteRunning = (!selectedDate || selectedDate === latestDate) && data.quote_status?.running // 实时模式: none / watchlist / full_market。 - // watchlist (Free 档) 仅自选 ≤5 只实时, 看板呈现的大盘数据实为盘后快照, 需提示避免误读。 + // watchlist 模式仅自选 ≤5 只实时, 看板呈现的大盘数据实为盘后快照, 需提示避免误读。 const quoteMode = data.quote_status?.mode as ('none' | 'watchlist' | 'full_market') | undefined return ( @@ -740,14 +740,14 @@ export function Dashboard() { - {/* Free 档提示: 大盘看板为盘后数据, 仅自选股实时。避免用户误读为全市场实时。 */} + {/* 自选实时模式提示: 大盘看板为盘后数据, 仅自选股实时。避免用户误读为全市场实时。 */} {quoteMode === 'watchlist' && (
当前为「自选实时」模式,看板展示的大盘数据为盘后快照(最新有数据日),并非盘中实时; 仅自选股({data.quote_status?.watchlist_symbol_count ?? 0} 只)支持实时监控。 - 全市场实时需 Starter+ + 全市场实时依赖数据源支持
)} diff --git a/frontend/src/pages/Data.tsx b/frontend/src/pages/Data.tsx index 7be680d..d8a0c51 100644 --- a/frontend/src/pages/Data.tsx +++ b/frontend/src/pages/Data.tsx @@ -29,6 +29,7 @@ import { useDataStatus, } from '@/lib/useSharedQueries' import { useToggleRealtimeQuotes, useUpdateQuoteInterval } from '@/lib/useSharedMutations' +import { MissingCapChip } from '@/lib/capability-labels' import { QK } from '@/lib/queryKeys' import { PageHeader } from '@/components/PageHeader' import { formatScheduleDatePart, formatScheduleTimePart, isToday } from '@/lib/format' @@ -400,7 +401,6 @@ export function Data() { stagePct={activeCard === 'instruments' ? (job.data?.stage_pct ?? 0) : 0} tierKey="instruments" capLimits={caps.data?.capabilities} - tierLabel={caps.data?.label} auto onShowFields={() => setSchemaTable('instruments')} /> @@ -418,7 +418,6 @@ export function Data() { stagePct={activeCard === 'daily' ? (job.data?.stage_pct ?? 0) : 0} tierKey="daily" capLimits={caps.data?.capabilities} - tierLabel={caps.data?.label} customProvider={getCustomProviderName('daily')} auto onShowFields={() => setSchemaTable('daily')} @@ -439,7 +438,6 @@ export function Data() { stagePct={activeCard === 'adj_factor' ? (job.data?.stage_pct ?? 0) : 0} tierKey="adj_factor" capLimits={caps.data?.capabilities} - tierLabel={caps.data?.label} customProvider={getCustomProviderName('adj_factor')} auto onShowFields={() => setSchemaTable('adj_factor')} @@ -458,7 +456,6 @@ export function Data() { stagePct={activeCard === 'enriched' ? (job.data?.stage_pct ?? 0) : 0} tierKey="enriched" capLimits={caps.data?.capabilities} - tierLabel={caps.data?.label} auto subLabel={status.data?.indicators_ready === false ? '字段 · 指标计算中…' : '字段 · 指标 · 信号'} localBadgeSuffix={`${prefs.data?.enriched_batch_size ?? 1000}只/批`} @@ -480,7 +477,6 @@ export function Data() { stagePct={activeCard === 'index_daily' ? (job.data?.stage_pct ?? 0) : 0} tierKey="daily" capLimits={caps.data?.capabilities} - tierLabel={caps.data?.label} auto={indexAuto} subLabel={indexOverviewLabel} fieldTabs={[ @@ -502,7 +498,6 @@ export function Data() { loading={isLoading} tierKey="etf" capLimits={caps.data?.capabilities} - tierLabel={caps.data?.label} customProvider={getCustomProviderName('etf')} auto={etfAuto} subLabel="维表 · 日K · 指标" @@ -527,7 +522,6 @@ export function Data() { stagePct={activeCard === 'minute' ? (job.data?.stage_pct ?? 0) : 0} tierKey="minute" capLimits={caps.data?.capabilities} - tierLabel={caps.data?.label} customProvider={getCustomProviderName('minute')} auto={minuteAuto} onShowFields={() => setSchemaTable('minute')} @@ -545,7 +539,6 @@ export function Data() { loading={isLoading} tierKey="financials" capLimits={caps.data?.capabilities} - tierLabel={caps.data?.label} customProvider={getCustomProviderName('financials')} subLabel={`历史股本 · ${historicalShareRows.toLocaleString()} 条`} onSettings={hasData ? () => setOpenSettings(v => v === 'financials' ? null : 'financials') : undefined} @@ -566,7 +559,6 @@ export function Data() { stagePct={activeCard === 'regime' ? (job.data?.stage_pct ?? 0) : 0} tierKey="regime" capLimits={caps.data?.capabilities} - tierLabel={caps.data?.label} auto={prefs.data?.pipeline_regime_enabled === true} subLabel="状态 · 综合分 · 指标" onSettings={hasData ? () => setOpenSettings(v => v === 'regime' ? null : 'regime') : undefined} @@ -662,17 +654,17 @@ export function Data() { />
- {/* None 档提示 —— 非阻断: 无需 Key 也可获取历史日K, 仅实时行情等扩展能力受限 */} + {/* 无 Key 提示 —— 非阻断: 历史日K走免费通道, 实时等能力取决于所选数据源 */} {isNoKey && (
- 当前为 None 档,将使用免费数据源获取历史日K(无需注册)。 - 配置 API Key 可解锁实时行情监控等扩展能力,前往 + 当前无需 API Key,历史日K将使用免费通道获取。 + 实时行情、分钟K等能力取决于所选数据源,可在 - 配置 + 数据源设置 - 。 + 中配置。
)} @@ -1144,9 +1136,7 @@ export function Data() { )} {!hasDailyBatchCap && ( - - 需 Starter+ / Pro 批量日 K 权限 - + )}
diff --git a/frontend/src/pages/Financials.tsx b/frontend/src/pages/Financials.tsx index e298bb5..1c46508 100644 --- a/frontend/src/pages/Financials.tsx +++ b/frontend/src/pages/Financials.tsx @@ -62,15 +62,15 @@ export function Financials() { if (!hasFinancial) { return ( <> - +
-

需要 Expert 套餐

+

财务数据不可用

- 财务数据接口仅 Expert 套餐可用。升级后此页自动显示财务数据面板。 + 当前数据源未提供财务数据。配置提供财务数据的数据源后,此页自动显示财务数据面板。

{/* 当前财务数据源(TickFlow)需付费,后续将接入免费数据源;期间欢迎在 issues 推荐免费源 */}
@@ -159,7 +159,7 @@ export function Financials() { <> diff --git a/frontend/src/pages/Indices.tsx b/frontend/src/pages/Indices.tsx index 4496834..ae51cd3 100644 --- a/frontend/src/pages/Indices.tsx +++ b/frontend/src/pages/Indices.tsx @@ -75,7 +75,7 @@ export function Indices() { const [selectedDate, setSelectedDate] = useState(null) const [linkedPrice, setLinkedPrice] = useState(null) - // 分时数据需 Pro+ (kline.minute.batch) 能力 + // 分时数据依赖分钟K批量数据 (kline.minute.batch) const caps = useCapabilities() const hasMinuteCap = !!caps.data?.capabilities?.['kline.minute.batch'] @@ -315,8 +315,8 @@ export function Indices() { {!hasMinuteCap ? (
-
分时数据权限需 Pro+
-
升级套餐后可查看指数分时走势
+
指数分时数据不可用
+
分钟K(批量)数据不可用
) : ( <> diff --git a/frontend/src/pages/LimitUpLadder.tsx b/frontend/src/pages/LimitUpLadder.tsx index b8b17d9..d267741 100644 --- a/frontend/src/pages/LimitUpLadder.tsx +++ b/frontend/src/pages/LimitUpLadder.tsx @@ -259,7 +259,7 @@ const StockCard = React.memo(function StockCard({ stock, extFields, direction, s const hasTags = conceptTags.length > 0 || industryTags.length > 0 // 齿轮始终可见: 让免费用户也能看到功能入口, 点开后在菜单内提示权限不足。 - // Pro+ 用户正常设置; 免费用户保存按钮禁用 + 显示升级提示。 + // 有五档盘口能力的用户正常设置; 无能力时保存按钮禁用 + 显示能力提示。 return (
{/* 监控设置按钮 (右上角): 不能嵌在卡片 button 内 */} @@ -615,10 +615,10 @@ function MonitorMenu({ stock, direction, sealMode, monitorRule, anchorRect, hasD
diff --git a/frontend/src/pages/Screener.tsx b/frontend/src/pages/Screener.tsx index 0b93868..bc2910e 100644 --- a/frontend/src/pages/Screener.tsx +++ b/frontend/src/pages/Screener.tsx @@ -7,7 +7,6 @@ import { DEFAULT_STRATEGY_NOTIFY_EVENTS } from '@/lib/strategyMonitorEvents' import { toast } from '@/components/Toast' import { useDataStatus, usePreferences, useCapabilities, useQuoteStatus } from '@/lib/useSharedQueries' import { useWatchlistBatchAdd } from '@/lib/useSharedMutations' -import { isExpertOrAbove } from '@/lib/capability-labels' import { QK } from '@/lib/queryKeys' import { storage } from '@/lib/storage' import { PageHeader } from '@/components/PageHeader' @@ -399,13 +398,13 @@ export function Screener() { columns.find(c => c.source.type === 'builtin' && c.source.key === 'intraday' && c.visible), [columns], ) - // 分时图需 Pro+ (kline.minute.batch), 低档用户开了列也不拉数据 + // 分时图依赖分钟K批量数据 (kline.minute.batch), 无数据时开了列也不拉 const caps = useCapabilities() const hasMinuteBatch = !!caps.data?.capabilities?.['kline.minute.batch'] const intradayVisible = !!intradayColumn && hasMinuteBatch && intradayChartVisible // 分时数据加载策略 (与自选页一致, 简洁优先): - // - 全量加载当前列表 symbol, 但按套餐 batch 上限截断 (Pro=100 / Expert=200), + // - 全量加载当前列表 symbol, 但按数据源 batch 上限截断, // 超出时只取前 batch 只并提示用户, 避免一次性发太多请求打爆 rpm 配额 // - 刷新: minute_intraday_refresh 偏好开启时按用户设定间隔轮询; 否则仅首次加载, // 用户可点表头刷新按钮手动更新 @@ -420,9 +419,7 @@ export function Screener() { [displayRows], ) const intradayTruncated = intradayVisible && allIntradaySymbols.length > minuteBatchCap - // 是否已是最高档 (Expert+): 最高档时截断提示不再建议"升级套餐" - const isMaxTier = isExpertOrAbove(caps.data?.label ?? '') - // 截断到 batch 上限 (Pro=100 / Expert=200), 一次请求 = 一次 TickFlow 调用 + // 截断到 batch 上限, 一次请求 = 一次数据源调用 const intradaySymbols = useMemo( () => intradayTruncated ? allIntradaySymbols.slice(0, minuteBatchCap) : allIntradaySymbols, [allIntradaySymbols, intradayTruncated, minuteBatchCap], @@ -866,11 +863,10 @@ export function Screener() { {result.elapsed_ms.toFixed(1)} ms
)} - {/* 分时截断提示: 超套餐上限时在工具栏内联显示, 可关闭 */} + {/* 分时截断提示: 超数据源批量上限时在工具栏内联显示, 可关闭 */} {intradayTruncated && !intradayCapDismissed && ( - 分时仅前 {minuteBatchCap}/{allIntradaySymbols.length} - {!isMaxTier && ', 可升级'} + 分时仅前 {minuteBatchCap}/{allIntradaySymbols.length} · 受数据源批量上限限制 分钟成交 {!hasMinuteBatch && ( - Pro+ + 分钟K )}
diff --git a/frontend/src/pages/settings/Monitoring.tsx b/frontend/src/pages/settings/Monitoring.tsx index f42edfb..94607b1 100644 --- a/frontend/src/pages/settings/Monitoring.tsx +++ b/frontend/src/pages/settings/Monitoring.tsx @@ -19,7 +19,6 @@ import { import { useUpdateQuoteInterval, useToggleRealtimeQuotes } from '@/lib/useSharedMutations' import { api } from '@/lib/api' import { QK } from '@/lib/queryKeys' -import { tierRank } from '@/lib/capability-labels' import { toast } from '@/components/Toast' import { DepthConfigContent } from '@/components/data/DepthConfigCard' @@ -47,12 +46,9 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } = const { data: intervalData } = useQuoteInterval() const updateInterval = useUpdateQuoteInterval() const toggleQuote = useToggleRealtimeQuotes() - const tier = tierRank(caps?.label ?? '') - const isNoneTier = tier < 0 - // None 档但配了自定义实时源时, 后端 is_realtime_allowed 仍返回 True (realtime_mode=full_market) - // 此时不应拦截实时监控页 — 用 quoteStatus.realtime_allowed 作为最终判据 - const realtimeAllowed = quoteStatus?.realtime_allowed ?? !isNoneTier - const isFreeTier = tier === 0 + // 实时模式以 quote_status 为准 (数据源无关): watchlist=自选实时 / full_market=全市场 / none=不可用 + const quoteMode = quoteStatus?.mode ?? 'none' + const isWatchlistMode = quoteMode === 'watchlist' const realtimeEnabled = prefs?.realtime_quotes_enabled ?? false // 分时图实时刷新间隔 (秒), 与后端 [3,60] clamp 对齐; 默认 6 const intradayInterval = prefs?.minute_intraday_refresh_interval ?? 6 @@ -111,7 +107,7 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } = const watchlist = useQuery({ queryKey: QK.watchlist, queryFn: () => api.watchlistList(), - enabled: isFreeTier && watchlistSymbols.length > 0, + enabled: isWatchlistMode && watchlistSymbols.length > 0, }) const watchlistNameBySymbol = new Map( (watchlist.data?.symbols ?? []).map(row => [row.symbol, row.name] as const), @@ -281,29 +277,6 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } = } }, [highlight]) - if (isNoneTier && !realtimeAllowed) { - return ( -
-
- -
-

实时监控

-

- 实时行情需要 Free 及以上档位。None 档可使用 free-api 获取历史日K(当日数据需盘后1-2小时),但不能调用付费服务器实时接口。 -

- - 配置 API Key 升级 - -
- ) - } - return (
{/* ========== 左列 ========== */} @@ -328,7 +301,7 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } =
轮询间隔
- {isFreeTier ? '每轮拉取自选股实时行情的时间间隔' : '每轮拉取全市场行情的时间间隔'} + {isWatchlistMode ? '每轮拉取自选股实时行情的时间间隔' : '每轮拉取全市场行情的时间间隔'}
@@ -352,10 +325,10 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } =
- {isFreeTier && ( + {isWatchlistMode && (
- Free 档开启实时行情时自动监控「自选」页面前 5 个标的,最低 6 秒刷新。 + 自选实时模式下自动监控「自选」页面前 5 个标的,最低 6 秒刷新。
{watchlistSymbols.length > 0 ? (
@@ -374,7 +347,7 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } =
) : (
- 自选列表为空,Free 实时行情开启前请先添加自选股。 + 自选列表为空,开启自选实时前请先添加自选股。
)}
@@ -388,7 +361,7 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } =
)} - {!isFreeTier && ( + {!isWatchlistMode && (

选择哪些页面跟随 SSE 实时刷新数据。关闭的页面不会被推送, @@ -412,7 +385,7 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } = save({ minute_intraday_refresh: v })} /> @@ -445,7 +418,7 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } = - {!isFreeTier && ( + {!isWatchlistMode && (

选择实时行情开启时,左侧菜单底部显示哪些指数点位和涨跌幅。 @@ -483,7 +456,7 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } = runFix.mutate()}