2026-03-20 08:39:07 +00:00
|
|
|
|
import { useEffect, useState } from "react";
|
2026-03-19 06:23:03 +00:00
|
|
|
|
import { BarChart3, TrendingUp, Users, CheckCircle2 } from "lucide-react";
|
|
|
|
|
|
import { motion } from "motion/react";
|
2026-03-20 08:39:07 +00:00
|
|
|
|
import { getDashboardHome, type DashboardActivity, type DashboardHome, type DashboardStat, type DashboardTodo } from "@/lib/auth";
|
|
|
|
|
|
|
|
|
|
|
|
const DASHBOARD_PREVIEW_COUNT = 5;
|
|
|
|
|
|
|
|
|
|
|
|
const baseStats = [
|
|
|
|
|
|
{ name: "本月新增商机", metricKey: "monthlyOpportunities", icon: TrendingUp, color: "text-emerald-600 dark:text-emerald-400", bg: "bg-emerald-100 dark:bg-emerald-500/20" },
|
|
|
|
|
|
{ name: "跟进中客户", metricKey: "followingCustomers", icon: Users, color: "text-blue-600 dark:text-blue-400", bg: "bg-blue-100 dark:bg-blue-500/20" },
|
|
|
|
|
|
{ name: "已成单项目", metricKey: "wonProjects", icon: CheckCircle2, color: "text-violet-600 dark:text-violet-400", bg: "bg-violet-100 dark:bg-violet-500/20" },
|
|
|
|
|
|
{ name: "本月打卡天数", metricKey: "monthlyCheckins", icon: BarChart3, color: "text-amber-600 dark:text-amber-400", bg: "bg-amber-100 dark:bg-amber-500/20" },
|
|
|
|
|
|
] as const;
|
2026-03-19 06:23:03 +00:00
|
|
|
|
|
|
|
|
|
|
export default function Dashboard() {
|
2026-03-20 08:39:07 +00:00
|
|
|
|
const [home, setHome] = useState<DashboardHome>({});
|
|
|
|
|
|
const [showAllTodos, setShowAllTodos] = useState(false);
|
|
|
|
|
|
const [showAllActivities, setShowAllActivities] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
let cancelled = false;
|
|
|
|
|
|
|
|
|
|
|
|
async function loadDashboard() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = await getDashboardHome();
|
|
|
|
|
|
if (!cancelled) {
|
|
|
|
|
|
setHome(data ?? {});
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
if (!cancelled) {
|
|
|
|
|
|
setHome({});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void loadDashboard();
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
cancelled = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
setShowAllTodos(false);
|
|
|
|
|
|
setShowAllActivities(false);
|
|
|
|
|
|
}, [home.todos, home.activities]);
|
|
|
|
|
|
|
|
|
|
|
|
const statMap = new Map((home.stats ?? []).map((item: DashboardStat) => [item.metricKey, item.value]));
|
|
|
|
|
|
const stats = baseStats.map((stat) => ({
|
|
|
|
|
|
...stat,
|
|
|
|
|
|
value: statMap.get(stat.metricKey),
|
|
|
|
|
|
}));
|
|
|
|
|
|
const todos = (home.todos?.length ? home.todos : [{ id: 0, title: "无" }]) as DashboardTodo[];
|
|
|
|
|
|
const activities = (home.activities?.length
|
|
|
|
|
|
? home.activities
|
|
|
|
|
|
: [{ id: 0, title: "无", content: "无", timeText: "无" }]) as DashboardActivity[];
|
|
|
|
|
|
const visibleTodos = showAllTodos ? todos : todos.slice(0, DASHBOARD_PREVIEW_COUNT);
|
|
|
|
|
|
const visibleActivities = showAllActivities ? activities : activities.slice(0, DASHBOARD_PREVIEW_COUNT);
|
|
|
|
|
|
const hasMoreTodos = todos.length > DASHBOARD_PREVIEW_COUNT && todos[0]?.id !== 0;
|
|
|
|
|
|
const hasMoreActivities = activities.length > DASHBOARD_PREVIEW_COUNT && activities[0]?.id !== 0;
|
2026-03-19 06:23:03 +00:00
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
|
<header>
|
|
|
|
|
|
<h1 className="text-2xl font-bold tracking-tight text-slate-900 dark:text-white">工作台</h1>
|
2026-03-20 08:39:07 +00:00
|
|
|
|
<p className="text-sm text-slate-500 dark:text-slate-400">
|
|
|
|
|
|
欢迎回来,{home.realName || "无"}。今天是你入职的第 {home.onboardingDays ?? 0} 天。
|
|
|
|
|
|
</p>
|
2026-03-19 06:23:03 +00:00
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
|
|
|
|
|
|
{stats.map((stat, i) => (
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
key={stat.name}
|
|
|
|
|
|
initial={{ opacity: 0, y: 20 }}
|
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
|
transition={{ delay: i * 0.1 }}
|
|
|
|
|
|
className="rounded-2xl border border-slate-100 dark:border-slate-800 bg-white dark:bg-slate-900/50 p-5 shadow-sm backdrop-blur-sm transition-all hover:shadow-md dark:hover:bg-slate-900"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
|
|
<div className={`flex h-12 w-12 items-center justify-center rounded-xl ${stat.bg}`}>
|
|
|
|
|
|
<stat.icon className={`h-6 w-6 ${stat.color}`} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p className="text-sm font-medium text-slate-500 dark:text-slate-400">{stat.name}</p>
|
2026-03-20 08:39:07 +00:00
|
|
|
|
<p className="text-2xl font-bold text-slate-900 dark:text-white">{stat.value ?? 0}</p>
|
2026-03-19 06:23:03 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="grid gap-6 md:grid-cols-2">
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
initial={{ opacity: 0, y: 20 }}
|
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
|
transition={{ delay: 0.4 }}
|
|
|
|
|
|
className="rounded-2xl border border-slate-100 dark:border-slate-800 bg-white dark:bg-slate-900/50 p-6 shadow-sm backdrop-blur-sm"
|
|
|
|
|
|
>
|
|
|
|
|
|
<h2 className="mb-4 text-lg font-semibold text-slate-900 dark:text-white">待办事项</h2>
|
|
|
|
|
|
<ul className="space-y-3">
|
2026-03-20 08:39:07 +00:00
|
|
|
|
{visibleTodos.map((task: DashboardTodo, i: number) => (
|
|
|
|
|
|
<li key={task.id ?? i} className="group flex cursor-pointer items-center gap-3 rounded-xl border border-slate-50 dark:border-slate-800/50 p-3 transition-all hover:bg-slate-50 dark:hover:bg-slate-800">
|
2026-03-19 06:23:03 +00:00
|
|
|
|
<div className="flex h-5 w-5 items-center justify-center rounded-full border-2 border-slate-300 dark:border-slate-600 group-hover:border-violet-500 dark:group-hover:border-violet-400 transition-colors" />
|
2026-03-20 08:39:07 +00:00
|
|
|
|
<span className="text-sm text-slate-700 dark:text-slate-300 group-hover:text-slate-900 dark:group-hover:text-white transition-colors">{task.title || "无"}</span>
|
2026-03-19 06:23:03 +00:00
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
2026-03-20 08:39:07 +00:00
|
|
|
|
{hasMoreTodos && !showAllTodos ? (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setShowAllTodos(true)}
|
|
|
|
|
|
className="mt-4 w-full rounded-xl border border-dashed border-slate-200 dark:border-slate-700 px-3 py-2 text-sm font-medium text-slate-500 transition-colors hover:border-violet-300 hover:text-violet-600 dark:text-slate-400 dark:hover:border-violet-700 dark:hover:text-violet-400"
|
|
|
|
|
|
>
|
|
|
|
|
|
...
|
|
|
|
|
|
</button>
|
|
|
|
|
|
) : null}
|
2026-03-19 06:23:03 +00:00
|
|
|
|
</motion.div>
|
|
|
|
|
|
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
initial={{ opacity: 0, y: 20 }}
|
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
|
transition={{ delay: 0.5 }}
|
|
|
|
|
|
className="rounded-2xl border border-slate-100 dark:border-slate-800 bg-white dark:bg-slate-900/50 p-6 shadow-sm backdrop-blur-sm"
|
|
|
|
|
|
>
|
|
|
|
|
|
<h2 className="mb-4 text-lg font-semibold text-slate-900 dark:text-white">最新动态</h2>
|
|
|
|
|
|
<div className="space-y-5">
|
2026-03-20 08:39:07 +00:00
|
|
|
|
{visibleActivities.map((news: DashboardActivity, i: number) => (
|
|
|
|
|
|
<div key={news.id ?? i} className="flex gap-4">
|
2026-03-19 06:23:03 +00:00
|
|
|
|
<div className="relative mt-1 flex h-3 w-3 items-center justify-center">
|
|
|
|
|
|
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-violet-400 opacity-20"></span>
|
|
|
|
|
|
<span className="relative inline-flex h-2 w-2 rounded-full bg-violet-500"></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2026-03-20 08:39:07 +00:00
|
|
|
|
<p className="text-sm font-medium text-slate-900 dark:text-white">{news.title || "无"}</p>
|
|
|
|
|
|
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">{news.content || "无"}</p>
|
|
|
|
|
|
<p className="mt-1 text-[10px] text-slate-400 dark:text-slate-500">{news.timeText || "无"}</p>
|
2026-03-19 06:23:03 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
2026-03-20 08:39:07 +00:00
|
|
|
|
{hasMoreActivities && !showAllActivities ? (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setShowAllActivities(true)}
|
|
|
|
|
|
className="mt-4 w-full rounded-xl border border-dashed border-slate-200 dark:border-slate-700 px-3 py-2 text-sm font-medium text-slate-500 transition-colors hover:border-violet-300 hover:text-violet-600 dark:text-slate-400 dark:hover:border-violet-700 dark:hover:text-violet-400"
|
|
|
|
|
|
>
|
|
|
|
|
|
...
|
|
|
|
|
|
</button>
|
|
|
|
|
|
) : null}
|
2026-03-19 06:23:03 +00:00
|
|
|
|
</motion.div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|