docs(uiux): UIUX 设计方案大改 + 5 份作业指导书对齐 + 开发任务文档

- UIUX 文档:填充 19 个缺口(多主体画像/健康度/AI+看板/增长域/洞察域/创始人端/OODA/助推/商密)
- UIUX 文档:插入 6 个新章节(十四~十九),旧章节重编号为二十~三十一,更新目录和交叉引用
- 作业指导书 x5:导航改为 6 域分组,新增 Context Bar/工作模式/Insight Rail/决策线程/多工作区等 UI 概念
- 新建 docs/2-task-uiux.md:50 个代码落地开发任务,按 P0-P6 分优先级 + 8 Sprint 规划
- 后端/前端:大量新增模型、路由、组件(来自之前 Phase 开发)
This commit is contained in:
selfrelease
2026-07-19 11:53:38 +08:00
parent 734a16a7f3
commit fad458b2a7
243 changed files with 19898 additions and 658 deletions
@@ -0,0 +1,187 @@
"use client";
import { useEffect, useState } from "react";
import { apiFetch } from "@/lib/api";
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
import { EmptyState } from "@/components/shared/EmptyState";
import { FileText, TrendingUp, AlertTriangle, Sparkles } from "lucide-react";
/** 客户成功运营页面 — QBR + 扩展机会 + 流失风险。 */
export default function CustomerSuccessPage() {
const [churnRisks, setChurnRisks] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [qbrData, setQbrData] = useState("");
const [qbrResult, setQbrResult] = useState<any>(null);
const [generating, setGenerating] = useState(false);
const [expansionData, setExpansionData] = useState("");
const [expansionResult, setExpansionResult] = useState<any>(null);
const [expanding, setExpanding] = useState(false);
useEffect(() => {
loadChurnRisks();
}, []);
async function loadChurnRisks() {
try {
const res = await apiFetch<any[]>("/customer-success/churn-risk");
setChurnRisks((res.data as any[]) || []);
} catch {
// ignore
} finally {
setLoading(false);
}
}
async function handleQBR() {
if (!qbrData.trim()) return;
setGenerating(true);
try {
const res = await apiFetch<any>("/customer-success/qbr", {
method: "POST",
body: JSON.stringify({ company_id: "", quarter_data: qbrData }),
});
setQbrResult(res.data);
} catch {
// ignore
} finally {
setGenerating(false);
}
}
async function handleExpansion() {
if (!expansionData.trim()) return;
setExpanding(true);
try {
const res = await apiFetch<any>("/customer-success/expansion", {
method: "POST",
body: JSON.stringify({ company_data: expansionData }),
});
setExpansionResult(res.data);
} catch {
// ignore
} finally {
setExpanding(false);
}
}
if (loading) {
return (
<div className="flex h-96 items-center justify-center">
<LoadingSpinner />
</div>
);
}
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold"></h1>
<p className="mt-1 text-sm text-muted-foreground">QBR · · </p>
</div>
<div className="grid gap-4 lg:grid-cols-2">
{/* QBR 生成 */}
<div className="rounded-lg border border-[var(--border)] bg-white p-4">
<div className="flex items-center gap-2">
<FileText size={18} className="text-[var(--investor-primary)]" />
<h2 className="text-sm font-medium">QBR </h2>
</div>
<textarea
className="mt-2 w-full rounded-md border border-[var(--border)] px-3 py-2 text-sm"
rows={3}
placeholder="输入季度数据:营收、客户数、流失率、关键进展..."
value={qbrData}
onChange={(e) => setQbrData(e.target.value)}
/>
<button
onClick={handleQBR}
disabled={generating || !qbrData.trim()}
className="mt-2 rounded-md bg-[var(--investor-primary)] px-4 py-2 text-sm text-white disabled:opacity-50"
>
{generating ? "生成中..." : "生成 QBR"}
</button>
{qbrResult && (
<div className="mt-3 space-y-2 text-sm">
{qbrResult.summary && <div><span className="text-muted-foreground"></span>{qbrResult.summary}</div>}
{qbrResult.key_metrics && Array.isArray(qbrResult.key_metrics) && (
<div>
<span className="text-muted-foreground"></span>
<ul className="ml-4 list-disc text-xs">
{qbrResult.key_metrics.map((m: string, i: number) => <li key={i}>{m}</li>)}
</ul>
</div>
)}
{qbrResult.next_quarter_recommendations && Array.isArray(qbrResult.next_quarter_recommendations) && (
<div>
<span className="text-muted-foreground"></span>
<ul className="ml-4 list-disc text-xs">
{qbrResult.next_quarter_recommendations.map((r: string, i: number) => <li key={i}>{r}</li>)}
</ul>
</div>
)}
</div>
)}
</div>
{/* 扩展机会 */}
<div className="rounded-lg border border-[var(--border)] bg-white p-4">
<div className="flex items-center gap-2">
<TrendingUp size={18} className="text-emerald-600" />
<h2 className="text-sm font-medium"></h2>
</div>
<textarea
className="mt-2 w-full rounded-md border border-[var(--border)] px-3 py-2 text-sm"
rows={3}
placeholder="描述企业当前产品、市场、客户群..."
value={expansionData}
onChange={(e) => setExpansionData(e.target.value)}
/>
<button
onClick={handleExpansion}
disabled={expanding || !expansionData.trim()}
className="mt-2 rounded-md bg-[var(--investor-primary)] px-4 py-2 text-sm text-white disabled:opacity-50"
>
{expanding ? "分析中..." : "识别扩展机会"}
</button>
{expansionResult && Array.isArray(expansionResult) && (
<div className="mt-3 space-y-2">
{expansionResult.map((opp: any, i: number) => (
<div key={i} className="rounded-md border border-[var(--border)] px-3 py-2 text-sm">
<div className="font-medium">{opp.type || opp.title || `机会 ${i + 1}`}</div>
{opp.description && <div className="mt-1 text-xs text-muted-foreground">{opp.description}</div>}
</div>
))}
</div>
)}
</div>
</div>
{/* 流失风险预警 */}
<div className="rounded-lg border border-[var(--border)] bg-white p-4">
<div className="flex items-center gap-2">
<AlertTriangle size={18} className="text-amber-500" />
<h2 className="text-sm font-medium"> ({churnRisks.length})</h2>
</div>
{churnRisks.length > 0 ? (
<div className="mt-2 space-y-2">
{churnRisks.map((risk, i) => (
<div key={i} className="flex items-center justify-between rounded-md border border-[var(--border)] px-3 py-2">
<div>
<span className="text-sm font-medium">{risk.company_name || risk.company_id || "企业"}</span>
{risk.signals && <p className="text-xs text-muted-foreground">{risk.signals}</p>}
</div>
<span className={`rounded px-2 py-0.5 text-xs ${
risk.risk_level === "high" ? "bg-rose-100 text-rose-700" :
risk.risk_level === "medium" ? "bg-amber-100 text-amber-700" :
"bg-muted text-muted-foreground"
}`}>{risk.risk_level || "low"}</span>
</div>
))}
</div>
) : (
<EmptyState title="暂无流失风险预警" />
)}
</div>
</div>
);
}