mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 16:44:15 +08:00
feat(v0.2): 功能门槛统一为数据源通用的能力标准
- 后端: 数据集→能力映射增广 (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 档位表加注解说明
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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 档位名。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="border-t border-border px-3 py-2.5 shrink-0">
|
||||
{isNoneTier && !realtimeProviderName ? (
|
||||
{realtimeUnavailable && !realtimeProviderName ? (
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-secondary truncate">实时行情</span>
|
||||
@@ -760,7 +762,7 @@ export function Layout() {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* Starter+ — 开关 + 跳转设置 */
|
||||
/* 实时可用 — 开关 + 跳转设置 */
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<span className={`inline-block h-2 w-2 shrink-0 rounded-full ${realtimeIndicatorClass}`} />
|
||||
@@ -810,13 +812,13 @@ export function Layout() {
|
||||
|
||||
{/* 状态提示 */}
|
||||
{realtimeEnabled
|
||||
&& (!isNoneTier || realtimeProviderName)
|
||||
&& (!realtimeUnavailable || realtimeProviderName)
|
||||
&& (isPaused || (isWatchlistMode && !dismissFreeHint && !realtimeProviderName))
|
||||
&& (
|
||||
<div className="mt-1.5 text-[10px] leading-snug space-y-0.5">
|
||||
{isWatchlistMode && !dismissFreeHint && !realtimeProviderName && (
|
||||
<div className="flex items-start gap-1 text-amber-400/80">
|
||||
<span className="flex-1">监控自选股前 5 只,全市场监控需 Starter+</span>
|
||||
<span className="flex-1">自选实时模式监控前 5 只,全市场实时依赖数据源支持</span>
|
||||
<button
|
||||
onClick={() => setDismissFreeHint(true)}
|
||||
className="text-amber-400/50 hover:text-amber-400 shrink-0 transition-colors"
|
||||
@@ -831,7 +833,7 @@ export function Layout() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{showSidebarQuotes && !isWatchlistMode && (!isNoneTier || !!realtimeProviderName) && (
|
||||
{showSidebarQuotes && !isWatchlistMode && (!realtimeUnavailable || !!realtimeProviderName) && (
|
||||
<SidebarIndexQuotes rows={sidebarIndexQuotes?.rows} items={sidebarIndexes} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
</div>
|
||||
))}
|
||||
<div className="mt-1.5 pt-1.5 border-t border-border text-muted">
|
||||
真假板判定依赖五档盘口实时快照(卖一/买一量)。Pro+ 套餐的当天数据在收盘后自动恢复。
|
||||
真假板判定依赖五档盘口实时快照(卖一/买一量)。配置提供五档盘口的数据源后,当天数据在收盘后自动恢复。
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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 (
|
||||
<p className="text-xs text-muted leading-relaxed">
|
||||
真假涨停判定依赖五档盘口实时快照,需 <span className="text-accent">Pro 及以上套餐</span>。
|
||||
升级后连板梯队将自动区分真封板(显示封单量)与假涨停(归入炸板)。
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-muted leading-relaxed">
|
||||
真假涨停判定依赖五档盘口实时快照,该数据当前不可用。
|
||||
配置提供五档盘口的数据源后,连板梯队将自动区分真封板(显示封单量)与假涨停(归入炸板)。
|
||||
</p>
|
||||
<MissingCapChip capKey="depth5.batch" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string, { rpm: number | null; batch: number | null; subscribe: number | null }> } | undefined
|
||||
@@ -89,9 +90,7 @@ export function ExtendHistoryPanel({ caps, isRunning, earliestDate, onStart }: {
|
||||
</button>
|
||||
|
||||
{!hasBatchCap && (
|
||||
<span className="text-[10px] text-warning/80 bg-warning/8 rounded px-1.5 py-px font-medium">
|
||||
需 Pro+ 权限
|
||||
</span>
|
||||
<MissingCapChip capKey="kline.daily.batch" />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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<string, { rpm: number | null; batch: number | null; subscribe: number | null }> } | undefined; onJobStart?: (jobId: string) => void }) {
|
||||
const qc = useQueryClient()
|
||||
@@ -110,7 +111,7 @@ export function MinuteSyncConfig({ caps, onJobStart }: { caps: { label: string;
|
||||
</div>
|
||||
<span className="text-[10px] text-muted">天</span>
|
||||
{!hasMinuteCap && (
|
||||
<span className="text-[10px] text-warning/80 bg-warning/8 rounded px-1.5 py-px font-medium">需 Pro+</span>
|
||||
<MissingCapChip capKey="kline.minute.batch" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 },
|
||||
]
|
||||
|
||||
|
||||
@@ -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 }: {
|
||||
</button>
|
||||
|
||||
{!hasBatchCap && (
|
||||
<span className="block text-[10px] text-warning/80 bg-warning/8 rounded px-1.5 py-px font-medium text-center">
|
||||
需 Pro+ 权限
|
||||
<span className="block text-center">
|
||||
<MissingCapChip capKey="kline.daily.batch" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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<string, {
|
||||
capKey: string // 对应的 capability key,空串表示本地计算 / free 服务器可用
|
||||
tierReq: string // 最低档位要求(无权限时显示)
|
||||
capKey: string // 对应的 capability key,空串表示本地计算 / 免费服务器可用
|
||||
missingCapName: string // 缺能力时提示的能力名(空串表示缺能力也不显示徽章)
|
||||
}> = {
|
||||
// 标的维表走 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 (
|
||||
<span className="text-[10px] text-accent/80 bg-accent/8 rounded px-1.5 py-px font-medium">
|
||||
@@ -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 (
|
||||
<span className="text-[10px] text-accent/80 bg-accent/8 rounded px-1.5 py-px font-mono font-medium">
|
||||
@@ -67,20 +67,15 @@ function CapBadge({ hasCap, isLocal, tierLabel, tierReq, capInfo, localSuffix, c
|
||||
)
|
||||
}
|
||||
|
||||
if (!hasCap && tierReq && tierReq !== 'Free') {
|
||||
// 缺权限且非 Free 档(付费档位才提示升级);Free 档人人可用,
|
||||
// 若显示"需 Free"会造成 Expert 等用户困惑(通常是探测瞬时失败丢能力)
|
||||
return (
|
||||
<span className="text-[10px] text-warning/90 bg-warning/8 rounded px-1.5 py-px font-medium">
|
||||
需 {tierReq}
|
||||
</span>
|
||||
)
|
||||
if (!hasCap && missingCapName) {
|
||||
// 能力标准对所有数据源一致: 缺能力提示能力名而非档位, 点击跳数据源设置
|
||||
return <MissingCapChip label={missingCapName} />
|
||||
}
|
||||
|
||||
if (hasCap) {
|
||||
return (
|
||||
<span className="text-[10px] text-accent/80 bg-accent/8 rounded px-1.5 py-px font-medium">
|
||||
{tierLabel ?? '已授权'}
|
||||
可用
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -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<string, { rpm: number | null; batch: number | null; subscribe: number | null }>
|
||||
tierLabel?: string
|
||||
customProvider?: string | null
|
||||
onSettings?: () => void
|
||||
onShowFields?: (table?: string) => void
|
||||
@@ -248,8 +242,7 @@ export function StatCard({
|
||||
<CapBadge
|
||||
hasCap={hasCap}
|
||||
isLocal={isLocal}
|
||||
tierLabel={tierLabel}
|
||||
tierReq={meta?.tierReq}
|
||||
missingCapName={meta?.missingCapName}
|
||||
capInfo={capInfo}
|
||||
localSuffix={localBadgeSuffix}
|
||||
customProvider={customProvider}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// capability 内部名 → 用户能理解的中文标签
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
export const CAP_LABELS: Record<string, { name: string; hint: string }> = {
|
||||
'quote.by_symbol': { name: '自选股实时监控', hint: 'Free 可按标的查询实时行情,用于少量自选股监控' },
|
||||
'quote.batch': { name: '实时行情(批量)', hint: '一次拿多只股票的价' },
|
||||
@@ -9,12 +11,55 @@ export const CAP_LABELS: Record<string, { name: string; hint: string }> = {
|
||||
'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 (
|
||||
<span className={`text-[10px] text-warning/90 bg-warning/8 rounded px-1.5 py-px font-medium ${className}`} title="该数据当前不可用">
|
||||
{content}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.stopPropagation(); navigate(to) }}
|
||||
className={`text-[10px] text-warning/90 bg-warning/8 rounded px-1.5 py-px font-medium hover:bg-warning/15 transition-colors ${className}`}
|
||||
title="前往 设置 → 数据源"
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
// 套餐等级 —— 仅用于 TickFlow 专属界面 (Key 配置 / 端点测速 / 引导页 tickflow 分支)。
|
||||
// 通用功能门槛一律用能力键 (capName/needCapText/MissingCapChip), 不用档位词。
|
||||
// 基础档提取与后端 quote_service.py 一致:取 label 第一个词("Pro +" → "pro")。
|
||||
// none = None 档(无 key / 无效 key),低于 free,仅历史日K无实时行情。
|
||||
export const TIER_RANK: Record<string, number> = { none: -1, free: 0, starter: 1, pro: 2, expert: 3 }
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Free 档提示: 大盘看板为盘后数据, 仅自选股实时。避免用户误读为全市场实时。 */}
|
||||
{/* 自选实时模式提示: 大盘看板为盘后数据, 仅自选股实时。避免用户误读为全市场实时。 */}
|
||||
{quoteMode === 'watchlist' && (
|
||||
<div className="mb-1.5 flex items-start gap-2 rounded-card border border-amber-500/30 bg-amber-500/8 px-3 py-1.5 text-[11px] leading-relaxed">
|
||||
<Info className="mt-0.5 h-3.5 w-3.5 shrink-0 text-amber-500" />
|
||||
<div className="min-w-0 flex-1 text-secondary">
|
||||
当前为「自选实时」模式,看板展示的大盘数据为<strong className="text-foreground">盘后快照</strong>(最新有数据日),并非盘中实时;
|
||||
仅自选股({data.quote_status?.watchlist_symbol_count ?? 0} 只)支持实时监控。
|
||||
<span className="ml-1 text-accent">全市场实时需 Starter+</span>
|
||||
<span className="ml-1 text-accent">全市场实时依赖数据源支持</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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() {
|
||||
/>
|
||||
|
||||
<div className="px-8 py-6 space-y-6 max-w-6xl">
|
||||
{/* None 档提示 —— 非阻断: 无需 Key 也可获取历史日K, 仅实时行情等扩展能力受限 */}
|
||||
{/* 无 Key 提示 —— 非阻断: 历史日K走免费通道, 实时等能力取决于所选数据源 */}
|
||||
{isNoKey && (
|
||||
<div className="flex items-center gap-2 rounded-card border border-border bg-elevated/40 px-3 py-2 text-xs">
|
||||
<Info className="h-4 w-4 shrink-0 text-muted" />
|
||||
<span className="text-secondary leading-relaxed">
|
||||
当前为 None 档,将使用免费数据源获取历史日K(无需注册)。
|
||||
配置 API Key 可解锁实时行情监控等扩展能力,前往
|
||||
当前无需 API Key,历史日K将使用免费通道获取。
|
||||
实时行情、分钟K等能力取决于所选数据源,可在
|
||||
<Link to="/settings?tab=data-sources" className="mx-0.5 font-medium text-accent hover:underline">
|
||||
配置
|
||||
数据源设置
|
||||
</Link>
|
||||
。
|
||||
中配置。
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -1144,9 +1136,7 @@ export function Data() {
|
||||
)}
|
||||
</button>
|
||||
{!hasDailyBatchCap && (
|
||||
<span className="text-[10px] text-warning/80 bg-warning/8 rounded px-1.5 py-px font-medium">
|
||||
需 Starter+ / Pro 批量日 K 权限
|
||||
</span>
|
||||
<MissingCapChip capKey="kline.daily.batch" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -62,15 +62,15 @@ export function Financials() {
|
||||
if (!hasFinancial) {
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="财务分析" subtitle="利润表 / 资负表 / 现金流 / 关键指标 / 股本 / AI分析 · Expert" />
|
||||
<PageHeader title="财务分析" subtitle="利润表 / 资负表 / 现金流 / 关键指标 / 股本 / AI分析" />
|
||||
<div className="px-8 py-10">
|
||||
<div className="mx-auto max-w-md rounded-card border border-warning/30 bg-warning/[0.04] p-8 text-center">
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-warning/10">
|
||||
<Lock className="h-6 w-6 text-warning" />
|
||||
</div>
|
||||
<h3 className="mt-4 text-base font-semibold text-foreground">需要 Expert 套餐</h3>
|
||||
<h3 className="mt-4 text-base font-semibold text-foreground">财务数据不可用</h3>
|
||||
<p className="mt-2 text-xs leading-relaxed text-secondary">
|
||||
财务数据接口仅 Expert 套餐可用。升级后此页自动显示财务数据面板。
|
||||
当前数据源未提供财务数据。配置提供财务数据的数据源后,此页自动显示财务数据面板。
|
||||
</p>
|
||||
{/* 当前财务数据源(TickFlow)需付费,后续将接入免费数据源;期间欢迎在 issues 推荐免费源 */}
|
||||
<div className="mt-5 rounded-btn border border-accent/25 bg-accent/[0.05] px-3.5 py-3 text-left">
|
||||
@@ -159,7 +159,7 @@ export function Financials() {
|
||||
<>
|
||||
<PageHeader
|
||||
title="财务分析"
|
||||
subtitle="利润表 / 资负表 / 现金流 / 关键指标 / 股本 / AI分析 · Expert"
|
||||
subtitle="利润表 / 资负表 / 现金流 / 关键指标 / 股本 / AI分析"
|
||||
right={
|
||||
<div className="flex items-center gap-2">
|
||||
<LastStockChip stock={lastStock} onSelect={pick} />
|
||||
|
||||
@@ -75,7 +75,7 @@ export function Indices() {
|
||||
const [selectedDate, setSelectedDate] = useState<string | null>(null)
|
||||
const [linkedPrice, setLinkedPrice] = useState<number | null>(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 ? (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-2 text-center">
|
||||
<Lock className="h-5 w-5 text-muted" />
|
||||
<div className="text-xs text-secondary">分时数据权限需 Pro+</div>
|
||||
<div className="text-[10px] text-muted">升级套餐后可查看指数分时走势</div>
|
||||
<div className="text-xs text-secondary">指数分时数据不可用</div>
|
||||
<div className="text-[10px] text-muted">分钟K(批量)数据不可用</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -259,7 +259,7 @@ const StockCard = React.memo(function StockCard({ stock, extFields, direction, s
|
||||
const hasTags = conceptTags.length > 0 || industryTags.length > 0
|
||||
|
||||
// 齿轮始终可见: 让免费用户也能看到功能入口, 点开后在菜单内提示权限不足。
|
||||
// Pro+ 用户正常设置; 免费用户保存按钮禁用 + 显示升级提示。
|
||||
// 有五档盘口能力的用户正常设置; 无能力时保存按钮禁用 + 显示能力提示。
|
||||
return (
|
||||
<div className="relative group w-full">
|
||||
{/* 监控设置按钮 (右上角): 不能嵌在卡片 button 内 */}
|
||||
@@ -615,10 +615,10 @@ function MonitorMenu({ stock, direction, sealMode, monitorRule, anchorRect, hasD
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving || !threshold || !hasDepth}
|
||||
title={!hasDepth ? '需 Pro+ 套餐 (批量五档能力)' : ''}
|
||||
title={!hasDepth ? '五档盘口(批量)数据不可用' : ''}
|
||||
className="flex-1 h-7 rounded text-[11px] font-medium transition-all cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed bg-accent text-white hover:bg-accent/90 active:scale-[0.98] disabled:active:scale-100"
|
||||
>
|
||||
{saving ? '保存中…' : !hasDepth ? '需 Pro+ 套餐' : existing ? '更新监控' : '开启监控'}
|
||||
{saving ? '保存中…' : !hasDepth ? '五档盘口不可用' : existing ? '更新监控' : '开启监控'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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() {
|
||||
<span className="num">{result.elapsed_ms.toFixed(1)} ms</span>
|
||||
</div>
|
||||
)}
|
||||
{/* 分时截断提示: 超套餐上限时在工具栏内联显示, 可关闭 */}
|
||||
{/* 分时截断提示: 超数据源批量上限时在工具栏内联显示, 可关闭 */}
|
||||
{intradayTruncated && !intradayCapDismissed && (
|
||||
<span className="inline-flex items-center gap-1 text-xs text-warning/90">
|
||||
分时仅前 {minuteBatchCap}/{allIntradaySymbols.length}
|
||||
{!isMaxTier && ', 可升级'}
|
||||
分时仅前 {minuteBatchCap}/{allIntradaySymbols.length} · 受数据源批量上限限制
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIntradayCapDismissed(true)}
|
||||
|
||||
@@ -412,7 +412,7 @@ function StockSearchBox({
|
||||
// 视觉: 内圈实心点 + 外圈 animate-ping 扩散晕, 语义=「在线/活动」。
|
||||
// 配色用 accent (电光蓝) 而非绿/红: 项目设计规范规定红绿仅用于价格/K线,
|
||||
// UI 状态用 accent, 避免与 A 股涨跌色混淆。
|
||||
// 全市场模式 (Starter+) 不显示 —— 全部都在监控, 标记无信息量。
|
||||
// 全市场模式不显示 —— 全部都在监控, 标记无信息量。
|
||||
function RealtimeDot({ title = '实时监控中' }: { title?: string }) {
|
||||
return (
|
||||
<span
|
||||
@@ -731,7 +731,7 @@ export function Watchlist() {
|
||||
)
|
||||
// 分时列渲染配置(宽高, 来自列定制, 已钳制边界)
|
||||
const intradayResolved = useMemo(() => resolveIntradayConfig(intradayColumn?.intradayConfig), [intradayColumn])
|
||||
// 分时图需 Pro+ (kline.minute.batch), 低档用户开了列也不拉数据
|
||||
// 分时图依赖分钟K批量数据 (kline.minute.batch), 无数据时开了列也不拉
|
||||
const caps = useCapabilities()
|
||||
const hasMinuteBatch = !!caps.data?.capabilities?.['kline.minute.batch']
|
||||
const intradayVisible = !!intradayColumn && hasMinuteBatch && intradayChartVisible
|
||||
@@ -872,7 +872,7 @@ export function Watchlist() {
|
||||
return patched
|
||||
}, [dailyKVisible, klineBatch.data, enriched.data])
|
||||
|
||||
// 批量分时数据 (Pro+ 用户, 列可见时才拉)
|
||||
// 批量分时数据 (有分钟K批量能力时, 列可见才拉)
|
||||
// 刷新策略: 仅当实时行情运行 且 用户在实时监控设置里开启 minute_intraday_refresh 时
|
||||
// 按用户设定的间隔轮询 (不接 SSE 高频, 避免每秒拉 TickFlow 触限流); 与 Screener / 设置卡片描述一致。
|
||||
const { data: prefsData } = usePreferences()
|
||||
@@ -1053,8 +1053,8 @@ export function Watchlist() {
|
||||
const watchlistContentLoading = list.isLoading || (allSymbols.length > 0 && enriched.isLoading)
|
||||
|
||||
// 实时监控圆点: 仅 Free/低档 "按自选股实时监控" 模式 (mode === 'watchlist') 下显示;
|
||||
// Starter+ 全市场模式 (mode === 'full_market') 全部标的都在监控, 标圆点无意义, 故不显示。
|
||||
// 后端 Free 档实际只监控自选页前 N 个 (N = watchlist_symbol_count), 顺序与 allSymbols 一致。
|
||||
// 全市场模式 (mode === 'full_market') 全部标的都在监控, 标圆点无意义, 故不显示。
|
||||
// 后端自选实时模式实际只监控自选页前 N 个 (N = watchlist_symbol_count), 顺序与 allSymbols 一致。
|
||||
const realtimeMode = quoteStatus.data?.mode
|
||||
const watchlistMonitoredCount = quoteStatus.data?.watchlist_symbol_count ?? 0
|
||||
const showRealtimeDot = realtimeRunning && realtimeMode === 'watchlist'
|
||||
|
||||
@@ -927,7 +927,7 @@ export function StrategyBacktest() {
|
||||
const [regimeStates, setRegimeStates] = useState<string[]>(saved?.regimeStates ?? [])
|
||||
const [regimeMinScore, setRegimeMinScore] = useState<number | ''>(saved?.regimeMinScore ?? '')
|
||||
const [settingsOpen, setSettingsOpen] = useState(false)
|
||||
// 分钟K成交价细化: 不改变信号日或成交日, 需 Pro+ 分钟K能力
|
||||
// 分钟K成交价细化: 不改变信号日或成交日, 依赖分钟K批量数据
|
||||
const { data: caps } = useCapabilities()
|
||||
const hasMinuteBatch = !!caps?.capabilities?.['kline.minute.batch']
|
||||
const toggleMinuteFill = () => {
|
||||
@@ -1402,7 +1402,7 @@ export function StrategyBacktest() {
|
||||
onClick={toggleMinuteFill}
|
||||
disabled={!hasMinuteBatch}
|
||||
title={!hasMinuteBatch
|
||||
? '分钟K成交价:需 Pro+ 权限 (分钟K批量)'
|
||||
? '分钟K成交价:分钟K(批量)数据不可用'
|
||||
: '分钟K成交:细化成交价,并为兼容的卖出信号提供下一分钟成交。'
|
||||
}
|
||||
className={`group relative inline-flex h-3.5 w-6 items-center rounded-full shrink-0 transition-colors duration-200 ${
|
||||
@@ -1417,7 +1417,7 @@ export function StrategyBacktest() {
|
||||
</button>
|
||||
<span className={`text-[9px] font-medium ${highGranularity ? 'text-amber-400' : 'text-muted/50'}`}>分钟成交</span>
|
||||
{!hasMinuteBatch && (
|
||||
<span className="text-[8px] text-accent/70 font-medium bg-accent/10 px-1 py-px rounded">Pro+</span>
|
||||
<span className="text-[8px] text-accent/70 font-medium bg-accent/10 px-1 py-px rounded">分钟K</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-center">
|
||||
<div className="inline-flex items-center justify-center w-14 h-14 rounded-2xl
|
||||
bg-gradient-to-br from-purple-500/20 to-blue-500/20 mb-5">
|
||||
<Activity className="h-7 w-7 text-purple-400" />
|
||||
</div>
|
||||
<h2 className="text-lg font-medium text-foreground mb-2">实时监控</h2>
|
||||
<p className="text-sm text-secondary max-w-md mb-6">
|
||||
实时行情需要 Free 及以上档位。None 档可使用 free-api 获取历史日K(当日数据需盘后1-2小时),但不能调用付费服务器实时接口。
|
||||
</p>
|
||||
<a
|
||||
href="/settings?tab=data-sources"
|
||||
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-btn
|
||||
bg-accent text-white text-sm font-medium
|
||||
hover:bg-accent/90 transition-colors"
|
||||
>
|
||||
配置 API Key 升级
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] gap-6 max-w-5xl">
|
||||
{/* ========== 左列 ========== */}
|
||||
@@ -328,7 +301,7 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } =
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm text-foreground">轮询间隔</div>
|
||||
<div className="text-[11px] text-muted">
|
||||
{isFreeTier ? '每轮拉取自选股实时行情的时间间隔' : '每轮拉取全市场行情的时间间隔'}
|
||||
{isWatchlistMode ? '每轮拉取自选股实时行情的时间间隔' : '每轮拉取全市场行情的时间间隔'}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-[11px] font-mono text-foreground shrink-0 tabular-nums">
|
||||
@@ -352,10 +325,10 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } =
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{isFreeTier && (
|
||||
{isWatchlistMode && (
|
||||
<Card icon={Activity} title="自选股实时">
|
||||
<div className="mb-3 rounded-btn border border-accent/25 bg-accent/10 px-3 py-2 text-xs font-medium leading-snug text-accent">
|
||||
Free 档开启实时行情时自动监控「自选」页面前 5 个标的,最低 6 秒刷新。
|
||||
自选实时模式下自动监控「自选」页面前 5 个标的,最低 6 秒刷新。
|
||||
</div>
|
||||
{watchlistSymbols.length > 0 ? (
|
||||
<div className="space-y-1.5">
|
||||
@@ -374,7 +347,7 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } =
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-btn border border-border bg-base/40 px-3 py-3 text-xs text-muted">
|
||||
自选列表为空,Free 实时行情开启前请先添加自选股。
|
||||
自选列表为空,开启自选实时前请先添加自选股。
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-2 flex items-center justify-between gap-3">
|
||||
@@ -388,7 +361,7 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } =
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
{!isFreeTier && (
|
||||
{!isWatchlistMode && (
|
||||
<Card icon={Wifi} title="页面实时刷新">
|
||||
<p className="text-xs text-secondary mb-4">
|
||||
选择哪些页面跟随 SSE 实时刷新数据。关闭的页面不会被推送,
|
||||
@@ -412,7 +385,7 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } =
|
||||
<Card icon={Activity} title="分时图刷新">
|
||||
<ToggleRow
|
||||
label="自选/策略分时图实时刷新"
|
||||
desc={`开启后自选与策略列表的分时图盘中每 ${intradayInterval} 秒自动刷新(需 Pro+ 权限 + 实时行情运行)。关闭时仅打开页面时拉取一次, 可点表头刷新按钮手动更新。`}
|
||||
desc={`开启后自选与策略列表的分时图盘中每 ${intradayInterval} 秒自动刷新(依赖分钟K批量数据 + 实时行情运行)。关闭时仅打开页面时拉取一次, 可点表头刷新按钮手动更新。`}
|
||||
checked={prefs?.minute_intraday_refresh ?? false}
|
||||
onChange={(v) => save({ minute_intraday_refresh: v })}
|
||||
/>
|
||||
@@ -445,7 +418,7 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } =
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{!isFreeTier && (
|
||||
{!isWatchlistMode && (
|
||||
<Card icon={BarChart3} title="左侧菜单指数">
|
||||
<p className="text-xs text-secondary mb-4">
|
||||
选择实时行情开启时,左侧菜单底部显示哪些指数点位和涨跌幅。
|
||||
@@ -483,7 +456,7 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } =
|
||||
<Card
|
||||
icon={Flame}
|
||||
title="连板梯队降级修正"
|
||||
badge={!hasDepth ? '需 Pro+' : undefined}
|
||||
badge={!hasDepth ? '五档盘口不可用' : undefined}
|
||||
right={hasDepth ? (
|
||||
<button
|
||||
onClick={() => runFix.mutate()}
|
||||
|
||||
Reference in New Issue
Block a user