diff --git a/frontend/app/(dashboard)/dashboard/agents/page.tsx b/frontend/app/(dashboard)/dashboard/agents/page.tsx index fa792be..5091bbe 100644 --- a/frontend/app/(dashboard)/dashboard/agents/page.tsx +++ b/frontend/app/(dashboard)/dashboard/agents/page.tsx @@ -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 : "获取执行记录失败"); diff --git a/frontend/app/(dashboard)/dashboard/strategy/page.tsx b/frontend/app/(dashboard)/dashboard/strategy/page.tsx index da1b41c..72be7aa 100644 --- a/frontend/app/(dashboard)/dashboard/strategy/page.tsx +++ b/frontend/app/(dashboard)/dashboard/strategy/page.tsx @@ -278,7 +278,7 @@ function CompetitorWarning({ brandId }: { brandId: string }) { export default function StrategyPage() { const { data: session } = useSession(); - const token = (session as Record)?.accessToken as string | undefined; + const token = (session as unknown as Record)?.accessToken as string | undefined; const [brandId, setBrandId] = useState(""); const [currentPlan, setCurrentPlan] = useState(null); const [generating, setGenerating] = useState(false); diff --git a/frontend/app/(dashboard)/onboarding/Step0HealthScore.tsx b/frontend/app/(dashboard)/onboarding/Step0HealthScore.tsx index 69ea295..90fbe51 100644 --- a/frontend/app/(dashboard)/onboarding/Step0HealthScore.tsx +++ b/frontend/app/(dashboard)/onboarding/Step0HealthScore.tsx @@ -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 = { - "内容可提取性": 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 (
@@ -180,13 +159,13 @@ export function Step0HealthScore({ {dim.name}
- + {round(dim.percentage, 1)}%
@@ -237,8 +216,3 @@ export function Step0HealthScore({
); } - -function round(value: number, decimals: number): number { - const factor = Math.pow(10, decimals); - return Math.round(value * factor) / factor; -} diff --git a/frontend/app/(dashboard)/onboarding/Step2Competitors.tsx b/frontend/app/(dashboard)/onboarding/Step2Competitors.tsx index a35a00e..5ba9fae 100644 --- a/frontend/app/(dashboard)/onboarding/Step2Competitors.tsx +++ b/frontend/app/(dashboard)/onboarding/Step2Competitors.tsx @@ -57,7 +57,6 @@ export function Step2Competitors({ )) as CompetitorRecommendation[]; setRecommendations(data || []); } catch (err) { - console.error("获取竞品推荐失败:", err); setError("获取竞品推荐失败,请重试"); } finally { setLoading(false); diff --git a/frontend/app/(dashboard)/onboarding/Step3Platforms.tsx b/frontend/app/(dashboard)/onboarding/Step3Platforms.tsx index a712024..c5f5a6c 100644 --- a/frontend/app/(dashboard)/onboarding/Step3Platforms.tsx +++ b/frontend/app/(dashboard)/onboarding/Step3Platforms.tsx @@ -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, diff --git a/frontend/app/(dashboard)/onboarding/Step4HealthReport.tsx b/frontend/app/(dashboard)/onboarding/Step4HealthReport.tsx index 8d4abf6..1e06096 100644 --- a/frontend/app/(dashboard)/onboarding/Step4HealthReport.tsx +++ b/frontend/app/(dashboard)/onboarding/Step4HealthReport.tsx @@ -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 = { - "内容可提取性": 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(null); const [dimensions, setDimensions] = useState([]); - const [recommendations, setRecommendations] = useState([]); + const [recommendations, setRecommendations] = useState< + HealthRecommendation[] + >([]); const [isFullReport, setIsFullReport] = useState(false); const [loading, setLoading] = useState(true); const [error, setError] = useState(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({ 维度评分 {!isFullReport && ( - + )} @@ -282,7 +256,9 @@ export function Step4HealthReport({ {dim.name} - + {round(dim.percentage, 1)}% @@ -517,8 +493,3 @@ export function Step4HealthReport({ ); } - -function round(value: number, decimals: number): number { - const factor = Math.pow(10, decimals); - return Math.round(value * factor) / factor; -} diff --git a/frontend/app/(dashboard)/onboarding/Step5ActionSuggestions.tsx b/frontend/app/(dashboard)/onboarding/Step5ActionSuggestions.tsx index 5e38ce3..173b578 100644 --- a/frontend/app/(dashboard)/onboarding/Step5ActionSuggestions.tsx +++ b/frontend/app/(dashboard)/onboarding/Step5ActionSuggestions.tsx @@ -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(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({

{error}

- - + +
); @@ -247,9 +253,7 @@ export function Step5ActionSuggestions({

{suggestion.description}

- {actionButton && ( -
{actionButton}
- )} + {actionButton &&
{actionButton}
}
@@ -270,7 +274,8 @@ export function Step5ActionSuggestions({

下一步行动建议

- 基于您的品牌 “{brandName}” 的表现,我们为您准备了以下优化建议 + 基于您的品牌 “{brandName}” + 的表现,我们为您准备了以下优化建议

diff --git a/frontend/app/(dashboard)/onboarding/page.tsx b/frontend/app/(dashboard)/onboarding/page.tsx index 64e75b4..c66631e 100644 --- a/frontend/app/(dashboard)/onboarding/page.tsx +++ b/frontend/app/(dashboard)/onboarding/page.tsx @@ -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 && ( 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}`); }, }; diff --git a/frontend/lib/utils/health-score.ts b/frontend/lib/utils/health-score.ts new file mode 100644 index 0000000..636f1f4 --- /dev/null +++ b/frontend/lib/utils/health-score.ts @@ -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 = { + "内容可提取性": FileText, + "E-E-A-T信号": Shield, + "引用就绪度": Activity, + "结构化数据": BarChart3, + "语义一致性": Shield, + "技术可访问性": Activity, +}; diff --git a/frontend/lib/utils.ts b/frontend/lib/utils/index.ts similarity index 100% rename from frontend/lib/utils.ts rename to frontend/lib/utils/index.ts diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 1e7d6c7..4be3818 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -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"] } diff --git a/frontend/types/onboarding.ts b/frontend/types/onboarding.ts index 1d4a3d2..9ce6809 100644 --- a/frontend/types/onboarding.ts +++ b/frontend/types/onboarding.ts @@ -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, + }, ];