1
This commit is contained in:
parent
e1d4916615
commit
76bad64163
Binary file not shown.
@ -13,11 +13,28 @@ from app.db.database import init_db
|
|||||||
from app.engine.scheduler import start_scheduler, stop_scheduler
|
from app.engine.scheduler import start_scheduler, stop_scheduler
|
||||||
from app.api import market, sectors, recommendations, stocks, watchlists, websocket, chat, auth, debug
|
from app.api import market, sectors, recommendations, stocks, watchlists, websocket, chat, auth, debug
|
||||||
|
|
||||||
logging.basicConfig(
|
def configure_logging() -> None:
|
||||||
level=logging.DEBUG if settings.debug else logging.INFO,
|
logging.basicConfig(
|
||||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
level=logging.DEBUG if settings.debug else logging.INFO,
|
||||||
datefmt="%Y-%m-%d %H:%M:%S",
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||||
)
|
datefmt="%Y-%m-%d %H:%M:%S",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 保留应用日志的调试能力,但压制基础设施层的噪音。
|
||||||
|
noisy_loggers = {
|
||||||
|
"aiosqlite": logging.WARNING,
|
||||||
|
"sqlalchemy": logging.WARNING,
|
||||||
|
"sqlalchemy.engine": logging.WARNING,
|
||||||
|
"sqlalchemy.pool": logging.WARNING,
|
||||||
|
"httpx": logging.INFO,
|
||||||
|
"httpcore": logging.WARNING,
|
||||||
|
"uvicorn.access": logging.INFO,
|
||||||
|
}
|
||||||
|
for name, level in noisy_loggers.items():
|
||||||
|
logging.getLogger(name).setLevel(level)
|
||||||
|
|
||||||
|
|
||||||
|
configure_logging()
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -11,8 +11,6 @@ import type {
|
|||||||
SectorData,
|
SectorData,
|
||||||
StrategyBoard,
|
StrategyBoard,
|
||||||
} from "@/lib/api";
|
} from "@/lib/api";
|
||||||
import MarketTemp from "@/components/market-temp";
|
|
||||||
import SectorHeatmap from "@/components/sector-heatmap";
|
|
||||||
import { ThemeToggle } from "@/components/theme-toggle";
|
import { ThemeToggle } from "@/components/theme-toggle";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { useWebSocket } from "@/hooks/use-websocket";
|
import { useWebSocket } from "@/hooks/use-websocket";
|
||||||
@ -209,9 +207,7 @@ export default function DashboardPage() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-text-muted mt-1">
|
<p className="mt-1 text-xs text-text-muted">先看结论,再看动作,再看焦点标的。</p>
|
||||||
先看今天能不能做,再看该做什么,最后进入执行。
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -242,17 +238,27 @@ export default function DashboardPage() {
|
|||||||
observeCount={observe.length}
|
observeCount={observe.length}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 xl:grid-cols-[minmax(0,1.3fr)_minmax(280px,0.7fr)] gap-4 animate-fade-in-up">
|
<div className="grid grid-cols-1 xl:grid-cols-[minmax(0,1.2fr)_minmax(300px,0.8fr)] gap-4 animate-fade-in-up">
|
||||||
<ActionPanel
|
<ActionPanel
|
||||||
actions={todayActions}
|
actions={todayActions}
|
||||||
focusQueue={focusQueue.slice(0, 4)}
|
summary={marketSummary}
|
||||||
fallbackTitle={actionable.length ? "优先执行" : watch.length ? "重点观察" : "仅保留观察"}
|
|
||||||
/>
|
/>
|
||||||
<ExecutionPanel
|
<FocusPanel
|
||||||
recommendations={recommendations}
|
focusQueue={focusQueue.slice(0, 3)}
|
||||||
actionableCount={actionable.length}
|
actionableCount={actionable.length}
|
||||||
watchCount={watch.length}
|
watchCount={watch.length}
|
||||||
observeCount={observe.length}
|
observeCount={observe.length}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 xl:grid-cols-[minmax(0,1fr)_320px] gap-4 animate-fade-in-up">
|
||||||
|
<MarketSnapshot
|
||||||
|
marketTemperature={marketTemperature ?? data?.market_temperature ?? null}
|
||||||
|
indices={indices}
|
||||||
|
sectors={sectors}
|
||||||
|
summary={marketSummary}
|
||||||
|
/>
|
||||||
|
<AdminPanel
|
||||||
isAdmin={user?.role === "admin"}
|
isAdmin={user?.role === "admin"}
|
||||||
opsStatus={opsStatus}
|
opsStatus={opsStatus}
|
||||||
refreshing={refreshing}
|
refreshing={refreshing}
|
||||||
@ -261,11 +267,6 @@ export default function DashboardPage() {
|
|||||||
onAction={handleAdminAction}
|
onAction={handleAdminAction}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 xl:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] gap-4 animate-fade-in-up">
|
|
||||||
<MarketTemp data={marketTemperature ?? data?.market_temperature ?? null} indices={indices} />
|
|
||||||
<SectorHeatmap sectors={sectors} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -321,63 +322,147 @@ function DecisionHero({
|
|||||||
|
|
||||||
function ActionPanel({
|
function ActionPanel({
|
||||||
actions,
|
actions,
|
||||||
focusQueue,
|
summary,
|
||||||
fallbackTitle,
|
|
||||||
}: {
|
}: {
|
||||||
actions: ReturnType<typeof buildActionGuides>;
|
actions: ReturnType<typeof buildActionGuides>;
|
||||||
focusQueue: RecommendationData[];
|
summary: ReturnType<typeof buildMarketSummary>;
|
||||||
fallbackTitle: string;
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="glass-card-static p-4 md:p-5">
|
||||||
<div className="glass-card-static p-4 md:p-5">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<CompactBadge label="策略" value={summary.modeLabel} />
|
||||||
<div>
|
<CompactBadge label="仓位" value={summary.positionLabel} />
|
||||||
<h3 className="text-sm font-semibold text-text-primary">今天该做什么</h3>
|
<CompactBadge label="风险" value={summary.riskLabel} />
|
||||||
<p className="mt-1 text-xs text-text-muted">把市场结论拆成可执行动作,而不是只给情绪描述。</p>
|
|
||||||
</div>
|
|
||||||
<a href="/strategy" className="text-xs text-text-muted transition-colors hover:text-amber-400">
|
|
||||||
系统校准
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-4 grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
||||||
<ActionBucket title="优先动作" items={actions.priority} tone="priority" />
|
|
||||||
<ActionBucket title="观察队列" items={actions.watch} tone="watch" />
|
|
||||||
<ActionBucket title="回避事项" items={actions.avoid} tone="avoid" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="glass-card-static p-4 md:p-5">
|
<div className="mt-4 grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<ActionBucket title="现在做" items={actions.priority} tone="priority" />
|
||||||
<div>
|
<ActionBucket title="盯住" items={actions.watch} tone="watch" />
|
||||||
<h3 className="text-sm font-semibold text-text-primary">{fallbackTitle}</h3>
|
<ActionBucket title="不要做" items={actions.avoid} tone="avoid" />
|
||||||
<p className="mt-1 text-xs text-text-muted">只展示最该盯的少量标的,避免首页无限拉长。</p>
|
|
||||||
</div>
|
|
||||||
<a href="/recommendations" className="text-xs text-text-muted transition-colors hover:text-amber-400">
|
|
||||||
全部推荐
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-4 grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
||||||
{focusQueue.length ? (
|
|
||||||
focusQueue.map((rec) => <FocusStockCard key={rec.ts_code} rec={rec} />)
|
|
||||||
) : (
|
|
||||||
<div className="col-span-full rounded-2xl border border-border-subtle bg-surface-1/70 p-6 text-center text-sm text-text-muted">
|
|
||||||
暂无标的,等待新一轮扫描输出。
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ExecutionPanel({
|
function FocusPanel({
|
||||||
recommendations,
|
focusQueue,
|
||||||
actionableCount,
|
actionableCount,
|
||||||
watchCount,
|
watchCount,
|
||||||
observeCount,
|
observeCount,
|
||||||
|
}: {
|
||||||
|
focusQueue: RecommendationData[];
|
||||||
|
actionableCount: number;
|
||||||
|
watchCount: number;
|
||||||
|
observeCount: number;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="glass-card-static p-4 md:p-5">
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-text-primary">焦点标的</h3>
|
||||||
|
<p className="mt-1 text-xs text-text-muted">首页只保留最该处理的少量标的。</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-2 text-center">
|
||||||
|
<MiniCount label="可操作" value={actionableCount} tone="text-red-400" />
|
||||||
|
<MiniCount label="关注" value={watchCount} tone="text-amber-400" />
|
||||||
|
<MiniCount label="观察" value={observeCount} tone="text-text-secondary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 space-y-3">
|
||||||
|
{focusQueue.length ? (
|
||||||
|
focusQueue.map((rec) => <FocusStockCard key={rec.ts_code} rec={rec} />)
|
||||||
|
) : (
|
||||||
|
<div className="rounded-2xl border border-border-subtle bg-surface-1/70 p-6 text-center text-sm text-text-muted">
|
||||||
|
今天没有需要处理的焦点标的。
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MarketSnapshot({
|
||||||
|
marketTemperature,
|
||||||
|
indices,
|
||||||
|
sectors,
|
||||||
|
summary,
|
||||||
|
}: {
|
||||||
|
marketTemperature: MarketTemperatureData | null;
|
||||||
|
indices: IndexOverview[];
|
||||||
|
sectors: SectorData[];
|
||||||
|
summary: ReturnType<typeof buildMarketSummary>;
|
||||||
|
}) {
|
||||||
|
const leadingSectors = sectors.slice(0, 4);
|
||||||
|
const majorIndices = indices.slice(0, 3);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="glass-card-static p-4 md:p-5">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-text-primary">市场证据</h3>
|
||||||
|
<p className="mt-1 text-xs text-text-muted">只保留对今天决策有用的信息。</p>
|
||||||
|
</div>
|
||||||
|
<span className="rounded-xl bg-surface-1/70 px-3 py-1.5 text-xs font-mono tabular-nums text-text-secondary">
|
||||||
|
温度 {Math.round(marketTemperature?.temperature ?? 0)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||||
|
<EvidenceStat label="上涨" value={marketTemperature?.up_count ?? 0} tone="text-red-400" />
|
||||||
|
<EvidenceStat label="下跌" value={marketTemperature?.down_count ?? 0} tone="text-emerald-400" />
|
||||||
|
<EvidenceStat label="涨停" value={marketTemperature?.limit_up_count ?? 0} tone="text-amber-400" />
|
||||||
|
<EvidenceStat label="跌停" value={marketTemperature?.limit_down_count ?? 0} tone="text-cyan-400" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{majorIndices.length ? (
|
||||||
|
<div className="mt-4 grid grid-cols-1 md:grid-cols-3 gap-2">
|
||||||
|
{majorIndices.map((item) => (
|
||||||
|
<div key={item.code} className="rounded-2xl border border-border-subtle bg-surface-1/70 px-3 py-3">
|
||||||
|
<div className="text-xs font-semibold text-text-primary">{item.name}</div>
|
||||||
|
<div className="mt-1 flex items-center justify-between gap-3">
|
||||||
|
<span className="text-xs font-mono tabular-nums text-text-secondary">{item.close.toFixed(2)}</span>
|
||||||
|
<span className={`text-xs font-mono tabular-nums ${item.pct_chg >= 0 ? "text-red-400" : "text-emerald-400"}`}>
|
||||||
|
{item.pct_chg >= 0 ? "+" : ""}{item.pct_chg.toFixed(2)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{leadingSectors.length ? (
|
||||||
|
<div className="mt-4 rounded-2xl border border-border-subtle bg-surface-1/70 p-3">
|
||||||
|
<div className="text-[11px] font-semibold text-text-secondary">今日盯住的板块</div>
|
||||||
|
<div className="mt-3 space-y-2">
|
||||||
|
{leadingSectors.map((sector) => {
|
||||||
|
const pct = sector.realtime_pct_change ?? sector.pct_change;
|
||||||
|
return (
|
||||||
|
<div key={sector.sector_code} className="flex items-center justify-between gap-3 text-sm">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="truncate font-medium text-text-primary">{sector.sector_name}</div>
|
||||||
|
<div className="text-[11px] text-text-muted">
|
||||||
|
{sector.limit_up_count > 0 ? `${sector.limit_up_count} 涨停` : "无涨停"} · {sector.stage || "mid"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className={`font-mono tabular-nums ${pct >= 0 ? "text-red-400" : "text-emerald-400"}`}>
|
||||||
|
{pct >= 0 ? "+" : ""}{pct.toFixed(2)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="mt-4 rounded-2xl border border-border-subtle bg-surface-1/70 px-3 py-3 text-sm text-text-secondary">
|
||||||
|
结论:{summary.headline}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AdminPanel({
|
||||||
isAdmin,
|
isAdmin,
|
||||||
opsStatus,
|
opsStatus,
|
||||||
refreshing,
|
refreshing,
|
||||||
@ -385,10 +470,6 @@ function ExecutionPanel({
|
|||||||
onRefresh,
|
onRefresh,
|
||||||
onAction,
|
onAction,
|
||||||
}: {
|
}: {
|
||||||
recommendations: RecommendationData[];
|
|
||||||
actionableCount: number;
|
|
||||||
watchCount: number;
|
|
||||||
observeCount: number;
|
|
||||||
isAdmin?: boolean;
|
isAdmin?: boolean;
|
||||||
opsStatus: OpsStatusResponse | null;
|
opsStatus: OpsStatusResponse | null;
|
||||||
refreshing: boolean;
|
refreshing: boolean;
|
||||||
@ -396,92 +477,43 @@ function ExecutionPanel({
|
|||||||
onRefresh: () => void;
|
onRefresh: () => void;
|
||||||
onAction: (action: "update_tracking" | "generate_strategy_board" | "generate_strategy_iteration") => void;
|
onAction: (action: "update_tracking" | "generate_strategy_board" | "generate_strategy_iteration") => void;
|
||||||
}) {
|
}) {
|
||||||
|
if (!isAdmin || !opsStatus) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="glass-card-static p-4">
|
||||||
<div className="glass-card-static p-4 md:p-5">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<h3 className="text-sm font-semibold text-text-primary">执行入口</h3>
|
<div>
|
||||||
<p className="mt-1 text-xs text-text-muted">不同任务进入不同页面,首页只保留关键入口和现状摘要。</p>
|
<h3 className="text-sm font-semibold text-text-primary">管理员</h3>
|
||||||
|
<p className="mt-1 text-xs text-text-muted">{opsStatus.data_freshness.message}</p>
|
||||||
<div className="mt-4 grid grid-cols-1 gap-3">
|
|
||||||
<NavCard
|
|
||||||
href="/recommendations"
|
|
||||||
title="推荐池"
|
|
||||||
description={`当前 ${recommendations.length} 只候选,${actionableCount} 只可操作,${watchCount} 只重点关注。`}
|
|
||||||
/>
|
|
||||||
<NavCard
|
|
||||||
href="/watchlist"
|
|
||||||
title="自选股"
|
|
||||||
description="查看你的自选股跟踪、AI 诊断与定时分析结果。"
|
|
||||||
/>
|
|
||||||
<NavCard
|
|
||||||
href="/strategy"
|
|
||||||
title="系统校准"
|
|
||||||
description="看系统近期方法是否有效、哪里失效,以及下一轮该怎么调整。"
|
|
||||||
/>
|
|
||||||
<NavCard
|
|
||||||
href="/chat"
|
|
||||||
title="AI 对话"
|
|
||||||
description="用于追问个股、板块和策略,不承担首页主决策职责。"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-4 rounded-2xl border border-border-subtle bg-surface-1/70 p-3">
|
|
||||||
<div className="flex items-center justify-between gap-3">
|
|
||||||
<span className="text-[11px] font-semibold text-text-secondary">推荐池状态</span>
|
|
||||||
<span className="text-[11px] text-text-muted">{recommendations.length} 只</span>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 grid grid-cols-3 gap-2 text-center">
|
|
||||||
<MiniCount label="可操作" value={actionableCount} tone="text-red-400" />
|
|
||||||
<MiniCount label="重点关注" value={watchCount} tone="text-amber-400" />
|
|
||||||
<MiniCount label="观察" value={observeCount} tone="text-text-secondary" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<span className="rounded-full bg-surface-2 px-2 py-0.5 text-[10px] text-text-muted">
|
||||||
|
{opsStatus.scan_running ? "扫描中" : "空闲"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isAdmin && opsStatus ? (
|
<div className="mt-3 grid grid-cols-2 gap-2 text-[11px] text-text-muted">
|
||||||
<div className="glass-card-static p-4">
|
<FreshnessPill label="市场" value={opsStatus.data_freshness.market_trade_date || "暂无"} />
|
||||||
<div className="flex items-center justify-between gap-3">
|
<FreshnessPill label="板块" value={opsStatus.data_freshness.sector_trade_date || "暂无"} />
|
||||||
<div>
|
<FreshnessPill label="跟踪" value={opsStatus.data_freshness.tracking_trade_date || "暂无"} />
|
||||||
<h3 className="text-sm font-semibold text-text-primary">管理员任务中心</h3>
|
<FreshnessPill label="推荐" value={opsStatus.data_freshness.last_recommendation_created_at || "暂无"} />
|
||||||
<p className="mt-1 text-xs text-text-muted">{opsStatus.data_freshness.message}</p>
|
</div>
|
||||||
</div>
|
|
||||||
<span className="rounded-full bg-surface-2 px-2 py-0.5 text-[10px] text-text-muted">
|
|
||||||
{opsStatus.scan_running ? "扫描中" : "空闲"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-3 grid grid-cols-2 gap-2 text-[11px] text-text-muted">
|
<div className="mt-3 flex flex-wrap gap-2">
|
||||||
<FreshnessPill label="市场" value={opsStatus.data_freshness.market_trade_date || "暂无"} />
|
<button
|
||||||
<FreshnessPill label="板块" value={opsStatus.data_freshness.sector_trade_date || "暂无"} />
|
onClick={onRefresh}
|
||||||
<FreshnessPill label="跟踪" value={opsStatus.data_freshness.tracking_trade_date || "暂无"} />
|
disabled={refreshing || !!opsRunning}
|
||||||
<FreshnessPill label="推荐" value={opsStatus.data_freshness.last_recommendation_created_at || "暂无"} />
|
className="rounded-xl border border-amber-500/15 bg-amber-500/10 px-3 py-2 text-xs text-amber-400 disabled:opacity-40"
|
||||||
</div>
|
>
|
||||||
|
{refreshing ? "扫描中..." : "立即扫描"}
|
||||||
<div className="mt-3 flex flex-wrap gap-2">
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={onRefresh}
|
onClick={() => onAction("update_tracking")}
|
||||||
disabled={refreshing || !!opsRunning}
|
disabled={refreshing || !!opsRunning}
|
||||||
className="rounded-xl border border-amber-500/15 bg-amber-500/10 px-3 py-2 text-xs text-amber-400 disabled:opacity-40"
|
className="rounded-xl border border-border-subtle bg-surface-1 px-3 py-2 text-xs text-text-secondary disabled:opacity-40"
|
||||||
>
|
>
|
||||||
{refreshing ? "扫描中..." : "立即扫描"}
|
{opsRunning === "update_tracking" ? "更新中..." : "更新跟踪"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
</div>
|
||||||
onClick={() => onAction("update_tracking")}
|
|
||||||
disabled={refreshing || !!opsRunning}
|
|
||||||
className="rounded-xl border border-border-subtle bg-surface-1 px-3 py-2 text-xs text-text-secondary disabled:opacity-40"
|
|
||||||
>
|
|
||||||
{opsRunning === "update_tracking" ? "更新中..." : "更新跟踪"}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => onAction("generate_strategy_board")}
|
|
||||||
disabled={refreshing || !!opsRunning}
|
|
||||||
className="rounded-xl border border-border-subtle bg-surface-1 px-3 py-2 text-xs text-text-secondary disabled:opacity-40"
|
|
||||||
>
|
|
||||||
{opsRunning === "generate_strategy_board" ? "生成中..." : "生成策略板"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -530,6 +562,15 @@ function HeroFact({ label, value }: { label: string; value: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CompactBadge({ label, value }: { label: string; value: string }) {
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center gap-2 rounded-xl border border-border-subtle bg-surface-1/70 px-3 py-1.5 text-xs text-text-secondary">
|
||||||
|
<span className="text-text-muted">{label}</span>
|
||||||
|
<span className="font-medium text-text-primary">{value}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ActionBucket({
|
function ActionBucket({
|
||||||
title,
|
title,
|
||||||
items,
|
items,
|
||||||
@ -602,12 +643,12 @@ function FocusStockCard({ rec }: { rec: RecommendationData }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function NavCard({ href, title, description }: { href: string; title: string; description: string }) {
|
function EvidenceStat({ label, value, tone }: { label: string; value: number; tone: string }) {
|
||||||
return (
|
return (
|
||||||
<a href={href} className="rounded-2xl border border-border-subtle bg-surface-1/70 p-3 transition-colors hover:border-amber-500/20">
|
<div className="rounded-2xl border border-border-subtle bg-surface-1/70 px-3 py-3">
|
||||||
<div className="text-sm font-semibold text-text-primary">{title}</div>
|
<div className="text-[10px] uppercase tracking-wider text-text-muted">{label}</div>
|
||||||
<div className="mt-1 text-xs leading-5 text-text-muted">{description}</div>
|
<div className={`mt-1 text-lg font-bold font-mono tabular-nums ${tone}`}>{value}</div>
|
||||||
</a>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -677,6 +718,7 @@ function buildMarketSummary(
|
|||||||
cannotDo,
|
cannotDo,
|
||||||
modeLabel: strategyProfile?.market_stance || board?.recommended_mode || "等待更新",
|
modeLabel: strategyProfile?.market_stance || board?.recommended_mode || "等待更新",
|
||||||
positionLabel: board?.position_suggestion || (strategyProfile?.max_position_pct ? `${strategyProfile.max_position_pct}% 以内` : "等待更新"),
|
positionLabel: board?.position_suggestion || (strategyProfile?.max_position_pct ? `${strategyProfile.max_position_pct}% 以内` : "等待更新"),
|
||||||
|
riskLabel: board?.risk_level || "等待更新",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user