diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 6f07f15..ebce2c4 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -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" diff --git a/frontend/package.json b/frontend/package.json index 07720ae..50abd9e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "tickflow-stock-panel-frontend", "private": true, - "version": "0.1.64", + "version": "0.1.65", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/components/data/PageSettingsModal.tsx b/frontend/src/components/data/PageSettingsModal.tsx index 121cce3..86abaae 100644 --- a/frontend/src/components/data/PageSettingsModal.tsx +++ b/frontend/src/components/data/PageSettingsModal.tsx @@ -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> = { adj_factor: 'adj_factor', minute: 'kline.minute.batch', @@ -35,6 +58,7 @@ const CAP_KEY_MAP: Partial> = { /** * 读取卡片显隐状态。结合档位能力决定默认值: * - 用户显式设置过 → 用设置值 + * - 未设置 + 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(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 | undefined }) { const [visible, setVisible] = useState>(() => getCardVisibility(caps)) + const [order, setOrder] = useState(() => getCardOrder()) - const toggle = (key: CardKey) => { - const next = { ...visible, [key]: !visible[key] } + const persistVisible = (next: Record) => { 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 = {} + 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 (

- 勾选要在数据画像区显示的卡片。未勾选的卡片将隐藏,不影响数据本身。 + 拖动手柄调整卡片顺序,勾选控制显隐。未勾选的卡片将隐藏,不影响数据本身。

-
- {DATA_CARD_DEFS.map((def) => { - const on = visible[def.key] ?? true - return ( - - ) - })} -
+ + +
+ {orderedDefs.map((def) => { + const on = visible[def.key] ?? true + return ( + toggle(def.key)} + /> + ) + })} +
+
+
+ {/* 显隐勾选 */} + +
+
{label}
+
{desc}
+
+
+ ) +} diff --git a/frontend/src/lib/storage.ts b/frontend/src/lib/storage.ts index ad134e7..8f368ef 100644 --- a/frontend/src/lib/storage.ts +++ b/frontend/src/lib/storage.ts @@ -110,4 +110,6 @@ export const storage = { /** 数据页画像卡片显隐 (卡片key → 是否显示) */ dataCardVisible: kv>('data-card-visible'), + /** 数据页画像卡片顺序 (卡片key 数组, 长度=卡片总数) */ + dataCardOrder: kv('data-card-order'), } as const diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 3903e7d..c5e8ee4 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -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 (
@@ -650,6 +653,18 @@ export function Dashboard() {
+ {/* Free 档提示: 大盘看板为盘后数据, 仅自选股实时。避免用户误读为全市场实时。 */} + {quoteMode === 'watchlist' && ( +
+ +
+ 当前为「自选实时」模式,看板展示的大盘数据为盘后快照(最新有数据日),并非盘中实时; + 仅自选股({data.quote_status?.watchlist_symbol_count ?? 0} 只)支持实时监控。 + 全市场实时需 Starter+ +
+
+ )} +
{data.indices.map(item => )}
diff --git a/frontend/src/pages/Data.tsx b/frontend/src/pages/Data.tsx index 54b1d92..ffc47cc 100644 --- a/frontend/src/pages/Data.tsx +++ b/frontend/src/pages/Data.tsx @@ -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 ( + setSchemaTable('instruments')} + /> + ) + case 'daily': + return ( + setSchemaTable('daily')} + onSettings={hasData ? () => setOpenSettings(v => v === 'daily' ? null : 'daily') : undefined} + settingsOpen={openSettings === 'daily'} + /> + ) + case 'adj_factor': + return ( + setSchemaTable('adj_factor')} + /> + ) + case 'enriched': + return ( + setSchemaTable('enriched')} + onSettings={hasData ? () => setOpenSettings(v => v === 'enriched' ? null : 'enriched') : undefined} + settingsOpen={openSettings === 'enriched'} + /> + ) + case 'index': + return ( + setSchemaTable(t ?? 'index_daily')} + onSettings={hasData ? () => setOpenSettings(v => v === 'index' ? null : 'index') : undefined} + settingsOpen={openSettings === 'index'} + /> + ) + case 'etf': + return ( + setSchemaTable(t ?? 'etf_daily')} + /> + ) + case 'minute': + return ( + setSchemaTable('minute')} + onSettings={hasData ? () => setOpenSettings(v => v === 'minute' ? null : 'minute') : undefined} + settingsOpen={openSettings === 'minute'} + /> + ) + case 'financials': + return ( + + ) + default: + return null + } + } + return ( <>
@@ -620,155 +785,9 @@ export function Data() {
数据画像
- {cardVisible.instruments && ( - setSchemaTable('instruments')} - /> - )} - {cardVisible.daily && ( - setSchemaTable('daily')} - onSettings={hasData ? () => setOpenSettings(v => v === 'daily' ? null : 'daily') : undefined} - settingsOpen={openSettings === 'daily'} - /> - )} - {cardVisible.enriched && ( - setSchemaTable('enriched')} - onSettings={hasData ? () => setOpenSettings(v => v === 'enriched' ? null : 'enriched') : undefined} - settingsOpen={openSettings === 'enriched'} - /> - )} - {cardVisible.index && ( - setSchemaTable(t ?? 'index_daily')} - onSettings={hasData ? () => setOpenSettings(v => v === 'index' ? null : 'index') : undefined} - settingsOpen={openSettings === 'index'} - /> - )} - {cardVisible.etf && ( - setSchemaTable(t ?? 'etf_daily')} - /> - )} - {cardVisible.adj_factor && ( - setSchemaTable('adj_factor')} - /> - )} - {cardVisible.minute && ( - setSchemaTable('minute')} - onSettings={hasData ? () => setOpenSettings(v => v === 'minute' ? null : 'minute') : undefined} - settingsOpen={openSettings === 'minute'} - /> - )} - {cardVisible.financials && ( - - )} + {getCardOrder().filter(k => cardVisible[k]).map((k: CardKey) => ( + {renderStatCard(k)} + ))} {(extConfigs.data?.items ?? []).map((ext) => ( -
+
{syncing && (
diff --git a/frontend/src/pages/StockAnalysis.tsx b/frontend/src/pages/StockAnalysis.tsx index 959a284..905ed65 100644 --- a/frontend/src/pages/StockAnalysis.tsx +++ b/frontend/src/pages/StockAnalysis.tsx @@ -90,7 +90,7 @@ export function StockAnalysis() { } /> -
+
{/* 搜索栏 */}
diff --git a/frontend/src/pages/settings/MenuSettings.tsx b/frontend/src/pages/settings/MenuSettings.tsx index 505bbbc..14d7303 100644 --- a/frontend/src/pages/settings/MenuSettings.tsx +++ b/frontend/src/pages/settings/MenuSettings.tsx @@ -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 }, diff --git a/screenshots/financials-ultrawide-fix.png b/screenshots/financials-ultrawide-fix.png deleted file mode 100644 index 1ee13d8..0000000 Binary files a/screenshots/financials-ultrawide-fix.png and /dev/null differ diff --git a/screenshots/stock-analysis-ultrawide-fix.png b/screenshots/stock-analysis-ultrawide-fix.png deleted file mode 100644 index 8285006..0000000 Binary files a/screenshots/stock-analysis-ultrawide-fix.png and /dev/null differ