有效路线
@@ -564,6 +658,81 @@ function PerformanceReviewCard({
);
}
+function ResearchEffectivenessPanel({ review }: { review: ResearchReviewStats }) {
+ const blocks = [
+ { title: "有效主题", items: review.theme_breakdown.slice(0, 3) },
+ { title: "有效环节", items: review.chain_breakdown.slice(0, 3) },
+ { title: "有效信号", items: review.signal_breakdown.slice(0, 3).map((item) => ({ ...item, label: formatSignalLabel(item.label) })) },
+ { title: "风险影响", items: review.risk_breakdown.slice(0, 3), risk: true },
+ ];
+
+ return (
+
+
+
+
Research Review
+
{review.summary.headline}
+
+ {review.tracked_count} 个跟踪样本 / {review.sample_count} 个研究样本
+
+
+
+
+
+
+ {blocks.map((block) => (
+
+ ))}
+
+
+
+ {review.summary.suggestions.slice(0, 4).map((item, index) => (
+
+ {item}
+
+ ))}
+
+
+ );
+}
+
+function ReviewBreakdownBlock({
+ title,
+ items,
+ risk,
+}: {
+ title: string;
+ items: ResearchReviewStats["theme_breakdown"];
+ risk?: boolean;
+}) {
+ return (
+
+
{title}
+
+ {items.length ? items.map((item) => (
+
+
+
{item.label || "未归类"}
+
= 0 && !risk ? "text-red-400" : item.avg_return < 0 ? "text-emerald-400" : "text-amber-400"}`}>
+ {formatSigned(item.avg_return)}
+
+
+
+ {item.tracked_count}/{item.sample_count} 样本
+ 胜率 {item.win_rate}%
+
+
+ )) : (
+
暂无样本
+ )}
+
+
+ );
+}
+
function ReviewRow({ item }: { item: TrackedRecommendation }) {
const pct = item.pct_from_entry ?? 0;
const pctTone = pct >= 0 ? "text-red-400" : "text-emerald-400";
@@ -626,6 +795,19 @@ function formatCloseReason(reason?: string) {
return labels[reason ?? ""] ?? "跟踪中";
}
+function formatSignalLabel(signal?: string) {
+ const labels: Record
= {
+ breakout: "突破",
+ breakout_confirm: "突破确认",
+ pullback: "回踩",
+ launch: "启动",
+ reversal: "反转",
+ flow_momentum: "资金动量",
+ none: "未归类",
+ };
+ return labels[signal ?? ""] ?? signal ?? "未归类";
+}
+
function formatNumber(value: number | null | undefined) {
return value == null ? "-" : Number(value).toFixed(2);
}
diff --git a/frontend/src/app/(auth)/sectors/page.tsx b/frontend/src/app/(auth)/sectors/page.tsx
index 7ac909b4..65ecafd5 100644
--- a/frontend/src/app/(auth)/sectors/page.tsx
+++ b/frontend/src/app/(auth)/sectors/page.tsx
@@ -2,7 +2,7 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { fetchAPI } from "@/lib/api";
-import type { LeadingStock, SectorData } from "@/lib/api";
+import type { LeadingStock, ResearchReport, SectorData, ThemeChainItem } from "@/lib/api";
import { formatNumber } from "@/lib/utils";
import { ErrorBoundary } from "@/components/error-boundary";
import { useWebSocket } from "@/hooks/use-websocket";
@@ -365,14 +365,168 @@ function MetricBox({ label, children }: { label: string; children: React.ReactNo
);
}
+function IndustryChainMatrix({ themes }: { themes: ResearchReport["theme_views"] }) {
+ const nodeCount = themes.reduce((sum, theme) => sum + normalizedChainItems(theme).length, 0);
+ const mappedStockCount = themes.reduce(
+ (sum, theme) => sum + normalizedChainItems(theme).reduce((nodeSum, node) => nodeSum + node.leader_stocks.length + node.related_stocks.length, 0),
+ 0
+ );
+
+ return (
+
+
+
+
Industry Chain
+
产业链扩散路径
+
从主题到环节,再到核心股和相关股。
+
+
+
+
+
+
+
+
+
+ {themes.map((theme) => (
+
+ ))}
+
+
+ );
+}
+
+function ThemeChainRow({ theme }: { theme: ResearchReport["theme_views"][number] }) {
+ const chainItems = normalizedChainItems(theme);
+
+ return (
+
+
+
+
+
{theme.theme}
+
+ {theme.lifecycle_status || theme.stage} · {theme.raw_sector || theme.theme}
+
+
+
+ {Math.round(theme.heat_score)}
+
+
+
{theme.logic}
+
+
+
+ {chainItems.slice(0, 9).map((item) => (
+
+ ))}
+
+
+ );
+}
+
+function ChainNodeBlock({ item }: { item: ThemeChainItem }) {
+ const leaders = item.leader_stocks.slice(0, 4);
+ const related = item.related_stocks.slice(0, 4);
+ const hasStocks = leaders.length || related.length;
+
+ return (
+
+
+
{item.chain_node}
+ {item.node_role ? (
+
+ {item.node_role}
+
+ ) : null}
+
+ {hasStocks ? (
+
+ {leaders.length ? : null}
+ {related.length ? : null}
+
+ ) : (
+
+ 待维护标的
+
+ )}
+
+ );
+}
+
+function StockStrip({
+ label,
+ stocks,
+ tone,
+}: {
+ label: string;
+ stocks: ThemeChainItem["leader_stocks"];
+ tone: "core" | "related";
+}) {
+ return (
+
+
{label}
+
+ {stocks.map((stock, index) => )}
+
+
+ );
+}
+
+function StockTag({ stock, tone }: { stock: string | { name?: string; ts_code?: string }; tone: "core" | "related" }) {
+ const label = stockLabel(stock);
+ const code = typeof stock === "string" ? "" : stock.ts_code || "";
+ const className = tone === "core"
+ ? "border-red-500/15 bg-red-500/[0.06] text-red-200"
+ : "border-border-subtle bg-surface-2/70 text-text-secondary";
+ if (code) {
+ return (
+
+ {label}
+
+ );
+ }
+ return {label};
+}
+
+function MatrixMetric({ label, value }: { label: string; value: number }) {
+ return (
+
+ );
+}
+
+function normalizedChainItems(theme: ResearchReport["theme_views"][number]): ThemeChainItem[] {
+ const explicitItems = (theme.chain_items ?? []).filter((item) => item.chain_node);
+ if (explicitItems.length) return explicitItems;
+ return theme.chain_nodes.map((node) => ({
+ chain_node: node,
+ leader_stocks: [],
+ related_stocks: [],
+ node_role: "",
+ }));
+}
+
+function stockLabel(stock: string | { name?: string; ts_code?: string }) {
+ if (typeof stock === "string") return stock;
+ return stock.name || stock.ts_code || "-";
+}
+
export default function SectorsPage() {
const [sectors, setSectors] = useState([]);
+ const [research, setResearch] = useState(null);
const [stageFilter, setStageFilter] = useState("all");
const loadData = useCallback(async () => {
try {
- const data = await fetchAPI("/api/sectors/hot?limit=20");
+ const [data, researchData] = await Promise.all([
+ fetchAPI("/api/sectors/hot?limit=20"),
+ fetchAPI("/api/research/today").catch(() => null),
+ ]);
setSectors(data);
+ setResearch(researchData);
} catch {
// ignore
}
@@ -416,7 +570,8 @@ export default function SectorsPage() {
-
主线主题
+
主题图谱
+
从板块强度升级到主题、产业链环节和前排公司。
{!sectors.length ? (
@@ -447,6 +602,8 @@ export default function SectorsPage() {