geo/frontend/app/(dashboard)/dashboard/page.tsx

351 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
);
}