geo/frontend/app/(dashboard)/onboarding/Step5ActionSuggestions.tsx

324 lines
9.9 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 { 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">
&ldquo;{brandName}&rdquo;
</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>
);
}