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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user