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:
parent
218ece564d
commit
45e151fc31
|
|
@ -86,7 +86,7 @@ export default function AgentsPage() {
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const params = filterStatus !== "all" ? { status: filterStatus, limit: 50 } : { limit: 50 };
|
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);
|
setTasks(result.items);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "获取执行记录失败");
|
setError(err instanceof Error ? err.message : "获取执行记录失败");
|
||||||
|
|
|
||||||
|
|
@ -278,7 +278,7 @@ function CompetitorWarning({ brandId }: { brandId: string }) {
|
||||||
|
|
||||||
export default function StrategyPage() {
|
export default function StrategyPage() {
|
||||||
const { data: session } = useSession();
|
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 [brandId, setBrandId] = useState<string>("");
|
||||||
const [currentPlan, setCurrentPlan] = useState<GeoPlan | null>(null);
|
const [currentPlan, setCurrentPlan] = useState<GeoPlan | null>(null);
|
||||||
const [generating, setGenerating] = useState(false);
|
const [generating, setGenerating] = useState(false);
|
||||||
|
|
|
||||||
|
|
@ -11,26 +11,24 @@ import {
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
Activity,
|
Activity,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Shield,
|
|
||||||
FileText,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
healthScoreApi,
|
healthScoreApi,
|
||||||
type HealthScoreResponse,
|
type HealthScoreResponse,
|
||||||
type HealthScoreDimension,
|
type HealthScoreDimension,
|
||||||
} from "@/lib/api/health-score";
|
} from "@/lib/api/health-score";
|
||||||
|
import {
|
||||||
|
round,
|
||||||
|
getStatusColor,
|
||||||
|
getProgressBg,
|
||||||
|
DIMENSION_ICONS,
|
||||||
|
} from "@/lib/utils/health-score";
|
||||||
|
|
||||||
interface Step0HealthScoreProps {
|
interface Step0HealthScoreProps {
|
||||||
onNext: (brandName: string, healthScore: HealthScoreResponse | null) => void;
|
onNext: (brandName: string, healthScore: HealthScoreResponse | null) => void;
|
||||||
initialBrandName?: string;
|
initialBrandName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DIMENSION_ICONS: Record<string, React.ElementType> = {
|
|
||||||
"内容可提取性": FileText,
|
|
||||||
"E-E-A-T信号": Shield,
|
|
||||||
"引用就绪度": Activity,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function Step0HealthScore({
|
export function Step0HealthScore({
|
||||||
onNext,
|
onNext,
|
||||||
initialBrandName = "",
|
initialBrandName = "",
|
||||||
|
|
@ -64,25 +62,6 @@ export function Step0HealthScore({
|
||||||
onNext(brandName.trim(), result);
|
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 (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center py-8">
|
<div className="flex flex-col items-center justify-center py-8">
|
||||||
<div className="mb-8 text-center">
|
<div className="mb-8 text-center">
|
||||||
|
|
@ -180,13 +159,13 @@ export function Step0HealthScore({
|
||||||
<Icon className="h-4 w-4 text-muted-foreground" />
|
<Icon className="h-4 w-4 text-muted-foreground" />
|
||||||
<span className="text-sm font-medium">{dim.name}</span>
|
<span className="text-sm font-medium">{dim.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className={`text-sm font-semibold ${getStatusColor(dim.status)}`}>
|
<span className={`text-sm font-semibold ${getStatusColor(dim.percentage)}`}>
|
||||||
{round(dim.percentage, 1)}%
|
{round(dim.percentage, 1)}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-2 w-full overflow-hidden rounded-full bg-gray-100">
|
<div className="h-2 w-full overflow-hidden rounded-full bg-gray-100">
|
||||||
<div
|
<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)}%` }}
|
style={{ width: `${Math.max(dim.percentage, 2)}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -237,8 +216,3 @@ export function Step0HealthScore({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function round(value: number, decimals: number): number {
|
|
||||||
const factor = Math.pow(10, decimals);
|
|
||||||
return Math.round(value * factor) / factor;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,6 @@ export function Step2Competitors({
|
||||||
)) as CompetitorRecommendation[];
|
)) as CompetitorRecommendation[];
|
||||||
setRecommendations(data || []);
|
setRecommendations(data || []);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("获取竞品推荐失败:", err);
|
|
||||||
setError("获取竞品推荐失败,请重试");
|
setError("获取竞品推荐失败,请重试");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,6 @@ import { Check, ArrowRight, ArrowLeft, Monitor, Info } from "lucide-react";
|
||||||
import { PLATFORMS } from "@/lib/platforms";
|
import { PLATFORMS } from "@/lib/platforms";
|
||||||
|
|
||||||
interface Step3PlatformsProps {
|
interface Step3PlatformsProps {
|
||||||
brandName: string;
|
|
||||||
competitors: string[];
|
|
||||||
initialPlatforms?: string[];
|
initialPlatforms?: string[];
|
||||||
onNext: (
|
onNext: (
|
||||||
platforms: string[],
|
platforms: string[],
|
||||||
|
|
@ -19,8 +17,6 @@ interface Step3PlatformsProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Step3Platforms({
|
export function Step3Platforms({
|
||||||
brandName: _brandName,
|
|
||||||
competitors: _competitors,
|
|
||||||
initialPlatforms,
|
initialPlatforms,
|
||||||
onNext,
|
onNext,
|
||||||
onBack,
|
onBack,
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,6 @@ import {
|
||||||
BarChart3,
|
BarChart3,
|
||||||
Trophy,
|
Trophy,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Shield,
|
|
||||||
FileText,
|
|
||||||
Lock,
|
Lock,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
|
|
@ -29,6 +27,12 @@ import {
|
||||||
type BrandHealthReport,
|
type BrandHealthReport,
|
||||||
} from "@/types/onboarding";
|
} from "@/types/onboarding";
|
||||||
import { UpgradePrompt } from "@/components/subscription/UpgradePrompt";
|
import { UpgradePrompt } from "@/components/subscription/UpgradePrompt";
|
||||||
|
import {
|
||||||
|
round,
|
||||||
|
getStatusColor,
|
||||||
|
getProgressBg,
|
||||||
|
DIMENSION_ICONS,
|
||||||
|
} from "@/lib/utils/health-score";
|
||||||
|
|
||||||
interface HealthDimension {
|
interface HealthDimension {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -54,15 +58,6 @@ interface Step4HealthReportProps {
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DIMENSION_ICONS: Record<string, React.ElementType> = {
|
|
||||||
"内容可提取性": FileText,
|
|
||||||
"E-E-A-T信号": Shield,
|
|
||||||
"引用就绪度": Activity,
|
|
||||||
"结构化数据": BarChart3,
|
|
||||||
"语义一致性": Shield,
|
|
||||||
"技术可访问性": Activity,
|
|
||||||
};
|
|
||||||
|
|
||||||
const LOCKED_DIMENSIONS = ["结构化数据", "语义一致性", "技术可访问性"];
|
const LOCKED_DIMENSIONS = ["结构化数据", "语义一致性", "技术可访问性"];
|
||||||
|
|
||||||
export function Step4HealthReport({
|
export function Step4HealthReport({
|
||||||
|
|
@ -76,7 +71,9 @@ export function Step4HealthReport({
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const [report, setReport] = useState<BrandHealthReport | null>(null);
|
const [report, setReport] = useState<BrandHealthReport | null>(null);
|
||||||
const [dimensions, setDimensions] = useState<HealthDimension[]>([]);
|
const [dimensions, setDimensions] = useState<HealthDimension[]>([]);
|
||||||
const [recommendations, setRecommendations] = useState<HealthRecommendation[]>([]);
|
const [recommendations, setRecommendations] = useState<
|
||||||
|
HealthRecommendation[]
|
||||||
|
>([]);
|
||||||
const [isFullReport, setIsFullReport] = useState(false);
|
const [isFullReport, setIsFullReport] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
@ -102,7 +99,6 @@ export function Step4HealthReport({
|
||||||
setRecommendations(data.recommendations || []);
|
setRecommendations(data.recommendations || []);
|
||||||
setIsFullReport(data.is_full_report || false);
|
setIsFullReport(data.is_full_report || false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("获取健康报告失败:", err);
|
|
||||||
setError("获取健康报告失败,请重试");
|
setError("获取健康报告失败,请重试");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
@ -186,25 +182,6 @@ export function Step4HealthReport({
|
||||||
const healthLevel = getHealthLevel(report.overall_score);
|
const healthLevel = getHealthLevel(report.overall_score);
|
||||||
const healthConfig = HEALTH_LEVELS[healthLevel];
|
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(
|
const leadingCount = report.competitor_scores.filter(
|
||||||
(c) => c.is_leading,
|
(c) => c.is_leading,
|
||||||
).length;
|
).length;
|
||||||
|
|
@ -265,10 +242,7 @@ export function Step4HealthReport({
|
||||||
维度评分
|
维度评分
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
{!isFullReport && (
|
{!isFullReport && (
|
||||||
<UpgradePrompt
|
<UpgradePrompt variant="badge" feature="完整6维度诊断" />
|
||||||
variant="badge"
|
|
||||||
feature="完整6维度诊断"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
@ -282,7 +256,9 @@ export function Step4HealthReport({
|
||||||
<Icon className="h-4 w-4 text-muted-foreground" />
|
<Icon className="h-4 w-4 text-muted-foreground" />
|
||||||
<span className="text-sm font-medium">{dim.name}</span>
|
<span className="text-sm font-medium">{dim.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className={`text-sm font-semibold ${getStatusColor(dim.status)}`}>
|
<span
|
||||||
|
className={`text-sm font-semibold ${getStatusColor(dim.percentage)}`}
|
||||||
|
>
|
||||||
{round(dim.percentage, 1)}%
|
{round(dim.percentage, 1)}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -517,8 +493,3 @@ export function Step4HealthReport({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function round(value: number, decimals: number): number {
|
|
||||||
const factor = Math.pow(10, decimals);
|
|
||||||
return Math.round(value * factor) / factor;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,10 @@ import {
|
||||||
Zap,
|
Zap,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import { UpgradePrompt, PaidActionOverlay } from "@/components/subscription/UpgradePrompt";
|
import {
|
||||||
|
UpgradePrompt,
|
||||||
|
PaidActionOverlay,
|
||||||
|
} from "@/components/subscription/UpgradePrompt";
|
||||||
|
|
||||||
interface ActionSuggestionItem {
|
interface ActionSuggestionItem {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|
@ -85,7 +88,6 @@ export function Step5ActionSuggestions({
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [completing, setCompleting] = useState(false);
|
const [completing, setCompleting] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false);
|
|
||||||
|
|
||||||
const fetchSuggestions = async () => {
|
const fetchSuggestions = async () => {
|
||||||
if (!session?.accessToken) return;
|
if (!session?.accessToken) return;
|
||||||
|
|
@ -98,10 +100,11 @@ export function Step5ActionSuggestions({
|
||||||
brandId,
|
brandId,
|
||||||
)) as { suggestions: ActionSuggestionItem[] } | ActionSuggestionItem[];
|
)) 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);
|
setSuggestions(items);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("获取行动建议失败:", err);
|
|
||||||
setError("获取行动建议失败,请重试");
|
setError("获取行动建议失败,请重试");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
@ -117,11 +120,11 @@ export function Step5ActionSuggestions({
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setCompleting(true);
|
setCompleting(true);
|
||||||
|
setError(null);
|
||||||
await api.onboarding.completeOnboarding(session.accessToken, brandId);
|
await api.onboarding.completeOnboarding(session.accessToken, brandId);
|
||||||
onComplete();
|
onComplete();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("完成引导失败:", err);
|
setError("完成引导失败,请重试");
|
||||||
onComplete();
|
|
||||||
} finally {
|
} finally {
|
||||||
setCompleting(false);
|
setCompleting(false);
|
||||||
}
|
}
|
||||||
|
|
@ -129,7 +132,6 @@ export function Step5ActionSuggestions({
|
||||||
|
|
||||||
const handleActionClick = (suggestion: ActionSuggestionItem) => {
|
const handleActionClick = (suggestion: ActionSuggestionItem) => {
|
||||||
if (suggestion.is_paid_action) {
|
if (suggestion.is_paid_action) {
|
||||||
setUpgradeDialogOpen(true);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -180,8 +182,12 @@ export function Step5ActionSuggestions({
|
||||||
<p className="text-muted-foreground">{error}</p>
|
<p className="text-muted-foreground">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Button variant="outline" onClick={fetchSuggestions}>重试</Button>
|
<Button variant="outline" onClick={fetchSuggestions}>
|
||||||
<Button variant="ghost" onClick={onSkip}>稍后查看</Button>
|
重试
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" onClick={onSkip}>
|
||||||
|
稍后查看
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -247,9 +253,7 @@ export function Step5ActionSuggestions({
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{suggestion.description}
|
{suggestion.description}
|
||||||
</p>
|
</p>
|
||||||
{actionButton && (
|
{actionButton && <div className="mt-2">{actionButton}</div>}
|
||||||
<div className="mt-2">{actionButton}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-primary/10">
|
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-primary/10">
|
||||||
|
|
@ -270,7 +274,8 @@ export function Step5ActionSuggestions({
|
||||||
</div>
|
</div>
|
||||||
<h2 className="mb-2 text-2xl font-bold">下一步行动建议</h2>
|
<h2 className="mb-2 text-2xl font-bold">下一步行动建议</h2>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
基于您的品牌 “{brandName}” 的表现,我们为您准备了以下优化建议
|
基于您的品牌 “{brandName}”
|
||||||
|
的表现,我们为您准备了以下优化建议
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,20 +12,9 @@ import { Step3Platforms } from "./Step3Platforms";
|
||||||
import { Step4HealthReport } from "./Step4HealthReport";
|
import { Step4HealthReport } from "./Step4HealthReport";
|
||||||
import { Step5ActionSuggestions } from "./Step5ActionSuggestions";
|
import { Step5ActionSuggestions } from "./Step5ActionSuggestions";
|
||||||
import { useOnboardingData } from "@/lib/hooks/use-onboarding-data";
|
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";
|
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 = {
|
const initialState: OnboardingState = {
|
||||||
currentStep: 0,
|
currentStep: 0,
|
||||||
brandName: "",
|
brandName: "",
|
||||||
|
|
@ -268,8 +257,6 @@ export default function OnboardingPage() {
|
||||||
|
|
||||||
{state.currentStep === 3 && (
|
{state.currentStep === 3 && (
|
||||||
<Step3Platforms
|
<Step3Platforms
|
||||||
brandName={state.brandName}
|
|
||||||
competitors={state.competitors}
|
|
||||||
initialPlatforms={state.platforms}
|
initialPlatforms={state.platforms}
|
||||||
onNext={handleStep3Next}
|
onNext={handleStep3Next}
|
||||||
onBack={handleStep3Back}
|
onBack={handleStep3Back}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { API_BASE } from "./client";
|
import { fetchWithAuth } from "./client";
|
||||||
|
|
||||||
export interface HealthScoreDimension {
|
export interface HealthScoreDimension {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -32,11 +32,6 @@ export const healthScoreApi = {
|
||||||
if (competitors && competitors.length > 0) {
|
if (competitors && competitors.length > 0) {
|
||||||
params.set("competitors", competitors.join(","));
|
params.set("competitors", competitors.join(","));
|
||||||
}
|
}
|
||||||
const res = await fetch(`${API_BASE}/api/v1/public/health-score?${params}`);
|
return fetchWithAuth(`/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();
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
|
@ -22,5 +22,5 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
"exclude": ["node_modules", "e2e"]
|
"exclude": ["node_modules", "e2e", "__tests__", "vitest.setup.ts"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,9 @@ export interface OnboardingState {
|
||||||
competitors: string[];
|
competitors: string[];
|
||||||
platforms: string[];
|
platforms: string[];
|
||||||
frequency: "daily" | "weekly" | "monthly";
|
frequency: "daily" | "weekly" | "monthly";
|
||||||
healthScore: number | null;
|
brandId: string | null;
|
||||||
isCompleted: boolean;
|
healthReport: BrandHealthReport | null;
|
||||||
isSkipped: boolean;
|
preCheckResult: import("@/lib/api/health-score").HealthScoreResponse | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CompetitorRecommendation {
|
export interface CompetitorRecommendation {
|
||||||
|
|
@ -79,9 +79,40 @@ export interface OnboardingStep {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ONBOARDING_STEPS: OnboardingStep[] = [
|
export const ONBOARDING_STEPS: OnboardingStep[] = [
|
||||||
{ id: 0, title: "健康分检测", description: "免费检测品牌GEO健康分", isSkippable: false },
|
{
|
||||||
{ id: 1, title: "创建品牌", description: "输入品牌名称开始监控", isSkippable: false },
|
id: 0,
|
||||||
{ id: 2, title: "确认竞品", description: "选择与您品牌竞争的对手", isSkippable: true },
|
title: "健康分检测",
|
||||||
{ id: 3, title: "健康报告", description: "查看详细诊断报告", isSkippable: false },
|
description: "免费检测品牌GEO健康分",
|
||||||
{ id: 4, title: "行动建议", description: "获取提升品牌曝光的建议", isSkippable: true },
|
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,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue