geo/frontend/app/(dashboard)/dashboard/content/editor/page.tsx

514 lines
18 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, useCallback, ChangeEvent } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Progress } from "@/components/ui/progress";
import { useToast } from "@/hooks/use-toast";
import { fetchWithAuth } from "@/lib/api/client";
import { platformRulesApi, PlatformBrief, PlatformDetailResponse, ContentValidationResponse } from "@/lib/api/platform-rules";
interface OptimizedContent {
title: string;
content: string;
platform: string;
tips: string[];
stages?: Array<{
stage: string;
status: string;
word_count?: number;
}>;
}
// 平台配置映射含emoji图标
const platformIcons: Record<string, string> = {
zhihu: "📖",
wechat: "📱",
baijiahao: "📰",
toutiao: "📢",
xiaohongshu: "📕",
default: "🌐",
};
export default function ContentEditorPage() {
const { toast } = useToast();
const [platforms, setPlatforms] = useState<PlatformBrief[]>([]);
const [platformDetail, setPlatformDetail] = useState<PlatformDetailResponse | null>(null);
const [selectedPlatform, setSelectedPlatform] = useState<string>("zhihu");
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [optimizedContent, setOptimizedContent] = useState<OptimizedContent | null>(null);
const [validationResult, setValidationResult] = useState<ContentValidationResponse | null>(null);
const [loading, setLoading] = useState(true);
const [validating, setValidating] = useState(false);
const [optimizing, setOptimizing] = useState(false);
const [optimizeProgress, setOptimizeProgress] = useState(0);
useEffect(() => {
loadPlatforms();
}, []);
// 加载平台详情(当选中平台变化时)
useEffect(() => {
if (selectedPlatform) {
loadPlatformDetail(selectedPlatform);
}
}, [selectedPlatform]);
const loadPlatforms = async () => {
try {
setLoading(true);
const response = await platformRulesApi.listPlatforms();
setPlatforms(response.platforms);
} catch (error) {
console.error("加载平台列表失败:", error);
toast({
title: "加载失败",
description: "无法加载平台列表,请刷新页面重试",
variant: "destructive",
});
} finally {
setLoading(false);
}
};
const loadPlatformDetail = async (platformId: string) => {
try {
const detail = await platformRulesApi.getPlatformDetail(platformId);
setPlatformDetail(detail);
} catch (error) {
console.error("加载平台详情失败:", error);
setPlatformDetail(null);
}
};
const handleValidate = async () => {
if (!content || !title) {
toast({
title: "验证失败",
description: "请先填写标题和内容",
variant: "destructive",
});
return;
}
try {
setValidating(true);
const result = await platformRulesApi.validateContent(selectedPlatform, content, title);
setValidationResult(result);
if (result.is_valid) {
toast({
title: "验证通过",
description: `内容得分: ${result.score}`,
});
} else {
toast({
title: "验证未通过",
description: `发现 ${result.issues.length} 个问题需要修复`,
variant: "destructive",
});
}
} catch (error) {
console.error("验证失败:", error);
toast({
title: "验证失败",
description: "内容验证过程中出现错误",
variant: "destructive",
});
} finally {
setValidating(false);
}
};
const handleOptimize = async () => {
if (!content || !title) {
toast({
title: "优化失败",
description: "请先填写标题和内容",
variant: "destructive",
});
return;
}
try {
setOptimizing(true);
setOptimizeProgress(0);
// 阶段1: 去AI化
setOptimizeProgress(10);
const deaiResult = await fetchWithAuth(`/api/v1/content/deai`, {
method: "POST",
body: JSON.stringify({ content, title }),
}).catch(() => ({ content }));
setOptimizeProgress(40);
let processedContent = deaiResult.content || content;
// 阶段2: 敏感词过滤
setOptimizeProgress(50);
const sensitiveResult = await fetchWithAuth(`/api/v1/content/filter-sensitive`, {
method: "POST",
body: JSON.stringify({ content: processedContent, platform: selectedPlatform }),
}).catch(() => ({ content: processedContent }));
setOptimizeProgress(70);
processedContent = sensitiveResult.content || processedContent;
// 阶段3: SEO优化
setOptimizeProgress(80);
const seoResult = await fetchWithAuth(`/api/v1/content/seo-optimize`, {
method: "POST",
body: JSON.stringify({
content: processedContent,
title,
platform: selectedPlatform
}),
}).catch(() => ({ content: processedContent }));
setOptimizeProgress(90);
processedContent = seoResult.content || processedContent;
// 获取优化建议
const tips = await platformRulesApi.getOptimizationTips(selectedPlatform);
setOptimizedContent({
title: title,
content: processedContent,
platform: selectedPlatform,
tips: tips.tips || [],
stages: [
{ stage: "去AI化", status: "success" },
{ stage: "敏感词过滤", status: "success" },
{ stage: "SEO优化", status: "success" },
],
});
setOptimizeProgress(100);
toast({
title: "优化完成",
description: "内容已成功优化,可以复制使用了",
});
} catch (error) {
console.error("优化失败:", error);
toast({
title: "优化失败",
description: "内容优化过程中出现错误,已保留原始内容",
variant: "destructive",
});
// 保留原始内容作为后备
setOptimizedContent({
title: title,
content: content,
platform: selectedPlatform,
tips: [],
});
} finally {
setOptimizing(false);
}
};
const handleCopyContent = useCallback((format: "html" | "markdown" | "text") => {
if (!optimizedContent) {
toast({
title: "复制失败",
description: "请先执行优化操作",
variant: "destructive",
});
return;
}
let copyText = "";
switch (format) {
case "html":
copyText = `<h1>${optimizedContent.title}</h1>\n<p>${optimizedContent.content.replace(/\n\n/g, "</p><p>")}</p>`;
break;
case "markdown":
copyText = `# ${optimizedContent.title}\n\n${optimizedContent.content}`;
break;
case "text":
copyText = `${optimizedContent.title}\n\n${optimizedContent.content}`;
break;
}
navigator.clipboard.writeText(copyText).then(() => {
const formatLabels = { html: "HTML", markdown: "Markdown", text: "纯文本" };
toast({
title: "复制成功",
description: `已复制为${formatLabels[format]}格式`,
});
}).catch(() => {
toast({
title: "复制失败",
description: "无法访问剪贴板,请检查浏览器权限",
variant: "destructive",
});
});
}, [optimizedContent, toast]);
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-muted-foreground">...</div>
</div>
);
}
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground">GEO内容</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* 左侧: 编辑区 */}
<Card className="lg:col-span-2">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</div>
<Select value={selectedPlatform} onValueChange={setSelectedPlatform}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="选择平台" />
</SelectTrigger>
<SelectContent>
{platforms.map((p) => (
<SelectItem key={p.id} value={p.id}>
{platformIcons[p.id] || platformIcons.default} {p.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 平台规则摘要 */}
{platformDetail && (
<div className="mt-3 p-3 bg-muted/50 rounded-lg text-xs space-y-1">
<div className="flex items-center gap-2 font-medium text-foreground">
<span>{platformIcons[selectedPlatform] || platformIcons.default}</span>
<span>{platformDetail.name}</span>
<Badge variant="outline" className="ml-auto">{platformDetail.content_style}</Badge>
</div>
<div className="grid grid-cols-2 gap-x-4 gap-y-0.5 text-muted-foreground">
<span>: {platformDetail.content_length.min}-{platformDetail.content_length.max}</span>
<span>: {platformDetail.content_length.recommended}</span>
<span>: {platformDetail.title_rules.min_length}-{platformDetail.title_rules.max_length}</span>
<span>: {platformDetail.tag_rules.min_tags}-{platformDetail.tag_rules.max_tags}</span>
<span>图片: 最多{platformDetail.max_images}</span>
<span>AI敏感度: {platformDetail.ai_sensitivity.detection_level}</span>
</div>
</div>
)}
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="title"></Label>
<Input
id="title"
placeholder="输入文章标题"
value={title}
onChange={(e: ChangeEvent<HTMLInputElement>) => setTitle(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="content"></Label>
<Textarea
id="content"
placeholder="输入文章内容..."
className="min-h-[400px] font-mono text-sm"
value={content}
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => setContent(e.target.value)}
/>
</div>
<div className="flex gap-2">
<Button
variant="outline"
onClick={handleValidate}
disabled={validating || !content || !title}
>
{validating ? "验证中..." : "校验内容"}
</Button>
<Button
onClick={handleOptimize}
disabled={optimizing || !content || !title}
>
{optimizing ? "优化中..." : "一键优化"}
</Button>
</div>
{/* 优化进度条 */}
{optimizing && (
<div className="space-y-2">
<div className="flex justify-between text-xs text-muted-foreground">
<span></span>
<span>{optimizeProgress}%</span>
</div>
<Progress value={optimizeProgress} className="h-2" />
<div className="flex justify-between text-xs text-muted-foreground">
<span>{optimizeProgress < 30 ? "去AI化处理中..." : optimizeProgress < 60 ? "敏感词过滤中..." : optimizeProgress < 90 ? "SEO优化中..." : "完成"}</span>
</div>
</div>
)}
{/* 验证结果 */}
{validationResult && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<h4 className="font-semibold"></h4>
<Badge
variant={validationResult.is_valid ? "default" : "destructive"}
className={validationResult.is_valid ? "bg-green-600" : ""}
>
{validationResult.is_valid ? "通过" : "未通过"}
</Badge>
<Badge variant="outline">
: {validationResult.score}
</Badge>
</div>
{validationResult.issues.length > 0 && (
<div className="space-y-1">
{validationResult.issues.map((issue, i) => (
<div
key={i}
className={`text-sm p-2 rounded ${
issue.severity === "high"
? "bg-red-50 text-red-800"
: issue.severity === "medium"
? "bg-yellow-50 text-yellow-800"
: "bg-blue-50 text-blue-800"
}`}
>
<span className="font-medium">[{issue.severity.toUpperCase()}]</span>{" "}
{issue.message}
</div>
))}
</div>
)}
{validationResult.passed.length > 0 && (
<div className="space-y-1">
<h5 className="text-sm font-medium text-muted-foreground">:</h5>
{validationResult.passed.map((p, i) => (
<div key={i} className="text-sm text-green-700">
{p}
</div>
))}
</div>
)}
</div>
)}
</CardContent>
</Card>
{/* 右侧: 预览和工具 */}
<Card>
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{!optimizedContent ? (
<div className="flex items-center justify-center h-64 text-muted-foreground">
<div className="text-center">
<p>"一键优化"</p>
</div>
</div>
) : (
<Tabs defaultValue="preview" className="space-y-4">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="preview"></TabsTrigger>
<TabsTrigger value="tips"></TabsTrigger>
</TabsList>
<TabsContent value="preview" className="space-y-4">
<div className="space-y-2">
<h4 className="font-semibold text-lg">{optimizedContent.title}</h4>
<div className="text-sm whitespace-pre-wrap">
{optimizedContent.content}
</div>
</div>
<div className="space-y-2">
<h5 className="font-medium">:</h5>
<div className="flex flex-wrap gap-2">
<Button size="sm" variant="outline" onClick={() => handleCopyContent("text")}>
</Button>
<Button size="sm" variant="outline" onClick={() => handleCopyContent("markdown")}>
Markdown
</Button>
<Button size="sm" variant="outline" onClick={() => handleCopyContent("html")}>
HTML
</Button>
</div>
</div>
</TabsContent>
<TabsContent value="tips">
<div className="space-y-2">
{optimizedContent.tips.length > 0 ? (
optimizedContent.tips.map((tip, i) => (
<div key={i} className="text-sm p-2 bg-muted rounded">
{tip}
</div>
))
) : (
<p className="text-sm text-muted-foreground"></p>
)}
</div>
</TabsContent>
</Tabs>
)}
</CardContent>
</Card>
</div>
{/* 优化流程说明 */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="p-4 border rounded-lg">
<div className="font-semibold mb-2">1. AI化</div>
<p className="text-sm text-muted-foreground">
AI敏感度AI写作特征使
</p>
</div>
<div className="p-4 border rounded-lg">
<div className="font-semibold mb-2">2. </div>
<p className="text-sm text-muted-foreground">
</p>
</div>
<div className="p-4 border rounded-lg">
<div className="font-semibold mb-2">3. SEO优化</div>
<p className="text-sm text-muted-foreground">
</p>
</div>
<div className="p-4 border rounded-lg">
<div className="font-semibold mb-2">4. </div>
<p className="text-sm text-muted-foreground">
HTML规则和结构偏好
</p>
</div>
</div>
</CardContent>
</Card>
</div>
);
}