test(frontend): UIUX E2E 测试 — 新增路由导航/创始人端/Admin端/工作台/移动端适配

新增 6 个 E2E 测试文件,覆盖 2-task-uiux.md 全部 50 项任务:
- uiux-navigation.spec.ts: 8 tests (today/compare/threads/workspace/ooda/ai-plus/profiles)
- sidebar-navigation.spec.ts: 6 tests (投资人 Sidebar 6 业务域)
- founder-uiux.spec.ts: 7 tests (创始人端 6 域导航)
- admin-uiux.spec.ts: 3 tests (Admin 6 管理域 + 商业秘密保护)
- workbench-uiux.spec.ts: 5 tests (Highlights/WorkMode/Tab/InsightRail)
- mobile-uiux.spec.ts: 5 tests (移动端抽屉导航 + 创始人底部导航)

同时修复全部 no-explicit-any 警告,替换为 TypeScript 接口定义。

测试结果: 41 E2E passed, 405 backend passed
This commit is contained in:
selfrelease
2026-07-19 20:37:25 +08:00
parent 3a905da35b
commit f4ddcab2ca
62 changed files with 1549 additions and 369 deletions
+27 -4
View File
@@ -2,8 +2,31 @@
import { Target, ArrowRight, DollarSign, Users, Trophy } from "lucide-react";
/** 决策链节点。 */
interface DecisionChainNode {
role?: string;
influence?: string;
}
/** 竞争分析。 */
interface CompetitiveAnalysis {
strengths?: string[];
weaknesses?: string[];
}
/** 客户获取方案。 */
interface CustomerPlanData {
target_customer?: string;
execution_status?: string;
entry_angle?: string;
pricing_strategy?: string;
decision_chain?: DecisionChainNode[];
competitive_analysis?: CompetitiveAnalysis;
lp_resources?: string[];
}
/** 客户获取方案卡片 — 展示切入角度/决策链/定价/竞争分析。 */
export function PlanCard({ plan }: { plan: any }) {
export function PlanCard({ plan }: { plan: CustomerPlanData }) {
const statusColors: Record<string, string> = {
planned: "bg-blue-100 text-blue-700",
executing: "bg-amber-100 text-amber-700",
@@ -19,8 +42,8 @@ export function PlanCard({ plan }: { plan: any }) {
<Target size={18} className="text-[var(--investor-primary)]" />
<h3 className="text-sm font-medium">{plan.target_customer || "未指定目标客户"}</h3>
</div>
<span className={`rounded px-2 py-0.5 text-xs ${statusColors[plan.execution_status] || "bg-muted"}`}>
{plan.execution_status}
<span className={`rounded px-2 py-0.5 text-xs ${plan.execution_status ? (statusColors[plan.execution_status] || "bg-muted") : "bg-muted"}`}>
{plan.execution_status || "未知"}
</span>
</div>
@@ -54,7 +77,7 @@ export function PlanCard({ plan }: { plan: any }) {
<span></span>
</div>
<div className="mt-1 flex flex-wrap gap-2">
{plan.decision_chain.map((node: any, i: number) => (
{plan.decision_chain.map((node, i: number) => (
<span key={i} className="rounded-md bg-muted px-2 py-1 text-xs">
{node.role || "角色"}
{node.influence && ` · ${node.influence}`}
@@ -114,7 +114,7 @@ export function AIWeeklyBrief() {
</div>
) : !isGenerating && !error ? (
<p className="py-4 text-center text-sm text-muted-foreground">
"生成周报" AI
&ldquo;&rdquo; AI
</p>
) : null}
</div>
@@ -6,16 +6,24 @@ import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
import { EmptyState } from "@/components/shared/EmptyState";
import { TrendingUp, TrendingDown, Minus, AlertCircle } from "lucide-react";
/** 预测数据。 */
interface ForecastData {
predictions?: number[];
trend_direction?: string;
confidence: number;
anomalies?: Record<string, number[]>;
}
/** 预测趋势图组件 — 含置信区间。 */
export function ForecastChart({ companyId }: { companyId?: string }) {
const [data, setData] = useState<any>(null);
const [data, setData] = useState<ForecastData | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const path = companyId
? `/dashboard/forecasts?company_id=${companyId}&months_ahead=3`
: "/dashboard/forecasts?months_ahead=3";
apiFetch<any>(path)
apiFetch<ForecastData>(path)
.then((res) => setData(res.data))
.catch(() => {})
.finally(() => setLoading(false));
@@ -37,7 +45,6 @@ export function ForecastChart({ companyId }: { companyId?: string }) {
const chartW = width - padding.left - padding.right;
const chartH = height - padding.top - padding.bottom;
const allValues = predictions.length > 0 ? predictions : [];
const maxVal = 100;
const minVal = 0;
const yScale = (v: number) => chartH - ((v - minVal) / (maxVal - minVal)) * chartH;
@@ -7,14 +7,28 @@ import { EmptyState } from "@/components/shared/EmptyState";
import { DIMENSIONS_14 } from "@/components/health/HealthRadar";
import { TrendingUp, TrendingDown, Minus } from "lucide-react";
/** 热力图行数据。 */
interface HeatmapRow {
company_id: string;
company_name: string;
total_score?: number;
scores: Record<string, number | null | undefined>;
}
/** 趋势数据点。 */
interface TrendDataPoint {
period: string;
avg_score: number;
}
/** 健康度热力图 — 企业 × 维度评分矩阵。 */
export function HealthHeatmap() {
const [data, setData] = useState<any[]>([]);
const [data, setData] = useState<HeatmapRow[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
apiFetch<any[]>("/dashboard/heatmap")
.then((res) => setData((res.data as any[]) || []))
apiFetch<HeatmapRow[]>("/dashboard/heatmap")
.then((res) => setData((res.data as HeatmapRow[]) || []))
.catch(() => {})
.finally(() => setLoading(false));
}, []);
@@ -80,15 +94,15 @@ export function HealthHeatmap() {
/** 健康度趋势对比图 — 按月汇总评分变化折线图。 */
export function HealthTrends({ companyId }: { companyId?: string }) {
const [data, setData] = useState<any[]>([]);
const [data, setData] = useState<TrendDataPoint[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const path = companyId
? `/dashboard/trends?company_id=${companyId}&months=6`
: "/dashboard/trends?months=6";
apiFetch<any[]>(path)
.then((res) => setData((res.data as any[]) || []))
apiFetch<TrendDataPoint[]>(path)
.then((res) => setData((res.data as TrendDataPoint[]) || []))
.catch(() => {})
.finally(() => setLoading(false));
}, [companyId]);
@@ -3,19 +3,58 @@
import { useState } from "react";
import { apiFetch } from "@/lib/api";
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
import { DollarSign, TrendingUp, Users, MessageSquare } from "lucide-react";
import { DollarSign,Users, MessageSquare } from "lucide-react";
/** 融资规划结果。 */
interface FinancingPlan {
round?: string;
target_amount?: string;
valuation_range?: string;
timeline?: string;
target_investors?: string[];
key_metrics?: string[];
}
/** 组织诊断结果。 */
interface OrgDiagnosticResult {
structure_assessment?: string;
key_role_risks?: KeyRoleRisk[];
talent_gaps?: string[];
recommendations?: string[];
}
/** 关键岗位风险项。 */
interface KeyRoleRisk {
role: string;
risk: string;
severity: string;
}
/** 投资人沟通准备结果。 */
interface InvestorCommResult {
board_material_outline?: string;
anticipated_questions?: AnticipatedQuestion[];
key_updates?: string[];
asks?: string[];
}
/** 预期问答项。 */
interface AnticipatedQuestion {
question: string;
suggested_answer: string;
}
/** 融资规划组件 — 节奏/估值/投资人画像。 */
export function FinancingPlanner() {
const [companyData, setCompanyData] = useState("");
const [result, setResult] = useState<any>(null);
const [result, setResult] = useState<FinancingPlan | null>(null);
const [loading, setLoading] = useState(false);
async function handleGenerate() {
if (!companyData.trim()) return;
setLoading(true);
try {
const res = await apiFetch<any>("/founder/financing-plan", {
const res = await apiFetch<FinancingPlan>("/founder/financing-plan", {
method: "POST",
body: JSON.stringify({ company_data: companyData }),
});
@@ -81,14 +120,14 @@ export function FinancingPlanner() {
/** 组织诊断组件 — 团队结构/关键岗位风险/人才缺口。 */
export function OrgDiagnostic() {
const [teamData, setTeamData] = useState("");
const [result, setResult] = useState<any>(null);
const [result, setResult] = useState<OrgDiagnosticResult | null>(null);
const [loading, setLoading] = useState(false);
async function handleGenerate() {
if (!teamData.trim()) return;
setLoading(true);
try {
const res = await apiFetch<any>("/founder/org-diagnostic", {
const res = await apiFetch<OrgDiagnosticResult>("/founder/org-diagnostic", {
method: "POST",
body: JSON.stringify({ team_data: teamData }),
});
@@ -130,7 +169,7 @@ export function OrgDiagnostic() {
<div>
<span className="text-muted-foreground"></span>
<div className="mt-1 space-y-1">
{result.key_role_risks.map((r: any, i: number) => (
{result.key_role_risks.map((r, i: number) => (
<div key={i} className="flex items-center justify-between rounded-md border border-[var(--border)] px-2 py-1 text-xs">
<span>{r.role}</span>
<span className={r.severity === "high" ? "text-rose-600" : r.severity === "medium" ? "text-amber-600" : "text-muted-foreground"}>
@@ -168,14 +207,14 @@ export function OrgDiagnostic() {
/** 投资人沟通准备组件 — 董事会材料/投资人问答。 */
export function InvestorComm() {
const [boardContext, setBoardContext] = useState("");
const [result, setResult] = useState<any>(null);
const [result, setResult] = useState<InvestorCommResult | null>(null);
const [loading, setLoading] = useState(false);
async function handleGenerate() {
if (!boardContext.trim()) return;
setLoading(true);
try {
const res = await apiFetch<any>("/founder/investor-comm-prep", {
const res = await apiFetch<InvestorCommResult>("/founder/investor-comm-prep", {
method: "POST",
body: JSON.stringify({ board_context: boardContext }),
});
@@ -220,7 +259,7 @@ export function InvestorComm() {
<div>
<span className="text-muted-foreground"></span>
<div className="mt-1 space-y-1">
{result.anticipated_questions.map((q: any, i: number) => (
{result.anticipated_questions.map((q, i: number) => (
<div key={i} className="rounded-md border border-[var(--border)] px-2 py-1 text-xs">
<div className="font-medium">Q: {q.question}</div>
<div className="mt-1 text-muted-foreground">A: {q.suggested_answer}</div>
@@ -1,12 +1,23 @@
"use client";
import { useState } from "react";
import { apiFetch } from "@/lib/api";
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
import { FileText, Printer, Download } from "lucide-react";
/** 报告数据。 */
interface ReportData {
executive_summary?: string;
financial_performance?: string;
operational_highlights?: string;
risk_assessment?: string;
recommendations?: string[];
next_quarter_focus?: string;
year_in_review?: string;
key_achievements?: string[];
}
/** 报告预览组件 — 浏览器打印优化。 */
export function ReportPreview({ report, companyName }: { report: any; companyName: string }) {
export function ReportPreview({ report, companyName }: { report: ReportData; companyName: string }) {
if (!report) return null;
return (
@@ -3,18 +3,18 @@
import { useState, useRef, useCallback } from "react";
import { Upload, File as FileIcon, X, Loader2 } from "lucide-react";
const ALLOWED_TYPES = [".xlsx", ".xls", ".pdf", ".txt", ".md", ".csv"];
/** 文件上传组件 — 支持拖拽 + 点击上传。 */
export function FileUploader({ onParsed }: { onParsed: (result: any) => void }) {
export function FileUploader({ onParsed }: { onParsed: (result: unknown) => void }) {
const [dragging, setDragging] = useState(false);
const [uploading, setUploading] = useState(false);
const [file, setFile] = useState<File | null>(null);
const inputRef = useRef<HTMLInputElement>(null);
const allowedTypes = [".xlsx", ".xls", ".pdf", ".txt", ".md", ".csv"];
const handleFile = useCallback(async (f: File) => {
const ext = f.name.match(/\.[^.]+$/)?.[0]?.toLowerCase() || "";
if (!allowedTypes.includes(ext)) {
if (!ALLOWED_TYPES.includes(ext)) {
return;
}
setFile(f);
@@ -68,7 +68,7 @@ export function InsightRail({ defaultAgent = "risk" }: InsightRailProps) {
return (
<aside
className="sticky top-0 hidden h-screen w-10 shrink-0 items-center justify-center border-l bg-white md:flex"
aria-expanded={false}
>
<button
type="button"
@@ -87,7 +87,7 @@ export function InsightRail({ defaultAgent = "risk" }: InsightRailProps) {
return (
<aside
className="sticky top-0 hidden h-screen w-80 shrink-0 flex-col border-l bg-white md:flex"
aria-expanded={true}
>
{/* 顶部:折叠按钮 + 当前上下文 */}
<div className="flex items-center justify-between border-b px-3 py-2">
@@ -4,10 +4,17 @@ import { useState } from "react";
import { apiFetch } from "@/lib/api";
import { Search, FileText, Loader2 } from "lucide-react";
/** 搜索结果项。 */
interface SearchResult {
id: string;
source_type: string;
content: string;
}
/** 语义搜索组件 — 搜索知识库中的月报/报告片段。 */
export function KnowledgeSearch() {
const [query, setQuery] = useState("");
const [results, setResults] = useState<any[]>([]);
const [results, setResults] = useState<SearchResult[]>([]);
const [loading, setLoading] = useState(false);
const [searched, setSearched] = useState(false);
@@ -16,8 +23,8 @@ export function KnowledgeSearch() {
setLoading(true);
setSearched(true);
try {
const res = await apiFetch<any[]>(`/knowledge/search?q=${encodeURIComponent(query)}&top_k=5`);
setResults((res.data as any[]) || []);
const res = await apiFetch<SearchResult[]>(`/knowledge/search?q=${encodeURIComponent(query)}&top_k=5`);
setResults((res.data as SearchResult[]) || []);
} catch {
setResults([]);
} finally {
@@ -1,11 +1,11 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { AlertTriangle, X, CheckCircle } from "lucide-react";
import { AlertTriangle, X } from "lucide-react";
/** 风险预警 toast 推送组件 — 轮询新风险并弹出 toast。 */
import { listRisks, type RiskEvent } from "@/lib/risks";
import { listRisks } from "@/lib/risks";
const POLL_INTERVAL = 30000; // 30 秒轮询
@@ -3,7 +3,6 @@
"use client";
import { LayoutGrid, GitCompareArrows, Focus, Columns2 } from "lucide-react";
import { useState, type ReactNode } from "react";
/** 工作模式类型。 */
export type WorkMode = "overview" | "compare" | "focus" | "queue";
@@ -2,7 +2,6 @@
"use client";
import { useState } from "react";
import { X, Plus } from "lucide-react";
/** 工作区 Tab 定义。 */
@@ -5,17 +5,32 @@ import { apiFetch } from "@/lib/api";
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
import { AlertTriangle, GitBranch, TrendingDown } from "lucide-react";
/** 约束点分析结果。 */
interface ConstraintResult {
constraint?: string;
sensitivity?: string;
improvement_space?: string;
recommendations?: string[];
}
/** 鸿沟诊断结果。 */
interface ChasmResult {
stage?: string;
gap_detected?: boolean;
crossing_strategy?: string;
}
/** 约束点面板 — TOC 约束点识别。 */
export function ConstraintPanel() {
const [companyData, setCompanyData] = useState("");
const [result, setResult] = useState<any>(null);
const [result, setResult] = useState<ConstraintResult | null>(null);
const [loading, setLoading] = useState(false);
async function handleAnalyze() {
if (!companyData.trim()) return;
setLoading(true);
try {
const res = await apiFetch<any>("/advanced-analysis/constraints", {
const res = await apiFetch<ConstraintResult>("/advanced-analysis/constraints", {
method: "POST",
body: JSON.stringify({ company_data: companyData }),
});
@@ -86,14 +101,14 @@ export function BMLTracker() {
/** 鸿沟诊断预警组件。 */
export function ChasmAlert() {
const [companyData, setCompanyData] = useState("");
const [result, setResult] = useState<any>(null);
const [result, setResult] = useState<ChasmResult | null>(null);
const [loading, setLoading] = useState(false);
async function handleDiagnose() {
if (!companyData.trim()) return;
setLoading(true);
try {
const res = await apiFetch<any>("/advanced-analysis/chasm", {
const res = await apiFetch<ChasmResult>("/advanced-analysis/chasm", {
method: "POST",
body: JSON.stringify({ company_data: companyData }),
});
+1 -1
View File
@@ -1,6 +1,6 @@
"use client";
import { useState, type ReactNode } from "react";
import {type ReactNode } from "react";
import {
LayoutDashboard, DollarSign, Activity, Users, Bot,
AlertTriangle, FileText, ClipboardList, ScrollText, Network,