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

248 lines
8.4 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 { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import {
Loader2,
Users,
Plus,
X,
Check,
ArrowRight,
ArrowLeft,
Lightbulb,
} from "lucide-react";
import { api } from "@/lib/api";
import type { CompetitorRecommendation } from "@/types/onboarding";
interface Step2CompetitorsProps {
brandName: string;
initialCompetitors?: string[];
onNext: (competitors: string[]) => void;
onBack: () => void;
onSkip: () => void;
}
export function Step2Competitors({
brandName,
initialCompetitors = [],
onNext,
onBack,
onSkip,
}: Step2CompetitorsProps) {
const { data: session } = useSession();
const [recommendations, setRecommendations] = useState<
CompetitorRecommendation[]
>([]);
const [selectedCompetitors, setSelectedCompetitors] =
useState<string[]>(initialCompetitors);
const [customCompetitor, setCustomCompetitor] = useState("");
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchRecommendations = async () => {
if (!session?.accessToken) return;
try {
setLoading(true);
setError(null);
const data = (await api.onboarding.getCompetitorRecommendations(
session.accessToken,
brandName,
)) as CompetitorRecommendation[];
setRecommendations(data || []);
} catch (err) {
console.error("获取竞品推荐失败:", err);
setError("获取竞品推荐失败,请重试");
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchRecommendations();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [session?.accessToken, brandName]);
const toggleCompetitor = (competitorName: string) => {
setSelectedCompetitors((prev) =>
prev.includes(competitorName)
? prev.filter((c) => c !== competitorName)
: [...prev, competitorName],
);
};
const handleAddCustomCompetitor = () => {
const trimmed = customCompetitor.trim();
if (trimmed && !selectedCompetitors.includes(trimmed)) {
setSelectedCompetitors((prev) => [...prev, trimmed]);
setCustomCompetitor("");
}
};
const handleRemoveCompetitor = (competitorName: string) => {
setSelectedCompetitors((prev) => prev.filter((c) => c !== competitorName));
};
const handleNext = () => {
onNext(selectedCompetitors);
};
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-blue-50">
<Users className="h-8 w-8 text-blue-600" />
</div>
<h2 className="mb-2 text-2xl font-bold"></h2>
<p className="text-muted-foreground">
&ldquo;{brandName}&rdquo;
</p>
</div>
<Card className="w-full max-w-2xl">
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-lg">
<Lightbulb className="h-5 w-5 text-amber-500" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{loading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
<span className="ml-2 text-muted-foreground">...</span>
</div>
) : error ? (
<div className="text-center py-8">
<p className="text-muted-foreground mb-4">{error}</p>
<div className="flex gap-3 justify-center">
<Button variant="outline" onClick={fetchRecommendations}></Button>
<Button variant="ghost" onClick={onSkip}></Button>
</div>
</div>
) : recommendations.length > 0 ? (
<div className="grid gap-3 sm:grid-cols-2">
{recommendations.map((comp) => {
const isSelected = selectedCompetitors.includes(comp.name);
return (
<button
key={comp.id}
type="button"
onClick={() => toggleCompetitor(comp.name)}
className={`flex items-start gap-3 rounded-lg border p-3 text-left transition-all hover:border-primary/50 ${
isSelected
? "border-primary bg-primary/5 ring-1 ring-primary"
: "border-border bg-card"
}`}
>
<div
className={`mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded border ${
isSelected
? "border-primary bg-primary text-primary-foreground"
: "border-muted-foreground/30"
}`}
>
{isSelected && <Check className="h-3 w-3" />}
</div>
<div className="flex-1 min-w-0">
<p className="font-medium truncate">{comp.name}</p>
<p className="text-xs text-muted-foreground truncate">
{comp.reason}
</p>
</div>
</button>
);
})}
</div>
) : (
<p className="text-center text-muted-foreground py-4">
</p>
)}
{/* 已选择的竞品 */}
{selectedCompetitors.length > 0 && (
<div className="border-t pt-4">
<Label className="text-sm text-muted-foreground mb-2 block">
({selectedCompetitors.length})
</Label>
<div className="flex flex-wrap gap-2">
{selectedCompetitors.map((comp) => (
<Badge key={comp} variant="secondary" className="gap-1 pr-1">
{comp}
<button
type="button"
onClick={() => handleRemoveCompetitor(comp)}
className="ml-1 rounded-full hover:bg-muted p-0.5"
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
</div>
</div>
)}
{/* 手动添加竞品 */}
<div className="border-t pt-4">
<Label className="text-sm mb-2 block"></Label>
<div className="flex gap-2">
<Input
value={customCompetitor}
onChange={(e) => setCustomCompetitor(e.target.value)}
placeholder="输入竞品名称"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
handleAddCustomCompetitor();
}
}}
className="flex-1"
/>
<Button
type="button"
variant="outline"
onClick={handleAddCustomCompetitor}
disabled={!customCompetitor.trim()}
>
<Plus className="h-4 w-4" />
</Button>
</div>
</div>
</CardContent>
</Card>
<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" onClick={handleNext} className="flex-1">
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
<Button type="button" variant="ghost" onClick={onSkip} className="mt-2">
</Button>
<p className="mt-4 text-xs text-muted-foreground">
</p>
</div>
);
}