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()}