253 lines
10 KiB
TypeScript
253 lines
10 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|
import { FileText, History, Languages, Loader2, RotateCcw, Save, Sparkles, Trash2 } from "lucide-react";
|
|
import type { SavedScript } from "@/features/home/model/useSavedScripts";
|
|
|
|
const LANGUAGES = [
|
|
{ code: "English", label: "英语 English" },
|
|
{ code: "日本語", label: "日语 日本語" },
|
|
{ code: "한국어", label: "韩语 한국어" },
|
|
{ code: "Français", label: "法语 Français" },
|
|
{ code: "Deutsch", label: "德语 Deutsch" },
|
|
{ code: "Español", label: "西班牙语 Español" },
|
|
{ code: "Русский", label: "俄语 Русский" },
|
|
{ code: "Italiano", label: "意大利语 Italiano" },
|
|
{ code: "Português", label: "葡萄牙语 Português" },
|
|
];
|
|
|
|
interface ScriptEditorProps {
|
|
text: string;
|
|
onChangeText: (value: string) => void;
|
|
onOpenExtractModal: () => void;
|
|
onOpenRewriteModal: () => void;
|
|
onGenerateMeta: () => void;
|
|
isGeneratingMeta: boolean;
|
|
onTranslate: (targetLang: string) => void;
|
|
isTranslating: boolean;
|
|
hasOriginalText: boolean;
|
|
onRestoreOriginal: () => void;
|
|
savedScripts: SavedScript[];
|
|
onSaveScript: () => void;
|
|
onLoadScript: (content: string) => void;
|
|
onDeleteScript: (id: string) => void;
|
|
}
|
|
|
|
export function ScriptEditor({
|
|
text,
|
|
onChangeText,
|
|
onOpenExtractModal,
|
|
onOpenRewriteModal,
|
|
onGenerateMeta,
|
|
isGeneratingMeta,
|
|
onTranslate,
|
|
isTranslating,
|
|
hasOriginalText,
|
|
onRestoreOriginal,
|
|
savedScripts,
|
|
onSaveScript,
|
|
onLoadScript,
|
|
onDeleteScript,
|
|
}: ScriptEditorProps) {
|
|
const [showLangMenu, setShowLangMenu] = useState(false);
|
|
const langMenuRef = useRef<HTMLDivElement>(null);
|
|
const [showHistoryMenu, setShowHistoryMenu] = useState(false);
|
|
const historyMenuRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (!showLangMenu) return;
|
|
const handleClickOutside = (e: MouseEvent) => {
|
|
if (langMenuRef.current && !langMenuRef.current.contains(e.target as Node)) {
|
|
setShowLangMenu(false);
|
|
}
|
|
};
|
|
document.addEventListener("mousedown", handleClickOutside);
|
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
}, [showLangMenu]);
|
|
|
|
useEffect(() => {
|
|
if (!showHistoryMenu) return;
|
|
const handleClickOutside = (e: MouseEvent) => {
|
|
if (historyMenuRef.current && !historyMenuRef.current.contains(e.target as Node)) {
|
|
setShowHistoryMenu(false);
|
|
}
|
|
};
|
|
document.addEventListener("mousedown", handleClickOutside);
|
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
}, [showHistoryMenu]);
|
|
|
|
const handleSelectLang = (langCode: string) => {
|
|
setShowLangMenu(false);
|
|
onTranslate(langCode);
|
|
};
|
|
|
|
const formatDate = (ts: number) => {
|
|
const d = new Date(ts);
|
|
return `${(d.getMonth() + 1).toString().padStart(2, "0")}-${d.getDate().toString().padStart(2, "0")} ${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}`;
|
|
};
|
|
|
|
return (
|
|
<div className="relative z-10 bg-white/5 rounded-2xl p-4 sm:p-6 border border-white/10 backdrop-blur-sm">
|
|
<div className="mb-4 space-y-3">
|
|
<h2 className="text-base sm:text-lg font-semibold text-white flex items-center gap-2">
|
|
一、文案提取与编辑
|
|
</h2>
|
|
<div className="flex gap-2 flex-wrap justify-end items-center">
|
|
{/* 历史文案 */}
|
|
<div className="relative" ref={historyMenuRef}>
|
|
<button
|
|
onClick={() => setShowHistoryMenu((prev) => !prev)}
|
|
className="h-7 px-2.5 text-xs rounded transition-all whitespace-nowrap bg-gray-600 hover:bg-gray-500 text-white inline-flex items-center gap-1"
|
|
>
|
|
<History className="h-3.5 w-3.5" />
|
|
历史文案
|
|
</button>
|
|
{showHistoryMenu && (
|
|
<div className="absolute left-0 top-full mt-1 z-50 bg-gray-800 border border-white/10 rounded-lg shadow-xl py-1 min-w-[220px] max-h-[280px] overflow-y-auto">
|
|
{savedScripts.length === 0 ? (
|
|
<div className="px-3 py-3 text-xs text-gray-500 text-center">暂无保存的文案</div>
|
|
) : (
|
|
savedScripts.map((script) => (
|
|
<div
|
|
key={script.id}
|
|
className="flex items-center gap-1 px-3 py-1.5 hover:bg-white/10 transition-colors group"
|
|
>
|
|
<button
|
|
onClick={() => {
|
|
onLoadScript(script.content);
|
|
setShowHistoryMenu(false);
|
|
}}
|
|
className="flex-1 text-left min-w-0"
|
|
>
|
|
<div className="text-xs text-gray-200 truncate">{script.name}</div>
|
|
<div className="text-[10px] text-gray-500">{formatDate(script.savedAt)}</div>
|
|
</button>
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onDeleteScript(script.id);
|
|
}}
|
|
className="opacity-40 group-hover:opacity-100 p-1 text-gray-500 hover:text-red-400 transition-all shrink-0"
|
|
>
|
|
<Trash2 className="h-3 w-3" />
|
|
</button>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<button
|
|
onClick={onOpenExtractModal}
|
|
className="h-7 px-2.5 text-xs rounded transition-all whitespace-nowrap bg-purple-600 hover:bg-purple-700 text-white inline-flex items-center gap-1"
|
|
>
|
|
<FileText className="h-3.5 w-3.5" />
|
|
文案提取助手
|
|
</button>
|
|
<div className="relative" ref={langMenuRef}>
|
|
<button
|
|
onClick={() => setShowLangMenu((prev) => !prev)}
|
|
disabled={isTranslating || !text.trim()}
|
|
className={`h-7 px-2.5 text-xs rounded transition-all whitespace-nowrap inline-flex items-center gap-1 ${
|
|
isTranslating || !text.trim()
|
|
? "bg-gray-600 cursor-not-allowed text-gray-400"
|
|
: "bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white"
|
|
}`}
|
|
>
|
|
{isTranslating ? (
|
|
<>
|
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
翻译中...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Languages className="h-3.5 w-3.5" />
|
|
AI多语言
|
|
</>
|
|
)}
|
|
</button>
|
|
{showLangMenu && (
|
|
<div className="absolute right-0 top-full mt-1 z-50 bg-gray-800 border border-white/10 rounded-lg shadow-xl py-1 min-w-[160px]">
|
|
{hasOriginalText && (
|
|
<>
|
|
<button
|
|
onClick={() => { setShowLangMenu(false); onRestoreOriginal(); }}
|
|
className="w-full text-left px-3 py-1.5 text-xs text-amber-400 hover:bg-white/10 transition-colors flex items-center gap-1"
|
|
>
|
|
<RotateCcw className="h-3 w-3" />
|
|
还原原文
|
|
</button>
|
|
<div className="border-t border-white/10 my-1" />
|
|
</>
|
|
)}
|
|
{LANGUAGES.map((lang) => (
|
|
<button
|
|
key={lang.code}
|
|
onClick={() => handleSelectLang(lang.code)}
|
|
className="w-full text-left px-3 py-1.5 text-xs text-gray-200 hover:bg-white/10 transition-colors"
|
|
>
|
|
{lang.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<button
|
|
onClick={onGenerateMeta}
|
|
disabled={isGeneratingMeta || !text.trim()}
|
|
className={`h-7 px-2.5 text-xs rounded transition-all whitespace-nowrap inline-flex items-center gap-1 ${isGeneratingMeta || !text.trim()
|
|
? "bg-gray-600 cursor-not-allowed text-gray-400"
|
|
: "bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-700 hover:to-cyan-700 text-white"
|
|
}`}
|
|
>
|
|
{isGeneratingMeta ? (
|
|
<>
|
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
生成中...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Sparkles className="h-3.5 w-3.5" />
|
|
AI生成标题标签
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<textarea
|
|
value={text}
|
|
onChange={(e) => onChangeText(e.target.value)}
|
|
placeholder="请输入你想说的话..."
|
|
className="w-full h-40 bg-black/30 border border-white/10 rounded-xl p-4 text-white placeholder-gray-500 resize-none focus:outline-none focus:border-purple-500 transition-colors hide-scrollbar"
|
|
/>
|
|
<div className="flex items-center justify-between mt-2 text-sm text-gray-400">
|
|
<span>{text.length} 字</span>
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={onOpenRewriteModal}
|
|
disabled={!text.trim()}
|
|
className={`px-2.5 py-1 text-xs rounded transition-all flex items-center gap-1 ${
|
|
!text.trim()
|
|
? "bg-gray-700 cursor-not-allowed text-gray-500"
|
|
: "bg-purple-600/80 hover:bg-purple-600 text-white"
|
|
}`}
|
|
>
|
|
<Sparkles className="h-3 w-3" />
|
|
AI智能改写
|
|
</button>
|
|
<button
|
|
onClick={onSaveScript}
|
|
disabled={!text.trim()}
|
|
className={`px-2.5 py-1 text-xs rounded transition-all flex items-center gap-1 ${
|
|
!text.trim()
|
|
? "bg-gray-700 cursor-not-allowed text-gray-500"
|
|
: "bg-amber-600/80 hover:bg-amber-600 text-white"
|
|
}`}
|
|
>
|
|
<Save className="h-3 w-3" />
|
|
保存文案
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|