324 lines
9.9 KiB
TypeScript
324 lines
9.9 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useEffect } from "react";
|
||
import { useSession } from "next-auth/react";
|
||
import { useRouter } from "next/navigation";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { Skeleton } from "@/components/ui/skeleton";
|
||
import {
|
||
Loader2,
|
||
Rocket,
|
||
Target,
|
||
TrendingUp,
|
||
Plus,
|
||
Users,
|
||
BarChart3,
|
||
ArrowLeft,
|
||
CheckCircle2,
|
||
LayoutDashboard,
|
||
AlertTriangle,
|
||
} from "lucide-react";
|
||
import { api } from "@/lib/api";
|
||
import type { ActionSuggestion } from "@/types/onboarding";
|
||
|
||
interface Step5ActionSuggestionsProps {
|
||
brandId: string;
|
||
brandName: string;
|
||
onComplete: () => void;
|
||
onBack: () => void;
|
||
onSkip: () => void;
|
||
}
|
||
|
||
const ACTION_ICONS: Record<string, React.ElementType> = {
|
||
improve_platform: BarChart3,
|
||
add_competitor: Users,
|
||
optimize_content: TrendingUp,
|
||
increase_frequency: Rocket,
|
||
};
|
||
|
||
const PRIORITY_COLORS: Record<
|
||
string,
|
||
{ bg: string; text: string; border: string }
|
||
> = {
|
||
high: { bg: "bg-red-50", text: "text-red-600", border: "border-red-200" },
|
||
medium: {
|
||
bg: "bg-amber-50",
|
||
text: "text-amber-600",
|
||
border: "border-amber-200",
|
||
},
|
||
low: { bg: "bg-blue-50", text: "text-blue-600", border: "border-blue-200" },
|
||
};
|
||
|
||
export function Step5ActionSuggestions({
|
||
brandId,
|
||
brandName,
|
||
onComplete,
|
||
onBack,
|
||
onSkip,
|
||
}: Step5ActionSuggestionsProps) {
|
||
const { data: session } = useSession();
|
||
const router = useRouter();
|
||
const [suggestions, setSuggestions] = useState<ActionSuggestion[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [completing, setCompleting] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
const fetchSuggestions = async () => {
|
||
if (!session?.accessToken) return;
|
||
|
||
try {
|
||
setLoading(true);
|
||
setError(null);
|
||
const data = (await api.onboarding.getActionSuggestions(
|
||
session.accessToken,
|
||
brandId,
|
||
)) as ActionSuggestion[];
|
||
setSuggestions(data || []);
|
||
} catch (err) {
|
||
console.error("获取行动建议失败:", err);
|
||
setError("获取行动建议失败,请重试");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
fetchSuggestions();
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [session?.accessToken, brandId]);
|
||
|
||
const handleComplete = async () => {
|
||
if (!session?.accessToken) return;
|
||
|
||
try {
|
||
setCompleting(true);
|
||
await api.onboarding.completeOnboarding(session.accessToken, brandId);
|
||
onComplete();
|
||
} catch (err) {
|
||
console.error("完成引导失败:", err);
|
||
// 即使API失败也继续,因为品牌已创建
|
||
onComplete();
|
||
} finally {
|
||
setCompleting(false);
|
||
}
|
||
};
|
||
|
||
const _handleGoToDashboard = () => {
|
||
router.push("/dashboard");
|
||
};
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="flex flex-col items-center justify-center py-8">
|
||
<div className="mb-6 text-center">
|
||
<div className="mb-4 inline-flex h-16 w-16 items-center justify-center rounded-full bg-primary/10">
|
||
<Target className="h-8 w-8 text-primary animate-pulse" />
|
||
</div>
|
||
<h2 className="mb-2 text-2xl font-bold">正在生成行动建议...</h2>
|
||
<p className="text-muted-foreground">
|
||
基于您的品牌表现,为您定制优化方案
|
||
</p>
|
||
</div>
|
||
|
||
<Card className="w-full max-w-2xl">
|
||
<CardContent className="pt-6 space-y-4">
|
||
<div className="flex items-center justify-center py-8">
|
||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||
</div>
|
||
<Skeleton className="h-24 w-full" />
|
||
<Skeleton className="h-24 w-full" />
|
||
<Skeleton className="h-24 w-full" />
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 错误状态
|
||
if (!loading && error) {
|
||
return (
|
||
<div className="flex flex-col items-center justify-center py-8">
|
||
<div className="mb-6 text-center">
|
||
<div className="mb-4 inline-flex h-16 w-16 items-center justify-center rounded-full bg-red-50">
|
||
<AlertTriangle className="h-8 w-8 text-red-600" />
|
||
</div>
|
||
<h2 className="mb-2 text-2xl font-bold">获取建议失败</h2>
|
||
<p className="text-muted-foreground">{error}</p>
|
||
</div>
|
||
<div className="flex gap-3">
|
||
<Button variant="outline" onClick={fetchSuggestions}>重试</Button>
|
||
<Button variant="ghost" onClick={onSkip}>稍后查看</Button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 按优先级分组
|
||
const highPriority = suggestions.filter((s) => s.priority === "high");
|
||
const mediumPriority = suggestions.filter((s) => s.priority === "medium");
|
||
const lowPriority = suggestions.filter((s) => s.priority === "low");
|
||
|
||
const renderSuggestionCard = (
|
||
suggestion: ActionSuggestion,
|
||
index: number,
|
||
) => {
|
||
const Icon = ACTION_ICONS[suggestion.action_type] || Target;
|
||
const colors = PRIORITY_COLORS[suggestion.priority];
|
||
|
||
return (
|
||
<div
|
||
key={suggestion.id}
|
||
className={`flex gap-4 rounded-lg border p-4 ${colors.border} ${colors.bg}`}
|
||
>
|
||
<div
|
||
className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-full ${colors.bg} ${colors.text}`}
|
||
>
|
||
<Icon className="h-5 w-5" />
|
||
</div>
|
||
<div className="flex-1">
|
||
<div className="flex items-center gap-2 mb-1">
|
||
<h4 className="font-semibold">{suggestion.title}</h4>
|
||
<Badge
|
||
variant="outline"
|
||
className={`${colors.bg} ${colors.text} border-0 text-xs`}
|
||
>
|
||
{suggestion.priority === "high"
|
||
? "高优"
|
||
: suggestion.priority === "medium"
|
||
? "中优"
|
||
: "低优"}
|
||
</Badge>
|
||
</div>
|
||
<p className="text-sm text-muted-foreground">
|
||
{suggestion.description}
|
||
</p>
|
||
</div>
|
||
<div className="flex items-center">
|
||
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-primary/10">
|
||
<span className="text-xs font-medium text-primary">
|
||
{index + 1}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
return (
|
||
<div className="flex flex-col items-center justify-center py-8">
|
||
<div className="mb-6 text-center">
|
||
<div className="mb-4 inline-flex h-16 w-16 items-center justify-center rounded-full bg-emerald-50">
|
||
<Rocket className="h-8 w-8 text-emerald-600" />
|
||
</div>
|
||
<h2 className="mb-2 text-2xl font-bold">下一步行动建议</h2>
|
||
<p className="text-muted-foreground">
|
||
基于您的品牌 “{brandName}” 的表现, 我们为您准备了以下优化建议
|
||
</p>
|
||
</div>
|
||
|
||
{/* 高优先级建议 */}
|
||
{highPriority.length > 0 && (
|
||
<Card className="w-full max-w-2xl mb-4 border-red-200">
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="flex items-center gap-2 text-lg text-red-600">
|
||
<Target className="h-5 w-5" />
|
||
主要行动(最需要改进)
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-3">
|
||
{highPriority.map((suggestion, index) =>
|
||
renderSuggestionCard(suggestion, index),
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* 中优先级建议 */}
|
||
{mediumPriority.length > 0 && (
|
||
<Card className="w-full max-w-2xl mb-4 border-amber-200">
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="flex items-center gap-2 text-lg text-amber-600">
|
||
<TrendingUp className="h-5 w-5" />
|
||
次要行动(可选项)
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-3">
|
||
{mediumPriority.map((suggestion, index) =>
|
||
renderSuggestionCard(suggestion, index + highPriority.length),
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* 低优先级建议 */}
|
||
{lowPriority.length > 0 && (
|
||
<Card className="w-full max-w-2xl mb-4 border-blue-200">
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="flex items-center gap-2 text-lg text-blue-600">
|
||
<Plus className="h-5 w-5" />
|
||
额外建议
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-3">
|
||
{lowPriority.map((suggestion, index) =>
|
||
renderSuggestionCard(
|
||
suggestion,
|
||
index + highPriority.length + mediumPriority.length,
|
||
),
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{error && <p className="text-sm text-destructive mb-4">{error}</p>}
|
||
|
||
<div className="mt-6 flex flex-col gap-3 sm:flex-row">
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
onClick={onBack}
|
||
className="flex-1"
|
||
>
|
||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||
上一步
|
||
</Button>
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
onClick={onSkip}
|
||
className="flex-1"
|
||
>
|
||
<LayoutDashboard className="mr-2 h-4 w-4" />
|
||
跳过,直接进入Dashboard
|
||
</Button>
|
||
</div>
|
||
|
||
<Button
|
||
type="button"
|
||
onClick={handleComplete}
|
||
disabled={completing}
|
||
size="lg"
|
||
className="mt-3 w-full max-w-2xl"
|
||
>
|
||
{completing ? (
|
||
<>
|
||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||
正在完成设置...
|
||
</>
|
||
) : (
|
||
<>
|
||
<CheckCircle2 className="mr-2 h-4 w-4" />
|
||
完成设置,进入Dashboard
|
||
</>
|
||
)}
|
||
</Button>
|
||
|
||
<p className="mt-4 text-xs text-muted-foreground">
|
||
您可以随时在品牌详情页查看完整建议
|
||
</p>
|
||
</div>
|
||
);
|
||
}
|