chore: bump version to 0.1.65

This commit is contained in:
shy3130
2026-06-29 17:21:01 +08:00
parent a72445d782
commit 73490fdf5e
11 changed files with 365 additions and 196 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "tickflow-stock-panel-backend"
version = "0.1.64"
version = "0.1.65"
description = "A 股选股 + 监控 + 回测面板 — TickFlow 适配"
readme = "../README.md"
requires-python = ">=3.11"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "tickflow-stock-panel-frontend",
"private": true,
"version": "0.1.64",
"version": "0.1.65",
"type": "module",
"scripts": {
"dev": "vite",
@@ -1,5 +1,22 @@
import { useState } from 'react'
import { Check } from 'lucide-react'
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
type DragEndEvent,
} from '@dnd-kit/core'
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
useSortable,
verticalListSortingStrategy,
} from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { Check, GripVertical } from 'lucide-react'
import { storage } from '@/lib/storage'
export type CardKey =
@@ -12,20 +29,26 @@ interface CardDef {
desc: string
/** 档位能力不足时该卡片是否默认隐藏(减少干扰) */
defaultHiddenIfNoCap: boolean
/** 无条件默认隐藏(用户可在设置里手动开启) */
defaultHidden?: boolean
}
/** 数据画像卡片定义 —— 顺序即弹窗展示顺序 */
/** 数据画像卡片定义 —— 默认顺序即此数组顺序 */
export const DATA_CARD_DEFS: CardDef[] = [
{ key: 'instruments', label: '个股维表', desc: 'A 股股票元数据', defaultHiddenIfNoCap: false },
{ key: 'daily', label: '日 K', desc: 'A 股日K线数据', defaultHiddenIfNoCap: false },
{ key: 'adj_factor', label: '除权因子', desc: '复权计算因子', defaultHiddenIfNoCap: true },
{ key: 'enriched', label: 'Enriched', desc: '技术指标计算结果', defaultHiddenIfNoCap: false },
{ key: 'index', label: '指数', desc: '主要市场指数日K', defaultHiddenIfNoCap: false },
{ key: 'etf', label: 'ETF', desc: '场内交易基金日K', defaultHiddenIfNoCap: false },
{ key: 'adj_factor', label: '除权因子', desc: '复权计算因子', defaultHiddenIfNoCap: true },
{ 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 },
]
const DEFAULT_ORDER = DATA_CARD_DEFS.map(d => d.key)
/** 恢复默认时显示的卡片数量(按默认顺序取前 N 张) */
const DEFAULT_VISIBLE_COUNT = 5
const CAP_KEY_MAP: Partial<Record<CardKey, string>> = {
adj_factor: 'adj_factor',
minute: 'kline.minute.batch',
@@ -35,6 +58,7 @@ const CAP_KEY_MAP: Partial<Record<CardKey, string>> = {
/**
* 读取卡片显隐状态。结合档位能力决定默认值:
* - 用户显式设置过 → 用设置值
* - 未设置 + defaultHidden → 隐藏(无条件默认隐藏)
* - 未设置 + defaultHiddenIfNoCap + 当前无能力 → 隐藏
* - 其他 → 显示
*/
@@ -47,6 +71,8 @@ export function getCardVisibility(
for (const def of DATA_CARD_DEFS) {
if (def.key in override) {
result[def.key] = override[def.key]
} else if (def.defaultHidden) {
result[def.key] = false
} else {
result[def.key] = def.defaultHiddenIfNoCap ? has(CAP_KEY_MAP[def.key] ?? '') : true
}
@@ -54,60 +80,102 @@ export function getCardVisibility(
return result
}
/**
* 读取卡片显示顺序。
* - 用户拖拽设置过 → 用设置值(过滤掉已不存在的 key, 补齐新增的 key)
* - 未设置 → 用 DATA_CARD_DEFS 默认顺序
*/
export function getCardOrder(): CardKey[] {
const saved = storage.dataCardOrder.get([])
if (!saved.length) return [...DEFAULT_ORDER]
const known = new Set<CardKey>(DEFAULT_ORDER)
const ordered = saved.filter(k => known.has(k as CardKey)) as CardKey[]
// 补齐新增的 key(默认顺序里新增的卡片追加到末尾)
for (const k of DEFAULT_ORDER) {
if (!ordered.includes(k)) ordered.push(k)
}
return ordered
}
export function PageSettingsModal({
caps,
}: {
caps: Record<string, unknown> | undefined
}) {
const [visible, setVisible] = useState<Record<string, boolean>>(() => getCardVisibility(caps))
const [order, setOrder] = useState<CardKey[]>(() => getCardOrder())
const toggle = (key: CardKey) => {
const next = { ...visible, [key]: !visible[key] }
const persistVisible = (next: Record<string, boolean>) => {
setVisible(next)
storage.dataCardVisible.set(next)
window.dispatchEvent(new CustomEvent('data-card-visible-change'))
}
const reset = () => {
storage.dataCardVisible.set({})
setVisible(getCardVisibility(caps))
const persistOrder = (next: CardKey[]) => {
setOrder(next)
storage.dataCardOrder.set(next)
window.dispatchEvent(new CustomEvent('data-card-visible-change'))
}
const toggle = (key: CardKey) => persistVisible({ ...visible, [key]: !(visible[key] ?? true) })
const reset = () => {
// 恢复默认: 默认顺序 + 仅勾选前 5 张卡片, 其余隐藏
const defaultOrder = [...DEFAULT_ORDER]
const defaultVisible: Record<string, boolean> = {}
defaultOrder.forEach((k, i) => { defaultVisible[k] = i < DEFAULT_VISIBLE_COUNT })
storage.dataCardVisible.set(defaultVisible)
storage.dataCardOrder.set(defaultOrder)
setVisible(defaultVisible)
setOrder(defaultOrder)
window.dispatchEvent(new CustomEvent('data-card-visible-change'))
}
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
)
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event
if (!over || active.id === over.id) return
const oldIdx = order.indexOf(active.id as CardKey)
const newIdx = order.indexOf(over.id as CardKey)
if (oldIdx < 0 || newIdx < 0) return
persistOrder(arrayMove(order, oldIdx, newIdx))
}
// 按 order 排序卡片定义
const defByKey = new Map(DATA_CARD_DEFS.map(d => [d.key, d]))
const orderedDefs = order.map(k => defByKey.get(k)!).filter(Boolean)
return (
<div className="space-y-2.5">
<p className="text-xs text-secondary leading-relaxed">
,
,,
</p>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext items={order} strategy={verticalListSortingStrategy}>
<div className="space-y-1.5">
{DATA_CARD_DEFS.map((def) => {
{orderedDefs.map((def) => {
const on = visible[def.key] ?? true
return (
<label
<SortableCardRow
key={def.key}
className={`flex items-center gap-2.5 rounded-card border px-3 py-2 cursor-pointer transition-colors ${
on ? 'border-accent/40 bg-accent/[0.05]' : 'border-border bg-base/30 hover:border-border/70'
}`}
>
<button
type="button"
onClick={() => toggle(def.key)}
className={`flex h-4 w-4 shrink-0 items-center justify-center rounded border transition-colors ${
on ? 'bg-accent border-accent' : 'bg-base border-border'
}`}
role="checkbox"
aria-checked={on}
>
{on && <Check className="h-3 w-3 text-white" strokeWidth={3} />}
</button>
<div className="min-w-0 flex-1">
<div className="text-xs font-medium text-foreground">{def.label}</div>
<div className="text-[10px] text-muted leading-snug">{def.desc}</div>
</div>
</label>
id={def.key}
label={def.label}
desc={def.desc}
on={on}
onToggle={() => toggle(def.key)}
/>
)
})}
</div>
</SortableContext>
</DndContext>
<div className="flex items-center justify-end pt-1">
<button
onClick={reset}
@@ -119,3 +187,67 @@ export function PageSettingsModal({
</div>
)
}
// ── 可拖拽的卡片行 ──
function SortableCardRow({
id, label, desc, on, onToggle,
}: {
id: CardKey
label: string
desc: string
on: boolean
onToggle: () => void
}) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id })
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.6 : 1,
zIndex: isDragging ? 10 : undefined,
}
return (
<div
ref={setNodeRef}
style={style}
className={`flex items-center gap-2 rounded-card border px-3 py-2 transition-colors ${
isDragging ? 'bg-elevated shadow-lg' : ''
} ${on ? 'border-accent/40 bg-accent/[0.05]' : 'border-border bg-base/30'}`}
>
{/* 拖拽手柄 */}
<button
type="button"
{...attributes}
{...listeners}
className="cursor-grab active:cursor-grabbing text-muted hover:text-foreground transition-colors shrink-0"
title="拖动排序"
>
<GripVertical className="h-4 w-4" />
</button>
{/* 显隐勾选 */}
<button
type="button"
onClick={onToggle}
className={`flex h-4 w-4 shrink-0 items-center justify-center rounded border transition-colors ${
on ? 'bg-accent border-accent' : 'bg-base border-border'
}`}
role="checkbox"
aria-checked={on}
>
{on && <Check className="h-3 w-3 text-white" strokeWidth={3} />}
</button>
<div className="min-w-0 flex-1">
<div className="text-xs font-medium text-foreground">{label}</div>
<div className="text-[10px] text-muted leading-snug">{desc}</div>
</div>
</div>
)
}
+2
View File
@@ -110,4 +110,6 @@ export const storage = {
/** 数据页画像卡片显隐 (卡片key → 是否显示) */
dataCardVisible: kv<Record<string, boolean>>('data-card-visible'),
/** 数据页画像卡片顺序 (卡片key 数组, 长度=卡片总数) */
dataCardOrder: kv<string[]>('data-card-order'),
} as const
+16 -1
View File
@@ -2,7 +2,7 @@ import { useState, useEffect, useRef, type ReactNode } from 'react'
import { Link } from 'react-router-dom'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { motion, AnimatePresence } from 'framer-motion'
import { Activity, ArrowDownRight, ArrowUpRight, BarChart3, BellRing, Database, Flame, Gauge, LineChart, Loader2, Play, RefreshCw, Sparkles, Target, Timer } from 'lucide-react'
import { Activity, ArrowDownRight, ArrowUpRight, BarChart3, BellRing, Database, Flame, Gauge, Info, LineChart, Loader2, Play, RefreshCw, Sparkles, Target, Timer } from 'lucide-react'
import { DatePicker } from '@/components/DatePicker'
import { api, type MarketSnapshotRow, type OverviewDimensionRankItem, type OverviewMarket, type AlertEvent } from '@/lib/api'
import { QK } from '@/lib/queryKeys'
@@ -583,6 +583,9 @@ export function Dashboard() {
const latestDate = dataStatus.data?.enriched?.latest_date ?? null
const currentDate = selectedDate ?? data.as_of ?? ''
const quoteRunning = (!selectedDate || selectedDate === latestDate) && data.quote_status?.running
// 实时模式: none / watchlist / full_market。
// watchlist (Free 档) 仅自选 ≤5 只实时, 看板呈现的大盘数据实为盘后快照, 需提示避免误读。
const quoteMode = data.quote_status?.mode as ('none' | 'watchlist' | 'full_market') | undefined
return (
<div className="min-h-full bg-base p-3">
@@ -650,6 +653,18 @@ export function Dashboard() {
</div>
</div>
{/* Free 档提示: 大盘看板为盘后数据, 仅自选股实时。避免用户误读为全市场实时。 */}
{quoteMode === 'watchlist' && (
<div className="mb-3 flex items-start gap-2 rounded-card border border-amber-500/30 bg-amber-500/8 px-3 py-2 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>
</div>
</div>
)}
<div className="mb-3 grid grid-cols-4 gap-2">
{data.indices.map(item => <IndexTicker key={item.symbol} item={item} />)}
</div>
+170 -151
View File
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { motion, AnimatePresence } from 'framer-motion'
import {
@@ -42,7 +42,7 @@ import { ExtendHistoryPanel } from '@/components/data/ExtendHistoryPanel'
import { EnrichedRebuildPanel } from '@/components/data/EnrichedRebuildPanel'
import { MinuteSyncConfig } from '@/components/data/MinuteSyncConfig'
import { PipelineScopeConfig } from '@/components/data/PipelineScopeConfig'
import { PageSettingsModal, getCardVisibility } from '@/components/data/PageSettingsModal'
import { PageSettingsModal, getCardVisibility, getCardOrder, type CardKey } from '@/components/data/PageSettingsModal'
import { QuoteConfigCard } from '@/components/data/QuoteConfigCard'
import { EnrichedSchemaModal } from '@/components/data/SchemaModal'
import { Skeleton } from '@/components/data/Skeleton'
@@ -317,6 +317,171 @@ export function Data() {
})
}, [])
// 按卡片 key 渲染对应的 StatCard (顺序由 getCardOrder 控制, 显隐由 cardVisible 控制)
const renderStatCard = (k: CardKey): React.ReactNode => {
switch (k) {
case 'instruments':
return (
<StatCard
title="个股维表"
hint="盘前同步 · 元数据快照"
stats={s?.instruments}
isInstrument
loading={isLoading}
active={activeCard === 'instruments'}
done={doneStages.has('instruments')}
skipped={skippedCards.has('instruments')}
stagePct={activeCard === 'instruments' ? (job.data?.stage_pct ?? 0) : 0}
tierKey="instruments"
capLimits={caps.data?.capabilities}
tierLabel={caps.data?.label}
auto
onShowFields={() => setSchemaTable('instruments')}
/>
)
case 'daily':
return (
<StatCard
title="日 K"
hint="增量同步 · 全市场"
stats={s?.daily}
loading={isLoading}
active={activeCard === 'daily'}
done={doneStages.has('daily')}
skipped={skippedCards.has('daily')}
stagePct={activeCard === 'daily' ? (job.data?.stage_pct ?? 0) : 0}
tierKey="daily"
capLimits={caps.data?.capabilities}
tierLabel={caps.data?.label}
auto
onShowFields={() => setSchemaTable('daily')}
onSettings={hasData ? () => setOpenSettings(v => v === 'daily' ? null : 'daily') : undefined}
settingsOpen={openSettings === 'daily'}
/>
)
case 'adj_factor':
return (
<StatCard
title="除权因子"
hint="增量同步 · 全市场"
stats={s?.adj_factor}
loading={isLoading}
active={activeCard === 'adj_factor'}
done={doneStages.has('adj_factor')}
skipped={skippedCards.has('adj_factor')}
stagePct={activeCard === 'adj_factor' ? (job.data?.stage_pct ?? 0) : 0}
tierKey="adj_factor"
capLimits={caps.data?.capabilities}
tierLabel={caps.data?.label}
auto
onShowFields={() => setSchemaTable('adj_factor')}
/>
)
case 'enriched':
return (
<StatCard
title="Enriched"
hint="复权 OHLCV + 技术指标"
stats={s?.enriched}
loading={isLoading}
active={activeCard === 'enriched'}
done={doneStages.has('enriched')}
skipped={skippedCards.has('enriched')}
stagePct={activeCard === 'enriched' ? (job.data?.stage_pct ?? 0) : 0}
tierKey="enriched"
capLimits={caps.data?.capabilities}
tierLabel={caps.data?.label}
auto
subLabel="字段 · 指标 · 信号"
localBadgeSuffix={`${prefs.data?.enriched_batch_size ?? 1000}只/批`}
onShowFields={() => setSchemaTable('enriched')}
onSettings={hasData ? () => setOpenSettings(v => v === 'enriched' ? null : 'enriched') : undefined}
settingsOpen={openSettings === 'enriched'}
/>
)
case 'index':
return (
<StatCard
title="指数"
hint="CN_Index · 独立存储"
stats={indexOverviewStats}
loading={isLoading}
active={activeCard === 'index_daily'}
done={doneStages.has('index_daily')}
skipped={skippedCards.has('index_daily')}
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={[
{ label: '维表', table: 'index_instruments' },
{ label: '日K', table: 'index_daily' },
{ label: '指标', table: 'index_enriched' },
] as FieldTab[]}
onShowFields={(t) => setSchemaTable(t ?? 'index_daily')}
onSettings={hasData ? () => setOpenSettings(v => v === 'index' ? null : 'index') : undefined}
settingsOpen={openSettings === 'index'}
/>
)
case 'etf':
return (
<StatCard
title="ETF"
hint="场内基金 · 独立存储"
stats={etfOverviewStats}
loading={isLoading}
tierKey="etf"
capLimits={caps.data?.capabilities}
tierLabel={caps.data?.label}
auto={etfAuto}
subLabel="维表 · 日K · 指标"
fieldTabs={[
{ label: '维表', table: 'etf_instruments' },
{ label: '日K', table: 'etf_daily' },
{ label: '指标', table: 'etf_enriched' },
] as FieldTab[]}
onShowFields={(t) => setSchemaTable(t ?? 'etf_daily')}
/>
)
case 'minute':
return (
<StatCard
title="分钟 K"
hint="全市场同步"
stats={s?.minute}
loading={isLoading}
active={activeCard === 'minute'}
done={doneStages.has('minute')}
skipped={skippedCards.has('minute')}
stagePct={activeCard === 'minute' ? (job.data?.stage_pct ?? 0) : 0}
tierKey="minute"
capLimits={caps.data?.capabilities}
tierLabel={caps.data?.label}
auto={minuteAuto}
onShowFields={() => setSchemaTable('minute')}
onSettings={hasData ? () => setOpenSettings(v => v === 'minute' ? null : 'minute') : undefined}
settingsOpen={openSettings === 'minute'}
/>
)
case 'financials':
return (
<StatCard
title="财务数据"
hint="利润表 / 资负表 / 现金流 / 指标"
stats={s?.financials ? { rows: s.financials.rows } : null}
loading={isLoading}
tierKey="financials"
capLimits={caps.data?.capabilities}
tierLabel={caps.data?.label}
/>
)
default:
return null
}
}
return (
<>
<div ref={topRef} />
@@ -620,155 +785,9 @@ export function Data() {
<div>
<SectionTitle icon={Database}></SectionTitle>
<div className="mt-3 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-4 items-stretch">
{cardVisible.instruments && (
<StatCard
title="个股维表"
hint="盘前同步 · 元数据快照"
stats={s?.instruments}
isInstrument
loading={isLoading}
active={activeCard === 'instruments'}
done={doneStages.has('instruments')}
skipped={skippedCards.has('instruments')}
stagePct={activeCard === 'instruments' ? (job.data?.stage_pct ?? 0) : 0}
tierKey="instruments"
capLimits={caps.data?.capabilities}
tierLabel={caps.data?.label}
auto
onShowFields={() => setSchemaTable('instruments')}
/>
)}
{cardVisible.daily && (
<StatCard
title="日 K"
hint="增量同步 · 全市场"
stats={s?.daily}
loading={isLoading}
active={activeCard === 'daily'}
done={doneStages.has('daily')}
skipped={skippedCards.has('daily')}
stagePct={activeCard === 'daily' ? (job.data?.stage_pct ?? 0) : 0}
tierKey="daily"
capLimits={caps.data?.capabilities}
tierLabel={caps.data?.label}
auto
onShowFields={() => setSchemaTable('daily')}
onSettings={hasData ? () => setOpenSettings(v => v === 'daily' ? null : 'daily') : undefined}
settingsOpen={openSettings === 'daily'}
/>
)}
{cardVisible.enriched && (
<StatCard
title="Enriched"
hint="复权 OHLCV + 技术指标"
stats={s?.enriched}
loading={isLoading}
active={activeCard === 'enriched'}
done={doneStages.has('enriched')}
skipped={skippedCards.has('enriched')}
stagePct={activeCard === 'enriched' ? (job.data?.stage_pct ?? 0) : 0}
tierKey="enriched"
capLimits={caps.data?.capabilities}
tierLabel={caps.data?.label}
auto
subLabel="字段 · 指标 · 信号"
localBadgeSuffix={`${prefs.data?.enriched_batch_size ?? 1000}只/批`}
onShowFields={() => setSchemaTable('enriched')}
onSettings={hasData ? () => setOpenSettings(v => v === 'enriched' ? null : 'enriched') : undefined}
settingsOpen={openSettings === 'enriched'}
/>
)}
{cardVisible.index && (
<StatCard
title="指数"
hint="CN_Index · 独立存储"
stats={indexOverviewStats}
loading={isLoading}
active={activeCard === 'index_daily'}
done={doneStages.has('index_daily')}
skipped={skippedCards.has('index_daily')}
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={[
{ label: '维表', table: 'index_instruments' },
{ label: '日K', table: 'index_daily' },
{ label: '指标', table: 'index_enriched' },
] as FieldTab[]}
onShowFields={(t) => setSchemaTable(t ?? 'index_daily')}
onSettings={hasData ? () => setOpenSettings(v => v === 'index' ? null : 'index') : undefined}
settingsOpen={openSettings === 'index'}
/>
)}
{cardVisible.etf && (
<StatCard
title="ETF"
hint="场内基金 · 独立存储"
stats={etfOverviewStats}
loading={isLoading}
tierKey="etf"
capLimits={caps.data?.capabilities}
tierLabel={caps.data?.label}
auto={etfAuto}
subLabel="维表 · 日K · 指标"
fieldTabs={[
{ label: '维表', table: 'etf_instruments' },
{ label: '日K', table: 'etf_daily' },
{ label: '指标', table: 'etf_enriched' },
] as FieldTab[]}
onShowFields={(t) => setSchemaTable(t ?? 'etf_daily')}
/>
)}
{cardVisible.adj_factor && (
<StatCard
title="除权因子"
hint="增量同步 · 全市场"
stats={s?.adj_factor}
loading={isLoading}
active={activeCard === 'adj_factor'}
done={doneStages.has('adj_factor')}
skipped={skippedCards.has('adj_factor')}
stagePct={activeCard === 'adj_factor' ? (job.data?.stage_pct ?? 0) : 0}
tierKey="adj_factor"
capLimits={caps.data?.capabilities}
tierLabel={caps.data?.label}
auto
onShowFields={() => setSchemaTable('adj_factor')}
/>
)}
{cardVisible.minute && (
<StatCard
title="分钟 K"
hint="全市场同步"
stats={s?.minute}
loading={isLoading}
active={activeCard === 'minute'}
done={doneStages.has('minute')}
skipped={skippedCards.has('minute')}
stagePct={activeCard === 'minute' ? (job.data?.stage_pct ?? 0) : 0}
tierKey="minute"
capLimits={caps.data?.capabilities}
tierLabel={caps.data?.label}
auto={minuteAuto}
onShowFields={() => setSchemaTable('minute')}
onSettings={hasData ? () => setOpenSettings(v => v === 'minute' ? null : 'minute') : undefined}
settingsOpen={openSettings === 'minute'}
/>
)}
{cardVisible.financials && (
<StatCard
title="财务数据"
hint="利润表 / 资负表 / 现金流 / 指标"
stats={s?.financials ? { rows: s.financials.rows } : null}
loading={isLoading}
tierKey="financials"
capLimits={caps.data?.capabilities}
tierLabel={caps.data?.label}
/>
)}
{getCardOrder().filter(k => cardVisible[k]).map((k: CardKey) => (
<Fragment key={k}>{renderStatCard(k)}</Fragment>
))}
{(extConfigs.data?.items ?? []).map((ext) => (
<ExtDataStatCard
key={ext.id}
+1 -1
View File
@@ -181,7 +181,7 @@ export function Financials() {
}
/>
<div className="w-full max-w-none px-8 py-6 space-y-6">
<div className="px-8 py-6 space-y-6 max-w-7xl">
{syncing && (
<div className="flex items-center gap-2 rounded-card border border-accent/30 bg-accent/[0.06] px-3 py-2 text-xs text-accent">
<Loader2 className="h-3.5 w-3.5 animate-spin shrink-0" />
+1 -1
View File
@@ -90,7 +90,7 @@ export function StockAnalysis() {
}
/>
<div className="w-full max-w-none px-8 py-6 space-y-6">
<div className="px-8 py-6 space-y-6 max-w-7xl">
{/* 搜索栏 */}
<div className="flex items-center gap-3">
<div className="w-72">
@@ -39,6 +39,7 @@ const BUILTIN_PAGES: NavEntry[] = [
{ id: '/concept-analysis', label: '概念分析', type: 'builtin', visible: true },
{ id: '/industry-analysis', label: '行业分析', type: 'builtin', visible: true },
{ id: '/stock-analysis', label: '个股分析', type: 'builtin', visible: true },
{ id: '/review', label: '复盘', type: 'builtin', visible: true },
{ id: '/financials', label: '财务分析', type: 'builtin', visible: true },
{ id: '/indices', label: '指数', type: 'builtin', visible: true },
{ id: '/trading', label: '交易', type: 'builtin', visible: true },
Binary file not shown.

Before

Width:  |  Height:  |  Size: 581 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 727 KiB