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:
@@ -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 摘要
|
||||
点击“生成周报”获取本周投资组合 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,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,
|
||||
|
||||
Reference in New Issue
Block a user