feat(ui): 数据源能力卡片显示 TickFlow 订阅档位要求与全档位标识

每张能力卡右上角显示该能力所需最低档位(全档位/Starter+/Pro+/Expert+,
配色与左侧菜单档位体系一致, 当前档位不足时琥珀描边提示), 替代原
"当前提供方"徽标(信息已由下方高亮标签承载)。TickFlow 详情头部与
列表卡片在"第三方"后增加 Expert 三色渐变的"已适配全档位"标识。
This commit is contained in:
shy3130
2026-08-24 17:14:48 +08:00
parent 971b4376d1
commit b14ffda1bd
+60 -18
View File
@@ -4,7 +4,8 @@ import { motion, AnimatePresence } from 'framer-motion'
import { Check, Database, Eye, EyeOff, KeyRound, Plus, RefreshCw, Zap, FileWarning, Puzzle, AlertCircle, CheckCircle2, Loader2, Save, Trash2 } from 'lucide-react'
import { api, type DataSourceItem, type PluginDataSourceItem } from '@/lib/api'
import { QK } from '@/lib/queryKeys'
import { usePreferences } from '@/lib/useSharedQueries'
import { useCapabilities, usePreferences } from '@/lib/useSharedQueries'
import { TIER_RANK, tierRank, tierStyle } from '@/lib/capability-labels'
import { toast } from '@/components/Toast'
import { DataSourceEditor } from './DataSourceEditor'
import { TickFlowKeyConfig } from './Keys'
@@ -37,6 +38,50 @@ const DATASET_ROUTE: Record<string, {
realtime: { field: 'realtime_data_provider', def: 'tickflow' },
}
/** 各能力在 TickFlow 需要的最低订阅档位 (对照 tiers.yaml: 日K 全档位可用,
* 全市场实时/除权因子需 Starter+, 分钟K需 Pro+, 财务需 Expert+) */
const TICKFLOW_TIER_REQ: Record<string, string> = {
daily: 'none',
adj_factor: 'starter',
realtime: 'starter',
minute: 'pro',
financial: 'expert',
}
/** TickFlow 档位要求徽标: 按所需档位配色(与左侧菜单/Key 页一致), 当前档位不足时琥珀描边提示 */
function TierReqChip({ tier, currentLabel }: { tier: string; currentLabel?: string }) {
const text = tier === 'none' ? '全档位' : `${tier}+`
const req = TIER_RANK[tier] ?? -1
const unmet = currentLabel != null && tierRank(currentLabel) < req
const t = tierStyle(tier)
return (
<span
title={unmet
? `TickFlow 该能力需 ${text} — 当前档位 ${currentLabel}`
: `TickFlow 该能力需 ${text}`}
className={`inline-flex h-[15px] shrink-0 items-center gap-1 rounded px-1.5 text-[9px] font-bold font-mono leading-none ${unmet ? 'ring-1 ring-warning/60' : ''}`}
style={t.tagBg}
>
<span className="h-1 w-1 rounded-full shrink-0" style={t.dotStyle} />
<span className="capitalize" style={t.labelTextStyle}>{text}</span>
</span>
)
}
/** TickFlow「已适配全档位」标识: Expert 三色渐变(全档位体系里最醒目的身份色) */
function AllTiersBadge({ size = 'text-[10px]' }: { size?: string }) {
const t = tierStyle('expert')
return (
<span
className="inline-flex shrink-0 items-center rounded px-1.5 py-0.5 font-medium leading-none"
style={t.tagBg}
title="无 Key 到 Expert 各订阅档位均有可用能力 — 日K全档位可用, 除权/分钟/财务等高级能力按订阅档位解锁"
>
<span className={size} style={t.labelTextStyle}> </span>
</span>
)
}
/** 卡片内静态数据集标签: 只展示该源适配了哪些数据集 (路由选择在下方能力卡片) */
function DatasetChipRow({ datasets }: { datasets: string[] }) {
if (datasets.length === 0) return null
@@ -55,7 +100,7 @@ function DatasetChipRow({ datasets }: { datasets: string[] }) {
/** 按能力路由网格: 每个数据源详情下展示一张能力卡,卡片上以可点标签列出
* 所有具备该能力的数据源 — 点谁,该数据集就立刻由谁提供,可跨源自由组合。
* TickFlow 详情含全部能力; 其他源只列自己参与的能力。 */
function SourceCapabilityGrid({ sourceName, sourceDisplay, datasets, candidatesOf, providerOf, displayOf, pending, onSelect, anyCustom, onReset }: {
function SourceCapabilityGrid({ sourceName, sourceDisplay, datasets, candidatesOf, providerOf, pending, onSelect, anyCustom, onReset }: {
sourceName: string
sourceDisplay: string
datasets: string[]
@@ -63,7 +108,6 @@ function SourceCapabilityGrid({ sourceName, sourceDisplay, datasets, candidatesO
candidatesOf: (dataset: string) => { name: string; display: string }[]
/** 该数据集当前的原始路由偏好值 (adj_factor 可能是 same_as_daily) */
providerOf: (dataset: string) => string
displayOf: (name?: string) => string
pending?: boolean
onSelect: (dataset: string, provider: string) => void
anyCustom?: boolean
@@ -71,6 +115,7 @@ function SourceCapabilityGrid({ sourceName, sourceDisplay, datasets, candidatesO
}) {
const isDefault = sourceName === 'tickflow'
const dsList = isDefault ? [...datasets, 'financial'] : datasets
const caps = useCapabilities()
if (dsList.length === 0) return null
return (
@@ -103,10 +148,13 @@ function SourceCapabilityGrid({ sourceName, sourceDisplay, datasets, candidatesO
<div key={ds} className="rounded-lg border border-border/50 bg-elevated/20 px-3 py-2.5" title="该数据集暂不支持切换数据源">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<div className="text-xs font-medium text-foreground">{label}</div>
<div className="text-xs font-medium text-foreground truncate">{label}</div>
{desc && <div className="text-[10px] text-muted mt-0.5">{desc}</div>}
</div>
<span className="shrink-0 px-1.5 py-0.5 rounded text-[10px] text-muted/50 bg-elevated/60"></span>
<div className="flex items-center gap-1 shrink-0">
{isDefault && <TierReqChip tier={TICKFLOW_TIER_REQ[ds]} currentLabel={caps.data?.label} />}
<span className="shrink-0 px-1.5 py-0.5 rounded text-[10px] text-muted/50 bg-elevated/60"></span>
</div>
</div>
</div>
)
@@ -120,15 +168,8 @@ function SourceCapabilityGrid({ sourceName, sourceDisplay, datasets, candidatesO
<div className="text-xs font-medium text-foreground">{label}</div>
{desc && <div className="text-[10px] text-muted mt-0.5">{desc}</div>}
</div>
{/* 右上角: 当前提供方 */}
<span
title={`${label}」当前由 ${displayOf(raw)} 提供`}
className={`shrink-0 px-1.5 py-0.5 rounded text-[10px] ${
raw === route.def ? 'text-muted/50 bg-elevated/60' : 'bg-accent/15 text-accent font-medium'
}`}
>
<span className="truncate max-w-[80px] inline-block align-middle">{displayOf(raw)}</span>
</span>
{/* 右上角: TickFlow 所需档位 (当前提供方由下方高亮标签指示) */}
{isDefault && <TierReqChip tier={TICKFLOW_TIER_REQ[ds]} currentLabel={caps.data?.label} />}
</div>
{/* 提供方标签: 点谁该数据集就由谁提供,当前项高亮 */}
<div className="flex flex-wrap gap-1 mt-2 pt-2 border-t border-border/50">
@@ -626,7 +667,10 @@ export function SettingsDataSourcesPanel() {
</span>
)}
{item.name === 'tickflow' && (
<span className="shrink-0 rounded bg-warning/15 px-1 py-0.5 text-[9px] font-medium leading-none text-warning"></span>
<>
<span className="shrink-0 rounded bg-warning/15 px-1 py-0.5 text-[9px] font-medium leading-none text-warning"></span>
<AllTiersBadge size="text-[9px]" />
</>
)}
{pluginNames.has(item.name) && (
<span className="shrink-0 rounded bg-warning/15 px-1 py-0.5 text-[9px] font-medium leading-none text-warning"></span>
@@ -785,7 +829,6 @@ export function SettingsDataSourcesPanel() {
datasets={selectedCustom?.datasets || []}
candidatesOf={routeCtx.candidatesOf}
providerOf={routeCtx.providerOf}
displayOf={routeCtx.displayOf}
pending={routeCtx.pending}
onSelect={routeCtx.onSelect}
/>
@@ -887,7 +930,6 @@ function PluginDetail({ plugin, isActive, onSwitch, switching, route }: {
datasets={plugin.datasets}
candidatesOf={route.candidatesOf}
providerOf={route.providerOf}
displayOf={route.displayOf}
pending={route.pending}
onSelect={route.onSelect}
/>
@@ -935,6 +977,7 @@ function TickFlowDetail({ active, onSwitch, switching, route }: {
<div className="flex items-center gap-2 flex-wrap">
<h2 className="text-base font-semibold text-foreground">TickFlow</h2>
<span className="rounded bg-warning/15 px-1.5 py-0.5 text-[10px] font-medium text-warning"></span>
<AllTiersBadge />
{active && (
<span className="inline-flex items-center gap-1 text-[10px] text-accent bg-accent/10 px-2 py-1 rounded">
<Check className="h-2.5 w-2.5" />
@@ -954,7 +997,6 @@ function TickFlowDetail({ active, onSwitch, switching, route }: {
datasets={['daily', 'adj_factor', 'realtime', 'minute']}
candidatesOf={route.candidatesOf}
providerOf={route.providerOf}
displayOf={route.displayOf}
pending={route.pending}
onSelect={route.onSelect}
anyCustom={route.anyCustom}