351 lines
12 KiB
TypeScript
351 lines
12 KiB
TypeScript
"use client";
|
||
|
||
import { useRouter } from "next/navigation";
|
||
import Link from "next/link";
|
||
import {
|
||
MetricCard,
|
||
StageProgress,
|
||
AgentStatusCard,
|
||
} from "@/components/business";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import {
|
||
Sparkles,
|
||
Search,
|
||
FileText,
|
||
Send,
|
||
BarChart3,
|
||
Target,
|
||
Plus,
|
||
ArrowRight,
|
||
Zap,
|
||
} from "lucide-react";
|
||
import { type GeoProject, type LifecycleStats } from "@/lib/api";
|
||
import { useApi } from "@/lib/hooks/use-api";
|
||
import { LoadingState, ErrorState, EmptyState } from "@/components/ui/api-states";
|
||
|
||
/* ─── Helpers ─────────────────────────────────────────────────────────────────*/
|
||
|
||
const STAGE_CONFIG = [
|
||
{ id: "diagnosis", label: "诊断分析" },
|
||
{ id: "strategy", label: "策略制定" },
|
||
{ id: "content", label: "内容生产" },
|
||
{ id: "publishing", label: "分发执行" },
|
||
{ id: "monitoring", label: "监测优化" },
|
||
];
|
||
|
||
const MOCK_AGENTS = [
|
||
{
|
||
name: "内容生成Agent",
|
||
description: "自动化内容生产",
|
||
status: "busy" as const,
|
||
lastActiveAt: "2分钟前",
|
||
completedCount: 156,
|
||
},
|
||
{
|
||
name: "引用监测Agent",
|
||
description: "AI平台引用追踪",
|
||
status: "online" as const,
|
||
lastActiveAt: "刚刚",
|
||
completedCount: 3420,
|
||
},
|
||
{
|
||
name: "SEO诊断Agent",
|
||
description: "搜索引擎优化分析",
|
||
status: "offline" as const,
|
||
lastActiveAt: "3小时前",
|
||
completedCount: 89,
|
||
},
|
||
];
|
||
|
||
function buildStages(currentStage: GeoProject["current_stage"]) {
|
||
const currentIndex = STAGE_CONFIG.findIndex((s) => s.id === currentStage);
|
||
return STAGE_CONFIG.map((stage, idx) => {
|
||
let status: "completed" | "active" | "pending" | "error" = "pending";
|
||
if (idx < currentIndex) status = "completed";
|
||
else if (idx === currentIndex) status = "active";
|
||
return { id: stage.id, label: stage.label, status };
|
||
});
|
||
}
|
||
|
||
function getRecommendation(stage: GeoProject["current_stage"]) {
|
||
const map: Record<
|
||
GeoProject["current_stage"],
|
||
{ title: string; description: string; icon: React.ReactNode; href: string }
|
||
> = {
|
||
diagnosis: {
|
||
title: "开始诊断分析",
|
||
description: "诊断您品牌在AI搜索中的可见性现状",
|
||
icon: <Search className="h-5 w-5" />,
|
||
href: "/dashboard/diagnosis",
|
||
},
|
||
strategy: {
|
||
title: "生成优化策略",
|
||
description: "基于诊断结果制定GEO+SEO优化方案",
|
||
icon: <Target className="h-5 w-5" />,
|
||
href: "/dashboard/strategy",
|
||
},
|
||
content: {
|
||
title: "创建新内容",
|
||
description: "使用AI Agent批量生成优化内容",
|
||
icon: <FileText className="h-5 w-5" />,
|
||
href: "/dashboard/content",
|
||
},
|
||
publishing: {
|
||
title: "配置分发渠道",
|
||
description: "将内容分发到各大AI平台和搜索引擎",
|
||
icon: <Send className="h-5 w-5" />,
|
||
href: "/dashboard/publishing",
|
||
},
|
||
monitoring: {
|
||
title: "查看监测报告",
|
||
description: "分析内容分发后的引用和排名数据",
|
||
icon: <BarChart3 className="h-5 w-5" />,
|
||
href: "/dashboard/monitoring",
|
||
},
|
||
};
|
||
return map[stage];
|
||
}
|
||
|
||
/* ─── Component ───────────────────────────────────────────────────────────────*/
|
||
|
||
export default function DashboardPage() {
|
||
const router = useRouter();
|
||
|
||
const {
|
||
data: projects,
|
||
isLoading: projectsLoading,
|
||
error: projectsError,
|
||
refresh: refreshProjects,
|
||
} = useApi<GeoProject[]>("/api/v1/lifecycle/projects/");
|
||
|
||
const {
|
||
data: stats,
|
||
isLoading: statsLoading,
|
||
error: statsError,
|
||
refresh: refreshStats,
|
||
} = useApi<LifecycleStats>("/api/v1/lifecycle/projects/stats");
|
||
|
||
const loading = projectsLoading || statsLoading;
|
||
|
||
// "用户未关联组织" 类错误视为空状态
|
||
const isOrgError = (err: Error | undefined) =>
|
||
err?.message.includes("未关联组织") || err?.message.includes("No organization");
|
||
|
||
const hasOrgError = isOrgError(projectsError) || isOrgError(statsError);
|
||
const error =
|
||
!hasOrgError && (projectsError || statsError) ? projectsError || statsError : undefined;
|
||
|
||
const safeProjects: GeoProject[] = hasOrgError ? [] : (projects ?? []);
|
||
const safeStats: LifecycleStats | null = hasOrgError ? null : (stats ?? null);
|
||
|
||
const handleRetry = () => {
|
||
refreshProjects();
|
||
refreshStats();
|
||
};
|
||
|
||
/* ─── Loading State ────────────────────────────────────────────────────────*/
|
||
if (loading) {
|
||
return (
|
||
<div className="space-y-6">
|
||
<div className="mb-8">
|
||
<LoadingState rows={1} rowHeight="h-8" />
|
||
<div className="mt-2"><LoadingState rows={1} rowHeight="h-4" /></div>
|
||
</div>
|
||
<LoadingState rows={4} grid cols={4} rowHeight="h-24" />
|
||
<LoadingState rows={1} rowHeight="h-28" />
|
||
<LoadingState rows={2} grid cols={2} rowHeight="h-48" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ─── Error State ──────────────────────────────────────────────────────────*/
|
||
if (error) {
|
||
return (
|
||
<div className="space-y-6">
|
||
<div className="mb-8">
|
||
<h1 className="text-2xl font-bold text-gray-900">Overview</h1>
|
||
</div>
|
||
<ErrorState error={error} onRetry={handleRetry} />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ─── Empty State ──────────────────────────────────────────────────────────*/
|
||
if (safeProjects.length === 0) {
|
||
return (
|
||
<div className="space-y-6">
|
||
<div className="mb-8">
|
||
<h1 className="text-2xl font-bold text-gray-900">Overview</h1>
|
||
<p className="mt-1 text-sm text-gray-500">GEO和SEO是AI营销时代的共生体</p>
|
||
</div>
|
||
<EmptyState
|
||
icon={<Sparkles className="h-6 w-6 text-gray-400" />}
|
||
message="开始优化您的AI可见性"
|
||
description="创建第一个项目,系统将自动引导您完成从诊断分析到监测优化的全生命周期管理。"
|
||
action={
|
||
<Link href="/dashboard/lifecycle/new">
|
||
<Button>
|
||
<Plus className="mr-2 h-4 w-4" />
|
||
创建项目
|
||
</Button>
|
||
</Link>
|
||
}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ─── Active Dashboard ─────────────────────────────────────────────────────*/
|
||
const project = safeProjects[0];
|
||
const stages = buildStages(project.current_stage);
|
||
const recommendation = getRecommendation(project.current_stage);
|
||
|
||
const citationRate = safeStats?.avg_ai_citation_rate != null
|
||
? `${(safeStats.avg_ai_citation_rate * 100).toFixed(1)}%`
|
||
: "—";
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* 1. Page Title */}
|
||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between mb-8">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-gray-900">Overview</h1>
|
||
<p className="mt-1 text-sm text-gray-500">
|
||
{project.brand_name} — {project.name}
|
||
</p>
|
||
</div>
|
||
<Link href="/dashboard/lifecycle/new">
|
||
<Button variant="outline" className="shrink-0">
|
||
<Plus className="mr-2 h-4 w-4" />
|
||
创建项目
|
||
</Button>
|
||
</Link>
|
||
</div>
|
||
|
||
{/* 2. KPI Cards */}
|
||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||
<MetricCard
|
||
label="活跃项目数"
|
||
value={safeStats?.active_projects ?? 0}
|
||
trend="neutral"
|
||
trendLabel={`共 ${safeStats?.total_projects ?? 0} 个项目`}
|
||
size="default"
|
||
/>
|
||
<MetricCard
|
||
label="内容产出统计"
|
||
value={safeStats?.contents_produced ?? 0}
|
||
trend="up"
|
||
trendLabel="全部内容"
|
||
size="default"
|
||
/>
|
||
<MetricCard
|
||
label="AI引用率"
|
||
value={citationRate}
|
||
trend={safeStats?.avg_ai_citation_rate ? "up" : "neutral"}
|
||
trendLabel="平均引用率"
|
||
size="default"
|
||
/>
|
||
<MetricCard
|
||
label="已完成项目"
|
||
value={safeStats?.completed_projects ?? 0}
|
||
trend="neutral"
|
||
trendLabel={`活跃 ${safeStats?.active_projects ?? 0} 个`}
|
||
size="default"
|
||
/>
|
||
</div>
|
||
|
||
{/* 3. Stage Progress */}
|
||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<p className="text-sm font-medium text-gray-500">生命周期进度</p>
|
||
<Badge variant="secondary" className="text-xs">
|
||
当前阶段:
|
||
{STAGE_CONFIG.find((s) => s.id === project.current_stage)?.label}
|
||
</Badge>
|
||
</div>
|
||
<StageProgress
|
||
stages={stages}
|
||
onStageClick={(stage) => {
|
||
router.push(`/dashboard/${stage.id}`);
|
||
}}
|
||
/>
|
||
</div>
|
||
|
||
{/* 4. Two-column: Recommendation + Agent Activity */}
|
||
<div className="grid gap-4 lg:grid-cols-2">
|
||
{/* Recommendation */}
|
||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||
<p className="text-sm font-medium text-gray-500 mb-4">推荐下一步</p>
|
||
<div className="flex items-center gap-4 rounded-lg border border-gray-100 bg-gray-50 p-4">
|
||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
||
{recommendation.icon}
|
||
</div>
|
||
<div className="flex-1 min-w-0">
|
||
<p className="text-sm font-semibold text-gray-900">
|
||
{recommendation.title}
|
||
</p>
|
||
<p className="text-xs text-gray-500 mt-0.5">
|
||
{recommendation.description}
|
||
</p>
|
||
</div>
|
||
<Button
|
||
size="sm"
|
||
className="shrink-0"
|
||
onClick={() => router.push(recommendation.href)}
|
||
>
|
||
执行
|
||
<ArrowRight className="ml-1 h-3.5 w-3.5" />
|
||
</Button>
|
||
</div>
|
||
<div className="mt-3 grid grid-cols-2 gap-3">
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
className="justify-start"
|
||
onClick={() => router.push("/dashboard/agents")}
|
||
>
|
||
<Zap className="mr-2 h-3.5 w-3.5" />
|
||
管理Agent
|
||
</Button>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
className="justify-start"
|
||
onClick={() => router.push("/dashboard/lifecycle")}
|
||
>
|
||
<BarChart3 className="mr-2 h-3.5 w-3.5" />
|
||
项目详情
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Agent Activity */}
|
||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<p className="text-sm font-medium text-gray-500">Agent活动</p>
|
||
<Link
|
||
href="/dashboard/agents"
|
||
className="text-xs text-primary hover:underline"
|
||
>
|
||
查看全部
|
||
</Link>
|
||
</div>
|
||
<div className="space-y-3">
|
||
{MOCK_AGENTS.map((agent) => (
|
||
<AgentStatusCard
|
||
key={agent.name}
|
||
name={agent.name}
|
||
description={agent.description}
|
||
status={agent.status}
|
||
lastActiveAt={agent.lastActiveAt}
|
||
completedCount={agent.completedCount}
|
||
/>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|