298 lines
7.8 KiB
TypeScript
298 lines
7.8 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useEffect, useCallback } from "react";
|
||
import { useSession } from "next-auth/react";
|
||
import { useRouter } from "next/navigation";
|
||
import { Loader2 } from "lucide-react";
|
||
import { OnboardingProgress } from "./OnboardingProgress";
|
||
import { Step1BrandName } from "./Step1BrandName";
|
||
import { Step2Competitors } from "./Step2Competitors";
|
||
import { Step3Platforms } from "./Step3Platforms";
|
||
import { Step4HealthReport } from "./Step4HealthReport";
|
||
import { Step5ActionSuggestions } from "./Step5ActionSuggestions";
|
||
import { api } from "@/lib/api";
|
||
import type { BrandHealthReport } from "@/types/onboarding";
|
||
|
||
interface OnboardingState {
|
||
currentStep: number;
|
||
brandName: string;
|
||
competitors: string[];
|
||
platforms: string[];
|
||
frequency: "daily" | "weekly" | "monthly";
|
||
brandId: string | null;
|
||
healthReport: BrandHealthReport | null;
|
||
}
|
||
|
||
const initialState: OnboardingState = {
|
||
currentStep: 1,
|
||
brandName: "",
|
||
competitors: [],
|
||
platforms: [],
|
||
frequency: "weekly",
|
||
brandId: null,
|
||
healthReport: null,
|
||
};
|
||
|
||
export default function OnboardingPage() {
|
||
const { data: session, status } = useSession();
|
||
const router = useRouter();
|
||
const [state, setState] = useState<OnboardingState>(initialState);
|
||
const [isCreatingBrand, setIsCreatingBrand] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
// 检查用户是否已完成引导
|
||
useEffect(() => {
|
||
const checkOnboardingStatus = async () => {
|
||
if (!session?.accessToken) return;
|
||
|
||
try {
|
||
const data = await api.onboarding.checkOnboardingStatus(
|
||
session.accessToken,
|
||
) as { completed: boolean };
|
||
if (data.completed) {
|
||
router.push("/dashboard");
|
||
}
|
||
} catch (err) {
|
||
// 如果API不可用,继续引导流程
|
||
console.error("检查引导状态失败:", err);
|
||
}
|
||
};
|
||
|
||
if (status === "authenticated") {
|
||
checkOnboardingStatus();
|
||
}
|
||
}, [session?.accessToken, status, router]);
|
||
|
||
// 创建品牌
|
||
const createBrand = useCallback(async () => {
|
||
if (!session?.accessToken || !state.brandName) return null;
|
||
|
||
try {
|
||
setIsCreatingBrand(true);
|
||
setError(null);
|
||
const data = await api.onboarding.createOnboardingBrand(
|
||
session.accessToken,
|
||
{
|
||
name: state.brandName,
|
||
competitors: state.competitors,
|
||
platforms: state.platforms,
|
||
frequency: state.frequency,
|
||
},
|
||
) as { brand_id: string };
|
||
return data.brand_id;
|
||
} catch (err) {
|
||
console.error("创建品牌失败:", err);
|
||
setError(err instanceof Error ? err.message : "创建品牌失败");
|
||
return null;
|
||
} finally {
|
||
setIsCreatingBrand(false);
|
||
}
|
||
}, [session?.accessToken, state.brandName, state.competitors, state.platforms, state.frequency]);
|
||
|
||
// Step 1: 品牌名称
|
||
const handleStep1Next = (brandName: string) => {
|
||
setState((prev) => ({
|
||
...prev,
|
||
brandName,
|
||
currentStep: 2,
|
||
}));
|
||
};
|
||
|
||
const handleStep1Skip = () => {
|
||
router.push("/dashboard");
|
||
};
|
||
|
||
// Step 2: 竞品
|
||
const handleStep2Next = (competitors: string[]) => {
|
||
setState((prev) => ({
|
||
...prev,
|
||
competitors,
|
||
currentStep: 3,
|
||
}));
|
||
};
|
||
|
||
const handleStep2Back = () => {
|
||
setState((prev) => ({
|
||
...prev,
|
||
currentStep: 1,
|
||
}));
|
||
};
|
||
|
||
const handleStep2Skip = async () => {
|
||
setState((prev) => ({
|
||
...prev,
|
||
competitors: [],
|
||
currentStep: 3,
|
||
}));
|
||
};
|
||
|
||
// Step 3: 平台
|
||
const handleStep3Next = async (platforms: string[], frequency: "daily" | "weekly" | "monthly") => {
|
||
const brandId = await createBrand();
|
||
if (brandId) {
|
||
setState((prev) => ({
|
||
...prev,
|
||
platforms,
|
||
frequency,
|
||
brandId,
|
||
currentStep: 4,
|
||
}));
|
||
}
|
||
};
|
||
|
||
const handleStep3Back = () => {
|
||
setState((prev) => ({
|
||
...prev,
|
||
currentStep: 2,
|
||
}));
|
||
};
|
||
|
||
const handleStep3Skip = async () => {
|
||
// 使用默认平台
|
||
const defaultPlatforms = ["wenxin", "kimi", "tongyi", "baidu_ai", "yuanbao", "qingyan", "doubao"];
|
||
const brandId = await createBrand();
|
||
if (brandId) {
|
||
setState((prev) => ({
|
||
...prev,
|
||
platforms: defaultPlatforms,
|
||
brandId,
|
||
currentStep: 4,
|
||
}));
|
||
}
|
||
};
|
||
|
||
// Step 4: 健康报告
|
||
const handleStep4Next = (report: BrandHealthReport) => {
|
||
setState((prev) => ({
|
||
...prev,
|
||
healthReport: report,
|
||
currentStep: 5,
|
||
}));
|
||
};
|
||
|
||
const handleStep4Back = () => {
|
||
setState((prev) => ({
|
||
...prev,
|
||
currentStep: 3,
|
||
}));
|
||
};
|
||
|
||
// Step 5: 行动建议
|
||
const handleStep5Complete = () => {
|
||
router.push("/dashboard");
|
||
};
|
||
|
||
const handleStep5Back = () => {
|
||
if (state.brandId) {
|
||
setState((prev) => ({
|
||
...prev,
|
||
currentStep: 4,
|
||
}));
|
||
}
|
||
};
|
||
|
||
const handleStep5Skip = () => {
|
||
router.push("/dashboard");
|
||
};
|
||
|
||
// 加载状态
|
||
if (status === "loading") {
|
||
return (
|
||
<div className="flex min-h-screen items-center justify-center">
|
||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 未登录重定向
|
||
if (status === "unauthenticated") {
|
||
router.push("/login");
|
||
return null;
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-b from-background to-muted/20 px-4 py-8">
|
||
<div className="mx-auto max-w-4xl">
|
||
{/* 顶部 Logo */}
|
||
<div className="mb-8 text-center">
|
||
<h1 className="text-2xl font-bold">GEO 平台</h1>
|
||
<p className="text-muted-foreground">新用户引导</p>
|
||
</div>
|
||
|
||
{/* 进度指示器 */}
|
||
<div className="mb-8">
|
||
<OnboardingProgress currentStep={state.currentStep} />
|
||
</div>
|
||
|
||
{/* 错误提示 */}
|
||
{error && (
|
||
<div className="mb-4 rounded-lg bg-destructive/10 p-4 text-destructive text-center">
|
||
{error}
|
||
</div>
|
||
)}
|
||
|
||
{/* 创建品牌加载状态 */}
|
||
{isCreatingBrand && (
|
||
<div className="mb-4 rounded-lg bg-primary/10 p-4 text-center">
|
||
<Loader2 className="mx-auto h-6 w-6 animate-spin text-primary" />
|
||
<p className="mt-2 text-sm">正在创建品牌...</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* 步骤内容 */}
|
||
<div className="mt-8">
|
||
{state.currentStep === 1 && (
|
||
<Step1BrandName
|
||
initialValue={state.brandName}
|
||
onNext={handleStep1Next}
|
||
onSkip={handleStep1Skip}
|
||
/>
|
||
)}
|
||
|
||
{state.currentStep === 2 && (
|
||
<Step2Competitors
|
||
brandName={state.brandName}
|
||
initialCompetitors={state.competitors}
|
||
onNext={handleStep2Next}
|
||
onBack={handleStep2Back}
|
||
onSkip={handleStep2Skip}
|
||
/>
|
||
)}
|
||
|
||
{state.currentStep === 3 && (
|
||
<Step3Platforms
|
||
brandName={state.brandName}
|
||
competitors={state.competitors}
|
||
initialPlatforms={state.platforms}
|
||
onNext={handleStep3Next}
|
||
onBack={handleStep3Back}
|
||
onSkip={handleStep3Skip}
|
||
/>
|
||
)}
|
||
|
||
{state.currentStep === 4 && state.brandId && (
|
||
<Step4HealthReport
|
||
brandId={state.brandId}
|
||
brandName={state.brandName}
|
||
competitors={state.competitors}
|
||
platforms={state.platforms}
|
||
onNext={handleStep4Next}
|
||
onBack={handleStep4Back}
|
||
/>
|
||
)}
|
||
|
||
{state.currentStep === 5 && state.brandId && (
|
||
<Step5ActionSuggestions
|
||
brandId={state.brandId}
|
||
brandName={state.brandName}
|
||
onComplete={handleStep5Complete}
|
||
onBack={handleStep5Back}
|
||
onSkip={handleStep5Skip}
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|