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

219 lines
7.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 } from "react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import {
Loader2,
Sparkles,
ArrowRight,
Activity,
AlertTriangle,
} from "lucide-react";
import {
healthScoreApi,
type HealthScoreResponse,
type HealthScoreDimension,
} from "@/lib/api/health-score";
import {
round,
getStatusColor,
getProgressBg,
DIMENSION_ICONS,
} from "@/lib/utils/health-score";
interface Step0HealthScoreProps {
onNext: (brandName: string, healthScore: HealthScoreResponse | null) => void;
initialBrandName?: string;
}
export function Step0HealthScore({
onNext,
initialBrandName = "",
}: Step0HealthScoreProps) {
const [brandName, setBrandName] = useState(initialBrandName);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [result, setResult] = useState<HealthScoreResponse | null>(null);
const handleCheck = async () => {
const trimmed = brandName.trim();
if (!trimmed || trimmed.length < 2) {
setError("请输入至少2个字符的品牌名称");
return;
}
setLoading(true);
setError(null);
try {
const data = await healthScoreApi.getHealthScore(trimmed);
setResult(data);
} catch (err) {
setError(err instanceof Error ? err.message : "检测失败,请重试");
} finally {
setLoading(false);
}
};
const handleNext = () => {
onNext(brandName.trim(), result);
};
return (
<div className="flex flex-col items-center justify-center py-8">
<div className="mb-8 text-center">
<div className="mb-4 inline-flex h-16 w-16 items-center justify-center rounded-full bg-primary/10">
<Sparkles className="h-8 w-8 text-primary" />
</div>
<h2 className="mb-2 text-2xl font-bold">GEO健康分</h2>
<p className="text-muted-foreground">
AI搜索中的表现
</p>
</div>
<Card className="w-full max-w-md">
<CardContent className="pt-6">
<div className="space-y-4">
<div className="flex gap-2">
<Input
value={brandName}
onChange={(e) => {
setBrandName(e.target.value);
setError(null);
}}
placeholder="输入品牌名称,例如:华为"
className="h-12 text-base"
maxLength={50}
autoFocus
onKeyDown={(e) => {
if (e.key === "Enter" && !loading) {
handleCheck();
}
}}
/>
<Button
onClick={handleCheck}
disabled={loading || !brandName.trim()}
size="lg"
className="h-12 px-6"
>
{loading ? (
<Loader2 className="h-5 w-5 animate-spin" />
) : (
<>
<ArrowRight className="ml-1 h-4 w-4" />
</>
)}
</Button>
</div>
{error && (
<div className="flex items-center gap-2 rounded-lg bg-destructive/10 p-3 text-destructive text-sm">
<AlertTriangle className="h-4 w-4 shrink-0" />
{error}
</div>
)}
</div>
</CardContent>
</Card>
{result && (
<div className="mt-6 w-full max-w-lg space-y-4">
<Card className="border-2 border-primary/20">
<CardContent className="pt-6">
<div className="flex flex-col items-center">
<div className="relative mb-3">
<span className="text-6xl font-bold">
{result.overall_score}
</span>
<span className="absolute top-1 text-xl text-muted-foreground">
/100
</span>
</div>
<Badge
variant="secondary"
className="text-base px-4 py-1 mb-2"
>
{result.health_level_label}
</Badge>
<p className="text-sm text-muted-foreground">
{result.brand_name} GEO健康分
</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6 space-y-4">
<h3 className="font-semibold text-base"></h3>
{result.dimensions.map((dim: HealthScoreDimension) => {
const Icon = DIMENSION_ICONS[dim.name] || Activity;
return (
<div key={dim.name} className="space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Icon className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium">{dim.name}</span>
</div>
<span className={`text-sm font-semibold ${getStatusColor(dim.percentage)}`}>
{round(dim.percentage, 1)}%
</span>
</div>
<div className="h-2 w-full overflow-hidden rounded-full bg-gray-100">
<div
className={`h-full rounded-full transition-all ${getProgressBg(dim.percentage)}`}
style={{ width: `${Math.max(dim.percentage, 2)}%` }}
/>
</div>
</div>
);
})}
{result.recommendations.length > 0 && (
<div className="border-t pt-4 space-y-2">
<h4 className="text-sm font-semibold"></h4>
{result.recommendations.slice(0, 3).map((rec, i) => (
<div
key={i}
className="flex items-start gap-2 text-sm text-muted-foreground"
>
<span className="mt-0.5 h-1.5 w-1.5 shrink-0 rounded-full bg-amber-500" />
<span>{rec.title}</span>
</div>
))}
</div>
)}
<div className="border-t pt-4">
<p className="text-xs text-muted-foreground text-center">
3 · Pro解锁完整6维度诊断
</p>
</div>
</CardContent>
</Card>
<Button onClick={handleNext} size="lg" className="w-full">
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
<p className="text-xs text-center text-muted-foreground">
</p>
</div>
)}
{!result && !loading && (
<div className="mt-8 flex items-center gap-2 text-sm text-muted-foreground">
<div className="h-2 w-2 rounded-full bg-primary" />
<span>GEO健康分</span>
</div>
)}
</div>
);
}