mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 16:44:15 +08:00
chore: bump version to 0.1.65
This commit is contained in:
@@ -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,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>
|
||||
<div className="space-y-1.5">
|
||||
{DATA_CARD_DEFS.map((def) => {
|
||||
const on = visible[def.key] ?? true
|
||||
return (
|
||||
<label
|
||||
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>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext items={order} strategy={verticalListSortingStrategy}>
|
||||
<div className="space-y-1.5">
|
||||
{orderedDefs.map((def) => {
|
||||
const on = visible[def.key] ?? true
|
||||
return (
|
||||
<SortableCardRow
|
||||
key={def.key}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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 |
Reference in New Issue
Block a user