feat(frontend): Phase 6 evaluation template config page + score compare view
This commit is contained in:
@@ -0,0 +1,432 @@
|
||||
/** 评分对比视图 — 多模板/多企业评分并排对比 + 雷达图。 */
|
||||
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { GitCompareArrows, Loader2, TrendingUp, TrendingDown, Minus } from "lucide-react";
|
||||
import {
|
||||
listEvaluationTemplates,
|
||||
listScores,
|
||||
type EvaluationTemplate,
|
||||
type ScoreRecord,
|
||||
} from "@/lib/api-v2";
|
||||
import {
|
||||
DIMENSION_LABELS,
|
||||
DIMENSION_SCORE_KEYS,
|
||||
FUND_TYPE_LABELS,
|
||||
STAGE_LABELS,
|
||||
INDUSTRY_LABELS,
|
||||
getScoreColor,
|
||||
getScoreBgColor,
|
||||
} from "@/lib/evaluation-constants";
|
||||
|
||||
/** 维度 key 列表(14 维度)。 */
|
||||
const ALL_DIMENSIONS = Object.keys(DIMENSION_LABELS);
|
||||
|
||||
/** 趋势图标。 */
|
||||
function TrendIcon({ trend }: { trend: string | null }) {
|
||||
if (trend === "up") return <TrendingUp size={14} className="text-emerald-500" />;
|
||||
if (trend === "down") return <TrendingDown size={14} className="text-rose-500" />;
|
||||
return <Minus size={14} className="text-muted-foreground" />;
|
||||
}
|
||||
|
||||
export default function EvaluationComparePage() {
|
||||
const [templates, setTemplates] = useState<EvaluationTemplate[]>([]);
|
||||
const [selectedTemplateIds, setSelectedTemplateIds] = useState<string[]>([]);
|
||||
const [scores, setScores] = useState<Record<string, ScoreRecord[]>>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [viewMode, setViewMode] = useState<"table" | "radar">("table");
|
||||
|
||||
/** 加载模板列表。 */
|
||||
const loadTemplates = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const resp = await listEvaluationTemplates({ is_default: true });
|
||||
if (resp.data) {
|
||||
setTemplates(resp.data);
|
||||
// 默认选前 2 个
|
||||
if (resp.data.length >= 2) {
|
||||
setSelectedTemplateIds([resp.data[0].id, resp.data[1].id]);
|
||||
} else if (resp.data.length === 1) {
|
||||
setSelectedTemplateIds([resp.data[0].id]);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 静默处理
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadTemplates();
|
||||
}, [loadTemplates]);
|
||||
|
||||
/** 加载选中模板的评分记录。 */
|
||||
useEffect(() => {
|
||||
async function loadScores() {
|
||||
const newScores: Record<string, ScoreRecord[]> = {};
|
||||
for (const tmplId of selectedTemplateIds) {
|
||||
try {
|
||||
const resp = await listScores({ template_id: tmplId, limit: 5 });
|
||||
if (resp.data) newScores[tmplId] = resp.data;
|
||||
} catch {
|
||||
// 静默处理
|
||||
}
|
||||
}
|
||||
setScores(newScores);
|
||||
}
|
||||
if (selectedTemplateIds.length > 0) {
|
||||
loadScores();
|
||||
}
|
||||
}, [selectedTemplateIds]);
|
||||
|
||||
/** 切换模板选择。 */
|
||||
function toggleTemplate(id: string) {
|
||||
setSelectedTemplateIds((prev) => {
|
||||
if (prev.includes(id)) return prev.filter((t) => t !== id);
|
||||
if (prev.length >= 4) return prev;
|
||||
return [...prev, id];
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取模板最新评分。 */
|
||||
function getLatestScore(tmplId: string): ScoreRecord | null {
|
||||
const list = scores[tmplId];
|
||||
if (!list || list.length === 0) return null;
|
||||
return list[0];
|
||||
}
|
||||
|
||||
/** 雷达图坐标计算。 */
|
||||
function getRadarPoints(scoreRecord: ScoreRecord, dims: string[], centerX: number, centerY: number, radius: number): string {
|
||||
return dims
|
||||
.map((dim, i) => {
|
||||
const scoreKey = DIMENSION_SCORE_KEYS[dim];
|
||||
const value = (scoreRecord as unknown as Record<string, number | null>)[scoreKey];
|
||||
const normalized = value ? value / 100 : 0;
|
||||
const angle = (i / dims.length) * 2 * Math.PI - Math.PI / 2;
|
||||
const x = centerX + radius * normalized * Math.cos(angle);
|
||||
const y = centerY + radius * normalized * Math.sin(angle);
|
||||
return `${x},${y}`;
|
||||
})
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
const selectedTemplates = templates.filter((t) => selectedTemplateIds.includes(t.id));
|
||||
const radarSize = 320;
|
||||
const radarCenter = radarSize / 2;
|
||||
const radarRadius = radarSize / 2 - 40;
|
||||
const radarColors = ["#6366f1", "#10b981", "#f59e0b", "#f43f5e"];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 页头 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<GitCompareArrows className="text-[var(--investor-primary)]" size={24} />
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">评分对比</h1>
|
||||
<p className="mt-0.5 text-sm text-muted-foreground">
|
||||
多模板评分并排对比 · 雷达图可视化
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 rounded-md border border-[var(--border)] p-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setViewMode("table")}
|
||||
className={`rounded px-3 py-1 text-sm ${
|
||||
viewMode === "table" ? "bg-[var(--investor-primary)] text-white" : "text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
表格
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setViewMode("radar")}
|
||||
className={`rounded px-3 py-1 text-sm ${
|
||||
viewMode === "radar" ? "bg-[var(--investor-primary)] text-white" : "text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
雷达图
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 模板选择器 */}
|
||||
<div className="rounded-lg border border-[var(--border)] bg-white p-4">
|
||||
<p className="text-xs text-muted-foreground">选择模板进行对比(最多 4 个):</p>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{templates.map((tmpl) => (
|
||||
<button
|
||||
key={tmpl.id}
|
||||
type="button"
|
||||
onClick={() => toggleTemplate(tmpl.id)}
|
||||
className={`rounded-md border px-3 py-1.5 text-xs transition-colors ${
|
||||
selectedTemplateIds.includes(tmpl.id)
|
||||
? "border-indigo-300 bg-indigo-50 text-indigo-600"
|
||||
: "border-gray-200 bg-white text-gray-600 hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
{tmpl.name}
|
||||
</button>
|
||||
))}
|
||||
{templates.length === 0 && !loading && (
|
||||
<span className="text-xs text-muted-foreground">暂无模板,请先在模板配置页创建</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading && (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 size={24} className="animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 对比内容 */}
|
||||
{!loading && selectedTemplates.length > 0 && (
|
||||
<>
|
||||
{viewMode === "table" ? (
|
||||
<div className="rounded-lg border border-[var(--border)] bg-white p-5">
|
||||
<h2 className="text-sm font-medium text-gray-700">维度评分对比</h2>
|
||||
<div className="mt-4 overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-[var(--border)]">
|
||||
<th className="py-2 text-left text-xs font-medium text-muted-foreground">维度</th>
|
||||
{selectedTemplates.map((tmpl, idx) => {
|
||||
const score = getLatestScore(tmpl.id);
|
||||
return (
|
||||
<th key={tmpl.id} className="px-4 py-2 text-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<span
|
||||
className="h-2 w-2 rounded-full"
|
||||
style={{ backgroundColor: radarColors[idx] }}
|
||||
/>
|
||||
<span className="mt-1 text-xs font-medium">{tmpl.name}</span>
|
||||
{score ? (
|
||||
<span className={`mt-0.5 text-lg font-bold ${getScoreColor(score.total_score)}`}>
|
||||
{score.total_score.toFixed(1)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="mt-0.5 text-xs text-muted-foreground">暂无评分</span>
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ALL_DIMENSIONS.map((dim) => {
|
||||
const isEnabled = selectedTemplates.some(
|
||||
(t) => !t.disabled_dimensions.includes(dim),
|
||||
);
|
||||
if (!isEnabled) return null;
|
||||
|
||||
return (
|
||||
<tr key={dim} className="border-b border-[var(--border)] last:border-0">
|
||||
<td className="py-2 text-xs text-muted-foreground">
|
||||
{DIMENSION_LABELS[dim]}
|
||||
</td>
|
||||
{selectedTemplates.map((tmpl) => {
|
||||
const score = getLatestScore(tmpl.id);
|
||||
const isDisabled = tmpl.disabled_dimensions.includes(dim);
|
||||
const scoreKey = DIMENSION_SCORE_KEYS[dim];
|
||||
const value = score
|
||||
? (score as unknown as Record<string, number | null>)[scoreKey]
|
||||
: null;
|
||||
|
||||
return (
|
||||
<td key={tmpl.id} className="px-4 py-2 text-center">
|
||||
{isDisabled ? (
|
||||
<span className="text-xs text-muted-foreground line-through">—</span>
|
||||
) : value !== null && value !== undefined ? (
|
||||
<div className="flex flex-col items-center">
|
||||
<span className={`text-sm font-medium ${getScoreColor(value)}`}>
|
||||
{value.toFixed(1)}
|
||||
</span>
|
||||
<div className="mt-1 h-1.5 w-16 overflow-hidden rounded-full bg-muted">
|
||||
<div
|
||||
className={`h-full rounded-full ${getScoreBgColor(value)}`}
|
||||
style={{ width: `${value}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">—</span>
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 模板信息对比 */}
|
||||
<div className="mt-6 border-t border-[var(--border)] pt-4">
|
||||
<h3 className="text-xs font-medium text-gray-600">模板配置对比</h3>
|
||||
<div className="mt-2 grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||
{selectedTemplates.map((tmpl) => (
|
||||
<div key={tmpl.id} className="rounded-md border border-[var(--border)] p-3">
|
||||
<p className="text-xs font-medium">{tmpl.name}</p>
|
||||
<div className="mt-2 space-y-1 text-[10px] text-muted-foreground">
|
||||
<p>基金类型: {FUND_TYPE_LABELS[tmpl.fund_type] ?? tmpl.fund_type}</p>
|
||||
<p>企业阶段: {STAGE_LABELS[tmpl.company_stage] ?? tmpl.company_stage}</p>
|
||||
<p>产业赛道: {INDUSTRY_LABELS[tmpl.industry] ?? tmpl.industry}</p>
|
||||
<p>启用维度: {tmpl.enabled_dimensions.length} / 14</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* 雷达图视图 */
|
||||
<div className="rounded-lg border border-[var(--border)] bg-white p-5">
|
||||
<h2 className="text-sm font-medium text-gray-700">雷达图对比</h2>
|
||||
|
||||
{/* SVG 雷达图 */}
|
||||
<div className="mt-4 flex flex-col items-center">
|
||||
<svg width={radarSize} height={radarSize} className="max-w-full">
|
||||
{/* 背景网格 */}
|
||||
{[0.25, 0.5, 0.75, 1.0].map((ratio) => (
|
||||
<circle
|
||||
key={ratio}
|
||||
cx={radarCenter}
|
||||
cy={radarCenter}
|
||||
r={radarRadius * ratio}
|
||||
fill="none"
|
||||
stroke="var(--border)"
|
||||
strokeWidth={1}
|
||||
opacity={0.5}
|
||||
/>
|
||||
))}
|
||||
{/* 轴线 */}
|
||||
{ALL_DIMENSIONS.map((dim, i) => {
|
||||
const angle = (i / ALL_DIMENSIONS.length) * 2 * Math.PI - Math.PI / 2;
|
||||
const x = radarCenter + radarRadius * Math.cos(angle);
|
||||
const y = radarCenter + radarRadius * Math.sin(angle);
|
||||
return (
|
||||
<line
|
||||
key={dim}
|
||||
x1={radarCenter}
|
||||
y1={radarCenter}
|
||||
x2={x}
|
||||
y2={y}
|
||||
stroke="var(--border)"
|
||||
strokeWidth={1}
|
||||
opacity={0.3}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{/* 维度标签 */}
|
||||
{ALL_DIMENSIONS.map((dim, i) => {
|
||||
const angle = (i / ALL_DIMENSIONS.length) * 2 * Math.PI - Math.PI / 2;
|
||||
const labelRadius = radarRadius + 20;
|
||||
const x = radarCenter + labelRadius * Math.cos(angle);
|
||||
const y = radarCenter + labelRadius * Math.sin(angle);
|
||||
return (
|
||||
<text
|
||||
key={dim}
|
||||
x={x}
|
||||
y={y}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
className="fill-muted-foreground text-[9px]"
|
||||
>
|
||||
{DIMENSION_LABELS[dim]}
|
||||
</text>
|
||||
);
|
||||
})}
|
||||
{/* 评分多边形 */}
|
||||
{selectedTemplates.map((tmpl, idx) => {
|
||||
const score = getLatestScore(tmpl.id);
|
||||
if (!score) return null;
|
||||
const enabledDims = tmpl.enabled_dimensions;
|
||||
const points = getRadarPoints(
|
||||
score,
|
||||
enabledDims,
|
||||
radarCenter,
|
||||
radarCenter,
|
||||
radarRadius,
|
||||
);
|
||||
return (
|
||||
<polygon
|
||||
key={tmpl.id}
|
||||
points={points}
|
||||
fill={radarColors[idx]}
|
||||
fillOpacity={0.1}
|
||||
stroke={radarColors[idx]}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
|
||||
{/* 图例 */}
|
||||
<div className="mt-4 flex flex-wrap justify-center gap-4">
|
||||
{selectedTemplates.map((tmpl, idx) => {
|
||||
const score = getLatestScore(tmpl.id);
|
||||
return (
|
||||
<div key={tmpl.id} className="flex items-center gap-2">
|
||||
<span
|
||||
className="h-3 w-3 rounded-sm"
|
||||
style={{ backgroundColor: radarColors[idx] }}
|
||||
/>
|
||||
<span className="text-xs">{tmpl.name}</span>
|
||||
{score && (
|
||||
<span className={`text-xs font-medium ${getScoreColor(score.total_score)}`}>
|
||||
{score.total_score.toFixed(1)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 评分历史趋势 */}
|
||||
{Object.values(scores).some((s) => s.length > 1) && (
|
||||
<div className="mt-6 border-t border-[var(--border)] pt-4">
|
||||
<h3 className="text-xs font-medium text-gray-600">评分历史趋势</h3>
|
||||
<div className="mt-2 space-y-2">
|
||||
{selectedTemplates.map((tmpl) => {
|
||||
const tmplScores = scores[tmpl.id];
|
||||
if (!tmplScores || tmplScores.length === 0) return null;
|
||||
return (
|
||||
<div key={tmpl.id} className="flex items-center gap-3">
|
||||
<span className="w-32 truncate text-xs">{tmpl.name}</span>
|
||||
<div className="flex flex-1 items-center gap-2">
|
||||
{tmplScores.slice(0, 5).reverse().map((s, i) => (
|
||||
<div key={s.id} className="flex items-center gap-1">
|
||||
{i > 0 && <TrendIcon trend={s.trend} />}
|
||||
<span className={`text-xs font-medium ${getScoreColor(s.total_score)}`}>
|
||||
{s.total_score.toFixed(1)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!loading && selectedTemplates.length === 0 && templates.length > 0 && (
|
||||
<div className="rounded-lg border border-dashed border-[var(--border)] p-12 text-center">
|
||||
<GitCompareArrows size={32} className="mx-auto text-muted-foreground" />
|
||||
<p className="mt-3 text-sm text-muted-foreground">请选择至少 1 个模板进行对比</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
/** 评价模板配置页 — 6 轴参数选择 + 权重预览 + 模板管理。 */
|
||||
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Settings2, Plus, ChevronDown, ChevronRight, Loader2, Save, BarChart3 } from "lucide-react";
|
||||
import {
|
||||
computeWeights,
|
||||
listEvaluationTemplates,
|
||||
createEvaluationTemplate,
|
||||
type EvaluationTemplate,
|
||||
} from "@/lib/api-v2";
|
||||
import { DIMENSION_LABELS } from "@/lib/evaluation-constants";
|
||||
|
||||
/** 6 轴选项定义。 */
|
||||
const AXIS_OPTIONS = {
|
||||
fund_type: [
|
||||
{ value: "angel", label: "天使/种子基金" },
|
||||
{ value: "early_vc", label: "早期VC" },
|
||||
{ value: "growth_vc", label: "成长期VC" },
|
||||
{ value: "pe", label: "PE/并购基金" },
|
||||
{ value: "cvc", label: "产业基金" },
|
||||
{ value: "fof", label: "母基金" },
|
||||
{ value: "distress", label: "困境/特殊机会" },
|
||||
{ value: "esg", label: "ESG/影响力" },
|
||||
],
|
||||
fund_lifecycle: [
|
||||
{ value: "investment", label: "投资期" },
|
||||
{ value: "growth", label: "成长期" },
|
||||
{ value: "exit_preparation", label: "退出准备期" },
|
||||
{ value: "liquidation", label: "清算期" },
|
||||
],
|
||||
company_stage: [
|
||||
{ value: "seed", label: "种子/天使" },
|
||||
{ value: "a", label: "A轮" },
|
||||
{ value: "b", label: "B轮" },
|
||||
{ value: "c", label: "C轮+" },
|
||||
{ value: "pre_ipo", label: "Pre-IPO" },
|
||||
],
|
||||
industry: [
|
||||
{ value: "ai", label: "AI/SaaS" },
|
||||
{ value: "saas", label: "企业服务" },
|
||||
{ value: "hardware", label: "硬科技/芯片" },
|
||||
{ value: "biotech", label: "生物医药" },
|
||||
{ value: "consumer", label: "消费品牌" },
|
||||
{ value: "fintech", label: "金融科技" },
|
||||
{ value: "manufacturing", label: "新能源/先进制造" },
|
||||
],
|
||||
strategy: [
|
||||
{ value: "growth", label: "成长型" },
|
||||
{ value: "value", label: "价值型" },
|
||||
{ value: "empowerment", label: "投后赋能型" },
|
||||
{ value: "turnaround", label: "困境反转型" },
|
||||
],
|
||||
investor_type: [
|
||||
{ value: "gp", label: "GP/合伙人" },
|
||||
{ value: "post_invest_lead", label: "投后负责人" },
|
||||
{ value: "investor", label: "投资经理" },
|
||||
],
|
||||
} as const;
|
||||
|
||||
/** 轴标签。 */
|
||||
const AXIS_LABELS: Record<string, string> = {
|
||||
fund_type: "基金类型",
|
||||
fund_lifecycle: "存续期阶段",
|
||||
company_stage: "企业阶段",
|
||||
industry: "产业赛道",
|
||||
strategy: "投资策略",
|
||||
investor_type: "投资人类型",
|
||||
};
|
||||
|
||||
export default function EvaluationTemplatesPage() {
|
||||
const [params, setParams] = useState({
|
||||
fund_type: "early_vc",
|
||||
fund_lifecycle: "investment",
|
||||
company_stage: "a",
|
||||
industry: "ai",
|
||||
strategy: "growth",
|
||||
investor_type: "investor",
|
||||
});
|
||||
|
||||
const [weights, setWeights] = useState<Record<string, number> | null>(null);
|
||||
const [enabledDims, setEnabledDims] = useState<string[]>([]);
|
||||
const [disabledDims, setDisabledDims] = useState<string[]>([]);
|
||||
const [customMetrics, setCustomMetrics] = useState<Array<{ key: string; label: string; description: string }>>([]);
|
||||
const [computing, setComputing] = useState(false);
|
||||
const [showDisabled, setShowDisabled] = useState(false);
|
||||
|
||||
const [templates, setTemplates] = useState<EvaluationTemplate[]>([]);
|
||||
const [loadingTemplates, setLoadingTemplates] = useState(false);
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||
const [newTemplateName, setNewTemplateName] = useState("");
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
/** 实时计算权重。 */
|
||||
const doCompute = useCallback(async () => {
|
||||
setComputing(true);
|
||||
setError("");
|
||||
try {
|
||||
const resp = await computeWeights(params);
|
||||
const data = resp.data;
|
||||
if (data) {
|
||||
setWeights(data.weights);
|
||||
setEnabledDims(data.enabled_dimensions);
|
||||
setDisabledDims(data.disabled_dimensions);
|
||||
setCustomMetrics(data.custom_metrics);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "权重计算失败");
|
||||
} finally {
|
||||
setComputing(false);
|
||||
}
|
||||
}, [params]);
|
||||
|
||||
useEffect(() => {
|
||||
doCompute();
|
||||
}, [doCompute]);
|
||||
|
||||
/** 加载模板列表。 */
|
||||
const loadTemplates = useCallback(async () => {
|
||||
setLoadingTemplates(true);
|
||||
try {
|
||||
const resp = await listEvaluationTemplates({
|
||||
fund_type: params.fund_type,
|
||||
company_stage: params.company_stage,
|
||||
industry: params.industry,
|
||||
});
|
||||
if (resp.data) setTemplates(resp.data);
|
||||
} catch {
|
||||
// 静默处理
|
||||
} finally {
|
||||
setLoadingTemplates(false);
|
||||
}
|
||||
}, [params.fund_type, params.company_stage, params.industry]);
|
||||
|
||||
useEffect(() => {
|
||||
loadTemplates();
|
||||
}, [loadTemplates]);
|
||||
|
||||
/** 创建模板。 */
|
||||
async function handleCreateTemplate() {
|
||||
if (!newTemplateName.trim()) {
|
||||
setError("请输入模板名称");
|
||||
return;
|
||||
}
|
||||
setCreating(true);
|
||||
setError("");
|
||||
try {
|
||||
await createEvaluationTemplate({
|
||||
name: newTemplateName,
|
||||
...params,
|
||||
});
|
||||
setShowCreateDialog(false);
|
||||
setNewTemplateName("");
|
||||
await loadTemplates();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "创建失败");
|
||||
} finally {
|
||||
setCreating(false);
|
||||
}
|
||||
}
|
||||
|
||||
const totalWeight = weights ? Object.values(weights).reduce((a, b) => a + b, 0) : 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 页头 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings2 className="text-[var(--investor-primary)]" size={24} />
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">评价模板配置</h1>
|
||||
<p className="mt-0.5 text-sm text-muted-foreground">
|
||||
6 轴动态权重 · 50 个预设 · 自定义模板
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowCreateDialog(true)}
|
||||
className="flex items-center gap-1.5 rounded-md bg-[var(--investor-primary)] px-4 py-2 text-sm text-white hover:opacity-90"
|
||||
>
|
||||
<Plus size={16} />
|
||||
新建模板
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 6 轴参数选择器 */}
|
||||
<div className="rounded-lg border border-[var(--border)] bg-white p-5">
|
||||
<h2 className="text-sm font-medium text-gray-700">6 轴参数配置</h2>
|
||||
<div className="mt-4 grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-6">
|
||||
{Object.entries(AXIS_OPTIONS).map(([key, options]) => (
|
||||
<div key={key}>
|
||||
<label className="text-xs text-muted-foreground">{AXIS_LABELS[key]}</label>
|
||||
<select
|
||||
className="mt-1 w-full rounded-md border border-[var(--border)] px-3 py-2 text-sm focus:border-indigo-400 focus:outline-none"
|
||||
value={params[key as keyof typeof params]}
|
||||
onChange={(e) =>
|
||||
setParams((prev) => ({ ...prev, [key]: e.target.value }))
|
||||
}
|
||||
>
|
||||
{options.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 权重预览 */}
|
||||
<div className="rounded-lg border border-[var(--border)] bg-white p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<BarChart3 size={18} className="text-[var(--investor-primary)]" />
|
||||
<h2 className="text-sm font-medium text-gray-700">权重预览</h2>
|
||||
{computing && <Loader2 size={14} className="animate-spin text-muted-foreground" />}
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
总权重: <span className="font-medium text-gray-700">{totalWeight.toFixed(2)}%</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mt-3 rounded-md border border-rose-200 bg-rose-50 p-2 text-xs text-rose-600">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 权重条形图 */}
|
||||
<div className="mt-4 space-y-2">
|
||||
{weights &&
|
||||
Object.entries(weights)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.map(([dim, weight]) => (
|
||||
<div key={dim} className="flex items-center gap-3">
|
||||
<span className="w-24 text-xs text-muted-foreground">
|
||||
{DIMENSION_LABELS[dim] ?? dim}
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<div className="h-5 overflow-hidden rounded bg-muted">
|
||||
<div
|
||||
className="flex h-full items-center justify-end rounded bg-indigo-500 px-1 text-[10px] text-white transition-all"
|
||||
style={{ width: `${Math.min(weight * 3, 100)}%` }}
|
||||
>
|
||||
{weight > 3 && `${weight.toFixed(1)}%`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="w-12 text-right text-xs font-medium">
|
||||
{weight.toFixed(2)}%
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 禁用维度 */}
|
||||
{disabledDims.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowDisabled(!showDisabled)}
|
||||
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-gray-700"
|
||||
>
|
||||
{showDisabled ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
禁用维度 ({disabledDims.length})
|
||||
</button>
|
||||
{showDisabled && (
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{disabledDims.map((dim) => (
|
||||
<span
|
||||
key={dim}
|
||||
className="rounded bg-muted px-2 py-1 text-xs text-muted-foreground line-through"
|
||||
>
|
||||
{DIMENSION_LABELS[dim] ?? dim}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 专属指标 */}
|
||||
{customMetrics.length > 0 && (
|
||||
<div className="mt-4 border-t border-[var(--border)] pt-3">
|
||||
<h3 className="text-xs font-medium text-gray-600">赛道专属指标</h3>
|
||||
<div className="mt-2 grid grid-cols-2 gap-2 md:grid-cols-4">
|
||||
{customMetrics.map((metric) => (
|
||||
<div
|
||||
key={metric.key}
|
||||
className="rounded-md border border-[var(--border)] p-2"
|
||||
>
|
||||
<p className="text-xs font-medium">{metric.label}</p>
|
||||
<p className="mt-0.5 text-[10px] text-muted-foreground">{metric.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 模板列表 */}
|
||||
<div className="rounded-lg border border-[var(--border)] bg-white p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-sm font-medium text-gray-700">
|
||||
匹配模板 ({templates.length})
|
||||
</h2>
|
||||
{loadingTemplates && <Loader2 size={14} className="animate-spin text-muted-foreground" />}
|
||||
</div>
|
||||
|
||||
{templates.length === 0 && !loadingTemplates ? (
|
||||
<div className="mt-4 rounded-md border border-dashed border-[var(--border)] p-6 text-center">
|
||||
<p className="text-sm text-muted-foreground">当前筛选条件下暂无模板</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">可点击右上角"新建模板"创建</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 space-y-2">
|
||||
{templates.map((tmpl) => (
|
||||
<div
|
||||
key={tmpl.id}
|
||||
className="flex items-center justify-between rounded-md border border-[var(--border)] p-3 hover:bg-muted/50"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium">{tmpl.name}</p>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||
{AXIS_OPTIONS.fund_type.find((o) => o.value === tmpl.fund_type)?.label} ·{" "}
|
||||
{AXIS_OPTIONS.company_stage.find((o) => o.value === tmpl.company_stage)?.label} ·{" "}
|
||||
{AXIS_OPTIONS.industry.find((o) => o.value === tmpl.industry)?.label}
|
||||
</p>
|
||||
</div>
|
||||
{tmpl.is_default && (
|
||||
<span className="rounded bg-indigo-50 px-2 py-0.5 text-xs text-indigo-600">
|
||||
默认
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{Object.keys(tmpl.weights).length} 维度
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">v{tmpl.version}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 创建模板对话框 */}
|
||||
{showCreateDialog && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
|
||||
onClick={() => setShowCreateDialog(false)}
|
||||
>
|
||||
<div
|
||||
className="w-96 rounded-lg bg-white p-5 shadow-lg"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h3 className="text-sm font-medium">新建评价模板</h3>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
基于当前 6 轴参数配置创建,权重将自动计算
|
||||
</p>
|
||||
<div className="mt-3">
|
||||
<label className="text-xs text-muted-foreground">模板名称</label>
|
||||
<input
|
||||
type="text"
|
||||
className="mt-1 w-full rounded-md border border-[var(--border)] px-3 py-2 text-sm"
|
||||
placeholder="如:早期VC-AI-成长型"
|
||||
value={newTemplateName}
|
||||
onChange={(e) => setNewTemplateName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleCreateTemplate();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3 rounded-md bg-muted p-2 text-xs text-muted-foreground">
|
||||
基金类型: {AXIS_OPTIONS.fund_type.find((o) => o.value === params.fund_type)?.label}
|
||||
{" · "}
|
||||
阶段: {AXIS_OPTIONS.company_stage.find((o) => o.value === params.company_stage)?.label}
|
||||
{" · "}
|
||||
赛道: {AXIS_OPTIONS.industry.find((o) => o.value === params.industry)?.label}
|
||||
</div>
|
||||
{error && (
|
||||
<p className="mt-2 text-xs text-rose-600">{error}</p>
|
||||
)}
|
||||
<div className="mt-4 flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowCreateDialog(false)}
|
||||
className="rounded-md border border-[var(--border)] px-4 py-2 text-sm hover:bg-muted"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCreateTemplate}
|
||||
disabled={creating}
|
||||
className="flex items-center gap-1.5 rounded-md bg-[var(--investor-primary)] px-4 py-2 text-sm text-white hover:opacity-90 disabled:opacity-50"
|
||||
>
|
||||
{creating ? <Loader2 size={14} className="animate-spin" /> : <Save size={14} />}
|
||||
创建
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -531,3 +531,194 @@ export async function founderOverview() {
|
||||
export async function founderHealth() {
|
||||
return apiFetch(`/founder/health`);
|
||||
}
|
||||
|
||||
// ============ 评价指标体系 ============
|
||||
|
||||
/** 权重计算请求参数。 */
|
||||
export interface WeightComputeParams {
|
||||
fund_type: string;
|
||||
fund_lifecycle?: string;
|
||||
company_stage?: string;
|
||||
industry?: string;
|
||||
strategy?: string;
|
||||
investor_type?: string;
|
||||
}
|
||||
|
||||
/** 评价模板。 */
|
||||
export interface EvaluationTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
fund_type: string;
|
||||
fund_lifecycle: string;
|
||||
company_stage: string;
|
||||
industry: string;
|
||||
strategy: string;
|
||||
investor_type: string;
|
||||
weights: Record<string, number>;
|
||||
enabled_dimensions: string[];
|
||||
disabled_dimensions: string[];
|
||||
custom_metrics: { metrics: Array<{ key: string; label: string; description: string }> } | null;
|
||||
is_default: boolean;
|
||||
version: number;
|
||||
}
|
||||
|
||||
/** 评分记录。 */
|
||||
export interface ScoreRecord {
|
||||
id: string;
|
||||
company_id: string;
|
||||
total_score: number;
|
||||
financial_score: number | null;
|
||||
operational_score: number | null;
|
||||
ai_commercial_score: number | null;
|
||||
ai_cost_score: number | null;
|
||||
org_talent_score: number | null;
|
||||
product_tech_score: number | null;
|
||||
market_compete_score: number | null;
|
||||
governance_score: number | null;
|
||||
financing_score: number | null;
|
||||
synergy_score: number | null;
|
||||
ai_model_product_score: number | null;
|
||||
data_compliance_score: number | null;
|
||||
team_tech_score: number | null;
|
||||
customer_success_score: number | null;
|
||||
trend: string | null;
|
||||
template_id: string | null;
|
||||
fund_type: string | null;
|
||||
fund_lifecycle: string | null;
|
||||
company_stage: string | null;
|
||||
industry: string | null;
|
||||
strategy: string | null;
|
||||
calculated_at: string | null;
|
||||
}
|
||||
|
||||
/** 基金信息。 */
|
||||
export interface FundInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
fund_type: string;
|
||||
strategy: string;
|
||||
established_date: string | null;
|
||||
total_lifespan_months: number;
|
||||
investment_period_months: number;
|
||||
current_lifecycle: string;
|
||||
lp_composition: Record<string, number> | null;
|
||||
primary_market: string | null;
|
||||
}
|
||||
|
||||
/** 计算权重(不持久化)。 */
|
||||
export async function computeWeights(params: WeightComputeParams) {
|
||||
return apiFetch<{
|
||||
weights: Record<string, number>;
|
||||
enabled_dimensions: string[];
|
||||
disabled_dimensions: string[];
|
||||
custom_metrics: Array<{ key: string; label: string; description: string }>;
|
||||
}>("/evaluation/weights/compute", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
fund_type: params.fund_type,
|
||||
fund_lifecycle: params.fund_lifecycle ?? "investment",
|
||||
company_stage: params.company_stage ?? "a",
|
||||
industry: params.industry ?? "ai",
|
||||
strategy: params.strategy ?? "growth",
|
||||
investor_type: params.investor_type ?? "investor",
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取模板列表。 */
|
||||
export async function listEvaluationTemplates(params?: {
|
||||
fund_type?: string;
|
||||
fund_lifecycle?: string;
|
||||
company_stage?: string;
|
||||
industry?: string;
|
||||
strategy?: string;
|
||||
is_default?: boolean;
|
||||
}) {
|
||||
const query = new URLSearchParams();
|
||||
if (params?.fund_type) query.set("fund_type", params.fund_type);
|
||||
if (params?.fund_lifecycle) query.set("fund_lifecycle", params.fund_lifecycle);
|
||||
if (params?.company_stage) query.set("company_stage", params.company_stage);
|
||||
if (params?.industry) query.set("industry", params.industry);
|
||||
if (params?.strategy) query.set("strategy", params.strategy);
|
||||
if (params?.is_default !== undefined) query.set("is_default", String(params.is_default));
|
||||
return apiFetch<EvaluationTemplate[]>(`/evaluation/templates?${query.toString()}`);
|
||||
}
|
||||
|
||||
/** 获取模板详情。 */
|
||||
export async function getEvaluationTemplate(id: string) {
|
||||
return apiFetch<EvaluationTemplate>(`/evaluation/templates/${id}`);
|
||||
}
|
||||
|
||||
/** 创建模板。 */
|
||||
export async function createEvaluationTemplate(data: {
|
||||
name: string;
|
||||
fund_type: string;
|
||||
fund_lifecycle?: string;
|
||||
company_stage?: string;
|
||||
industry?: string;
|
||||
strategy?: string;
|
||||
investor_type?: string;
|
||||
weights_json?: Record<string, number>;
|
||||
is_default?: boolean;
|
||||
}) {
|
||||
return apiFetch<{ id: string; name: string; weights: Record<string, number> }>(
|
||||
"/evaluation/templates",
|
||||
{ method: "POST", body: JSON.stringify(data) },
|
||||
);
|
||||
}
|
||||
|
||||
/** 计算评分。 */
|
||||
export async function calculateScore(data: {
|
||||
company_id: string;
|
||||
template_id?: string;
|
||||
fund_type?: string;
|
||||
fund_lifecycle?: string;
|
||||
company_stage?: string;
|
||||
industry?: string;
|
||||
strategy?: string;
|
||||
structured_data?: Record<string, unknown>;
|
||||
}) {
|
||||
return apiFetch<{
|
||||
score_id?: string;
|
||||
total_score: number;
|
||||
dimension_scores: Record<string, number>;
|
||||
template: { id: string; name: string; weights: Record<string, number> } | null;
|
||||
}>("/evaluation/score", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取评分历史。 */
|
||||
export async function listScores(params?: {
|
||||
company_id?: string;
|
||||
template_id?: string;
|
||||
limit?: number;
|
||||
}) {
|
||||
const query = new URLSearchParams();
|
||||
if (params?.company_id) query.set("company_id", params.company_id);
|
||||
if (params?.template_id) query.set("template_id", params.template_id);
|
||||
if (params?.limit) query.set("limit", String(params.limit));
|
||||
return apiFetch<ScoreRecord[]>(`/evaluation/scores?${query.toString()}`);
|
||||
}
|
||||
|
||||
/** 获取基金列表(评价体系)。 */
|
||||
export async function listEvaluationFunds() {
|
||||
return apiFetch<FundInfo[]>("/evaluation/funds");
|
||||
}
|
||||
|
||||
/** 创建基金(评价体系)。 */
|
||||
export async function createEvaluationFund(data: {
|
||||
name: string;
|
||||
fund_type: string;
|
||||
strategy?: string;
|
||||
established_date?: string;
|
||||
total_lifespan_months?: number;
|
||||
investment_period_months?: number;
|
||||
primary_market?: string;
|
||||
}) {
|
||||
return apiFetch<{ id: string; name: string; current_lifecycle: string }>(
|
||||
"/evaluation/funds",
|
||||
{ method: "POST", body: JSON.stringify(data) },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
/** 评价体系共享常量 — 维度标签、轴标签等。 */
|
||||
|
||||
/** 14 维度中文标签。 */
|
||||
export const DIMENSION_LABELS: Record<string, string> = {
|
||||
financial: "财务",
|
||||
operational: "经营",
|
||||
ai_commercial: "AI商业化",
|
||||
ai_cost: "AI成本",
|
||||
org_talent: "组织人才",
|
||||
product_tech: "产品技术",
|
||||
market_compete: "市场竞争",
|
||||
governance: "治理合规",
|
||||
financing: "融资资本",
|
||||
synergy: "协同赋能",
|
||||
ai_model_product: "AI模型产品",
|
||||
data_compliance: "数据合规",
|
||||
team_tech: "团队技术",
|
||||
customer_success: "客户成功",
|
||||
};
|
||||
|
||||
/** 维度 key → 评分字段 key 映射。 */
|
||||
export const DIMENSION_SCORE_KEYS: Record<string, string> = {
|
||||
financial: "financial_score",
|
||||
operational: "operational_score",
|
||||
ai_commercial: "ai_commercial_score",
|
||||
ai_cost: "ai_cost_score",
|
||||
org_talent: "org_talent_score",
|
||||
product_tech: "product_tech_score",
|
||||
market_compete: "market_compete_score",
|
||||
governance: "governance_score",
|
||||
financing: "financing_score",
|
||||
synergy: "synergy_score",
|
||||
ai_model_product: "ai_model_product_score",
|
||||
data_compliance: "data_compliance_score",
|
||||
team_tech: "team_tech_score",
|
||||
customer_success: "customer_success_score",
|
||||
};
|
||||
|
||||
/** 基金类型标签。 */
|
||||
export const FUND_TYPE_LABELS: Record<string, string> = {
|
||||
angel: "天使/种子基金",
|
||||
early_vc: "早期VC",
|
||||
growth_vc: "成长期VC",
|
||||
pe: "PE/并购基金",
|
||||
cvc: "产业基金",
|
||||
fof: "母基金",
|
||||
distress: "困境/特殊机会",
|
||||
esg: "ESG/影响力",
|
||||
};
|
||||
|
||||
/** 存续期标签。 */
|
||||
export const LIFECYCLE_LABELS: Record<string, string> = {
|
||||
investment: "投资期",
|
||||
growth: "成长期",
|
||||
exit_preparation: "退出准备期",
|
||||
liquidation: "清算期",
|
||||
};
|
||||
|
||||
/** 企业阶段标签。 */
|
||||
export const STAGE_LABELS: Record<string, string> = {
|
||||
seed: "种子/天使",
|
||||
a: "A轮",
|
||||
b: "B轮",
|
||||
c: "C轮+",
|
||||
pre_ipo: "Pre-IPO",
|
||||
};
|
||||
|
||||
/** 产业赛道标签。 */
|
||||
export const INDUSTRY_LABELS: Record<string, string> = {
|
||||
ai: "AI/SaaS",
|
||||
saas: "企业服务",
|
||||
hardware: "硬科技/芯片",
|
||||
biotech: "生物医药",
|
||||
consumer: "消费品牌",
|
||||
fintech: "金融科技",
|
||||
manufacturing: "新能源/先进制造",
|
||||
};
|
||||
|
||||
/** 投资策略标签。 */
|
||||
export const STRATEGY_LABELS: Record<string, string> = {
|
||||
growth: "成长型",
|
||||
value: "价值型",
|
||||
empowerment: "投后赋能型",
|
||||
turnaround: "困境反转型",
|
||||
};
|
||||
|
||||
/** 评分颜色等级。 */
|
||||
export function getScoreColor(score: number): string {
|
||||
if (score >= 80) return "text-emerald-600";
|
||||
if (score >= 60) return "text-indigo-600";
|
||||
if (score >= 40) return "text-amber-600";
|
||||
return "text-rose-600";
|
||||
}
|
||||
|
||||
/** 评分背景颜色。 */
|
||||
export function getScoreBgColor(score: number): string {
|
||||
if (score >= 80) return "bg-emerald-500";
|
||||
if (score >= 60) return "bg-indigo-500";
|
||||
if (score >= 40) return "bg-amber-500";
|
||||
return "bg-rose-500";
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
AlertTriangle, FileText, Receipt, ScrollText, Users, Flag, ListTodo,
|
||||
Network, UserCircle, TrendingUp, Target, GitBranch, Lightbulb,
|
||||
Radar, Crosshair, TrendingUp as TrendUp, LogOut, Bot, Cpu, Network as Net,
|
||||
BookOpen, ShieldCheck, FileBarChart, Settings,
|
||||
BookOpen, ShieldCheck, FileBarChart, Settings, Settings2,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
@@ -99,6 +99,8 @@ export const NAV_DOMAINS: Record<string, NavDomain> = {
|
||||
{ href: "/aars", label: "AAR 复盘", icon: BookOpen },
|
||||
{ href: "/pre-mortems", label: "Pre-mortem", icon: ShieldCheck },
|
||||
{ href: "/reports/templates", label: "投后报告", icon: FileBarChart },
|
||||
{ href: "/evaluation", label: "评价模板", icon: Settings2 },
|
||||
{ href: "/evaluation/compare", label: "评分对比", icon: GitCompareArrows },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user