fix: frontend quality improvements

- Fix ONBOARDING_STEPS count (5→6) to match actual flow
- Unify OnboardingState type (remove duplicate from page.tsx)
- Replace raw fetch with fetchWithAuth in health-score.ts
- Extract shared utils (round, getStatusColor, DIMENSION_ICONS) to lib/utils/health-score.ts
- Fix Step5 handleComplete silent failure on error
- Remove console.error from Step2/Step4/Step5
- Remove unused props from Step3Platforms
- Fix TS errors in agents/page.tsx and strategy/page.tsx
- Exclude test files from tsc (handled by vitest)
This commit is contained in:
chiguyong 2026-06-01 23:41:27 +08:00
parent 218ece564d
commit 45e151fc31
13 changed files with 112 additions and 126 deletions

View File

@ -86,7 +86,7 @@ export default function AgentsPage() {
setError(null);
try {
const params = filterStatus !== "all" ? { status: filterStatus, limit: 50 } : { limit: 50 };
const result = await agentsApi.listTasks(token, params);
const result = await agentsApi.listTasks(token!, params);
setTasks(result.items);
} catch (err) {
setError(err instanceof Error ? err.message : "获取执行记录失败");

View File

@ -278,7 +278,7 @@ function CompetitorWarning({ brandId }: { brandId: string }) {
export default function StrategyPage() {
const { data: session } = useSession();
const token = (session as Record<string, unknown>)?.accessToken as string | undefined;
const token = (session as unknown as Record<string, unknown>)?.accessToken as string | undefined;
const [brandId, setBrandId] = useState<string>("");
const [currentPlan, setCurrentPlan] = useState<GeoPlan | null>(null);
const [generating, setGenerating] = useState(false);

View File

@ -11,26 +11,24 @@ import {
ArrowRight,
Activity,
AlertTriangle,
Shield,
FileText,
} 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;
}
const DIMENSION_ICONS: Record<string, React.ElementType> = {
"内容可提取性": FileText,
"E-E-A-T信号": Shield,
"引用就绪度": Activity,
};
export function Step0HealthScore({
onNext,
initialBrandName = "",
@ -64,25 +62,6 @@ export function Step0HealthScore({
onNext(brandName.trim(), result);
};
const getStatusColor = (status: string) => {
switch (status) {
case "good":
return "text-emerald-600";
case "warning":
return "text-amber-600";
case "fail":
return "text-red-600";
default:
return "text-muted-foreground";
}
};
const getProgressColor = (percentage: number) => {
if (percentage >= 60) return "bg-emerald-500";
if (percentage >= 30) return "bg-amber-500";
return "bg-red-500";
};
return (
<div className="flex flex-col items-center justify-center py-8">
<div className="mb-8 text-center">
@ -180,13 +159,13 @@ export function Step0HealthScore({
<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.status)}`}>
<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 ${getProgressColor(dim.percentage)}`}
className={`h-full rounded-full transition-all ${getProgressBg(dim.percentage)}`}
style={{ width: `${Math.max(dim.percentage, 2)}%` }}
/>
</div>
@ -237,8 +216,3 @@ export function Step0HealthScore({
</div>
);
}
function round(value: number, decimals: number): number {
const factor = Math.pow(10, decimals);
return Math.round(value * factor) / factor;
}

View File

@ -57,7 +57,6 @@ export function Step2Competitors({
)) as CompetitorRecommendation[];
setRecommendations(data || []);
} catch (err) {
console.error("获取竞品推荐失败:", err);
setError("获取竞品推荐失败,请重试");
} finally {
setLoading(false);

View File

@ -7,8 +7,6 @@ import { Check, ArrowRight, ArrowLeft, Monitor, Info } from "lucide-react";
import { PLATFORMS } from "@/lib/platforms";
interface Step3PlatformsProps {
brandName: string;
competitors: string[];
initialPlatforms?: string[];
onNext: (
platforms: string[],
@ -19,8 +17,6 @@ interface Step3PlatformsProps {
}
export function Step3Platforms({
brandName: _brandName,
competitors: _competitors,
initialPlatforms,
onNext,
onBack,

View File

@ -17,8 +17,6 @@ import {
BarChart3,
Trophy,
AlertTriangle,
Shield,
FileText,
Lock,
} from "lucide-react";
import { api } from "@/lib/api";
@ -29,6 +27,12 @@ import {
type BrandHealthReport,
} from "@/types/onboarding";
import { UpgradePrompt } from "@/components/subscription/UpgradePrompt";
import {
round,
getStatusColor,
getProgressBg,
DIMENSION_ICONS,
} from "@/lib/utils/health-score";
interface HealthDimension {
name: string;
@ -54,15 +58,6 @@ interface Step4HealthReportProps {
onBack: () => void;
}
const DIMENSION_ICONS: Record<string, React.ElementType> = {
"内容可提取性": FileText,
"E-E-A-T信号": Shield,
"引用就绪度": Activity,
"结构化数据": BarChart3,
"语义一致性": Shield,
"技术可访问性": Activity,
};
const LOCKED_DIMENSIONS = ["结构化数据", "语义一致性", "技术可访问性"];
export function Step4HealthReport({
@ -76,7 +71,9 @@ export function Step4HealthReport({
const { data: session } = useSession();
const [report, setReport] = useState<BrandHealthReport | null>(null);
const [dimensions, setDimensions] = useState<HealthDimension[]>([]);
const [recommendations, setRecommendations] = useState<HealthRecommendation[]>([]);
const [recommendations, setRecommendations] = useState<
HealthRecommendation[]
>([]);
const [isFullReport, setIsFullReport] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@ -102,7 +99,6 @@ export function Step4HealthReport({
setRecommendations(data.recommendations || []);
setIsFullReport(data.is_full_report || false);
} catch (err) {
console.error("获取健康报告失败:", err);
setError("获取健康报告失败,请重试");
} finally {
setLoading(false);
@ -186,25 +182,6 @@ export function Step4HealthReport({
const healthLevel = getHealthLevel(report.overall_score);
const healthConfig = HEALTH_LEVELS[healthLevel];
const getStatusColor = (status: string) => {
switch (status) {
case "good":
return "text-emerald-600";
case "warning":
return "text-amber-600";
case "fail":
return "text-red-600";
default:
return "text-muted-foreground";
}
};
const getProgressBg = (percentage: number) => {
if (percentage >= 60) return "bg-emerald-500";
if (percentage >= 30) return "bg-amber-500";
return "bg-red-500";
};
const leadingCount = report.competitor_scores.filter(
(c) => c.is_leading,
).length;
@ -265,10 +242,7 @@ export function Step4HealthReport({
</CardTitle>
{!isFullReport && (
<UpgradePrompt
variant="badge"
feature="完整6维度诊断"
/>
<UpgradePrompt variant="badge" feature="完整6维度诊断" />
)}
</div>
</CardHeader>
@ -282,7 +256,9 @@ export function Step4HealthReport({
<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.status)}`}>
<span
className={`text-sm font-semibold ${getStatusColor(dim.percentage)}`}
>
{round(dim.percentage, 1)}%
</span>
</div>
@ -517,8 +493,3 @@ export function Step4HealthReport({
</div>
);
}
function round(value: number, decimals: number): number {
const factor = Math.pow(10, decimals);
return Math.round(value * factor) / factor;
}

View File

@ -23,7 +23,10 @@ import {
Zap,
} from "lucide-react";
import { api } from "@/lib/api";
import { UpgradePrompt, PaidActionOverlay } from "@/components/subscription/UpgradePrompt";
import {
UpgradePrompt,
PaidActionOverlay,
} from "@/components/subscription/UpgradePrompt";
interface ActionSuggestionItem {
id?: string;
@ -85,7 +88,6 @@ export function Step5ActionSuggestions({
const [loading, setLoading] = useState(true);
const [completing, setCompleting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false);
const fetchSuggestions = async () => {
if (!session?.accessToken) return;
@ -98,10 +100,11 @@ export function Step5ActionSuggestions({
brandId,
)) as { suggestions: ActionSuggestionItem[] } | ActionSuggestionItem[];
const items = Array.isArray(data) ? data : (data as { suggestions: ActionSuggestionItem[] }).suggestions || [];
const items = Array.isArray(data)
? data
: (data as { suggestions: ActionSuggestionItem[] }).suggestions || [];
setSuggestions(items);
} catch (err) {
console.error("获取行动建议失败:", err);
setError("获取行动建议失败,请重试");
} finally {
setLoading(false);
@ -117,11 +120,11 @@ export function Step5ActionSuggestions({
try {
setCompleting(true);
setError(null);
await api.onboarding.completeOnboarding(session.accessToken, brandId);
onComplete();
} catch (err) {
console.error("完成引导失败:", err);
onComplete();
setError("完成引导失败,请重试");
} finally {
setCompleting(false);
}
@ -129,7 +132,6 @@ export function Step5ActionSuggestions({
const handleActionClick = (suggestion: ActionSuggestionItem) => {
if (suggestion.is_paid_action) {
setUpgradeDialogOpen(true);
return;
}
@ -180,8 +182,12 @@ export function Step5ActionSuggestions({
<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>
<Button variant="outline" onClick={fetchSuggestions}>
</Button>
<Button variant="ghost" onClick={onSkip}>
</Button>
</div>
</div>
);
@ -247,9 +253,7 @@ export function Step5ActionSuggestions({
<p className="text-sm text-muted-foreground">
{suggestion.description}
</p>
{actionButton && (
<div className="mt-2">{actionButton}</div>
)}
{actionButton && <div className="mt-2">{actionButton}</div>}
</div>
<div className="flex items-center">
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-primary/10">
@ -270,7 +274,8 @@ export function Step5ActionSuggestions({
</div>
<h2 className="mb-2 text-2xl font-bold"></h2>
<p className="text-muted-foreground">
&ldquo;{brandName}&rdquo;
&ldquo;{brandName}&rdquo;
</p>
</div>

View File

@ -12,20 +12,9 @@ import { Step3Platforms } from "./Step3Platforms";
import { Step4HealthReport } from "./Step4HealthReport";
import { Step5ActionSuggestions } from "./Step5ActionSuggestions";
import { useOnboardingData } from "@/lib/hooks/use-onboarding-data";
import type { BrandHealthReport } from "@/types/onboarding";
import type { BrandHealthReport, OnboardingState } from "@/types/onboarding";
import type { HealthScoreResponse } from "@/lib/api/health-score";
interface OnboardingState {
currentStep: number;
brandName: string;
competitors: string[];
platforms: string[];
frequency: "daily" | "weekly" | "monthly";
brandId: string | null;
healthReport: BrandHealthReport | null;
preCheckResult: HealthScoreResponse | null;
}
const initialState: OnboardingState = {
currentStep: 0,
brandName: "",
@ -268,8 +257,6 @@ export default function OnboardingPage() {
{state.currentStep === 3 && (
<Step3Platforms
brandName={state.brandName}
competitors={state.competitors}
initialPlatforms={state.platforms}
onNext={handleStep3Next}
onBack={handleStep3Back}

View File

@ -1,4 +1,4 @@
import { API_BASE } from "./client";
import { fetchWithAuth } from "./client";
export interface HealthScoreDimension {
name: string;
@ -32,11 +32,6 @@ export const healthScoreApi = {
if (competitors && competitors.length > 0) {
params.set("competitors", competitors.join(","));
}
const res = await fetch(`${API_BASE}/api/v1/public/health-score?${params}`);
if (!res.ok) {
const error = await res.json().catch(() => ({}));
throw new Error(error.detail || `请求失败 (HTTP ${res.status})`);
}
return res.json();
return fetchWithAuth(`/api/v1/public/health-score?${params}`);
},
};

View File

@ -0,0 +1,28 @@
import type { ElementType } from "react";
import { FileText, Shield, Activity, BarChart3 } from "lucide-react";
export function round(value: number, decimals = 1): number {
const factor = Math.pow(10, decimals);
return Math.round(value * factor) / factor;
}
export function getStatusColor(score: number): string {
if (score >= 70) return "text-green-500";
if (score >= 40) return "text-yellow-500";
return "text-red-500";
}
export function getProgressBg(score: number): string {
if (score >= 70) return "bg-green-500";
if (score >= 40) return "bg-yellow-500";
return "bg-red-500";
}
export const DIMENSION_ICONS: Record<string, ElementType> = {
"内容可提取性": FileText,
"E-E-A-T信号": Shield,
"引用就绪度": Activity,
"结构化数据": BarChart3,
"语义一致性": Shield,
"技术可访问性": Activity,
};

View File

@ -22,5 +22,5 @@
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules", "e2e"]
"exclude": ["node_modules", "e2e", "__tests__", "vitest.setup.ts"]
}

View File

@ -12,9 +12,9 @@ export interface OnboardingState {
competitors: string[];
platforms: string[];
frequency: "daily" | "weekly" | "monthly";
healthScore: number | null;
isCompleted: boolean;
isSkipped: boolean;
brandId: string | null;
healthReport: BrandHealthReport | null;
preCheckResult: import("@/lib/api/health-score").HealthScoreResponse | null;
}
export interface CompetitorRecommendation {
@ -79,9 +79,40 @@ export interface OnboardingStep {
}
export const ONBOARDING_STEPS: OnboardingStep[] = [
{ id: 0, title: "健康分检测", description: "免费检测品牌GEO健康分", isSkippable: false },
{ id: 1, title: "创建品牌", description: "输入品牌名称开始监控", isSkippable: false },
{ id: 2, title: "确认竞品", description: "选择与您品牌竞争的对手", isSkippable: true },
{ id: 3, title: "健康报告", description: "查看详细诊断报告", isSkippable: false },
{ id: 4, title: "行动建议", description: "获取提升品牌曝光的建议", isSkippable: true },
{
id: 0,
title: "健康分检测",
description: "免费检测品牌GEO健康分",
isSkippable: false,
},
{
id: 1,
title: "创建品牌",
description: "输入品牌名称开始监控",
isSkippable: false,
},
{
id: 2,
title: "确认竞品",
description: "选择与您品牌竞争的对手",
isSkippable: true,
},
{
id: 3,
title: "平台选择",
description: "选择监控平台和频率",
isSkippable: false,
},
{
id: 4,
title: "健康报告",
description: "查看详细诊断报告",
isSkippable: false,
},
{
id: 5,
title: "行动建议",
description: "获取提升品牌曝光的建议",
isSkippable: true,
},
];