"use client"; import { useState, useEffect, useRef, useCallback } from "react"; import { useRouter } from "next/navigation"; import { ArrowLeft, CodeXml, Eye, EyeOff, Trash2 } from "lucide-react"; import { apiCreateSnippet, apiUpdateSnippet, apiDeleteSnippet, fetchSnippetLanguages, type SnippetInput, type SnippetVisibility } from "@/lib/api"; import { toast } from "@/lib/toast"; import { snippetPath, snippetsPath, type UrlStyle } from "@/lib/urlStyle"; import Modal from "@/components/Modal"; import SnippetLangSelect from "@/components/SnippetLangSelect"; import { hasAuthCookieHint } from "@/lib/cookies"; import { useSiteBrand } from "@/components/SiteBrandProvider"; const TITLE_MAX = 120; const CODE_MAX = 64 * 1024; // 64KB const DESC_MAX = 8 * 1024; // 8KB const VISIBILITY_OPTIONS: { value: SnippetVisibility; label: string; desc: string }[] = [ { value: "private", label: "私有", desc: "仅自己可见" }, { value: "public", label: "公开", desc: "所有人可见,出现在广场" }, ]; export default function SnippetEditor({ mode, snippetId, initialTitle = "", initialLanguage = "plaintext", initialDescription = "", initialCode = "", initialVisibility = "private", }: { mode: "create" | "edit"; snippetId?: number; initialTitle?: string; initialLanguage?: string; initialDescription?: string; initialCode?: string; initialVisibility?: SnippetVisibility; }) { const router = useRouter(); const { url_style: urlStyle } = useSiteBrand(); const [title, setTitle] = useState(initialTitle); const [language, setLanguage] = useState(initialLanguage); const [description, setDescription] = useState(initialDescription); const [code, setCode] = useState(initialCode); const [visibility, setVisibility] = useState(initialVisibility); const [languages, setLanguages] = useState([]); const [loading, setLoading] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false); const [deleting, setDeleting] = useState(false); const codeRef = useRef(null); useEffect(() => { if (!hasAuthCookieHint()) { router.push("/login?redirect=/snippets/new"); return; } fetchSnippetLanguages() .then((res) => setLanguages(res.languages)) .catch(() => {}); }, [router]); // Tab 键缩进 const handleCodeKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Tab") { e.preventDefault(); const el = e.currentTarget; const start = el.selectionStart; const end = el.selectionEnd; const newValue = code.substring(0, start) + " " + code.substring(end); setCode(newValue); requestAnimationFrame(() => { el.selectionStart = el.selectionEnd = start + 2; }); } }; const codeBytes = new TextEncoder().encode(code).length; const descBytes = new TextEncoder().encode(description).length; const canSubmit = !loading && title.trim().length > 0 && code.trim().length > 0 && codeBytes <= CODE_MAX && descBytes <= DESC_MAX; const handleSubmit = async (e?: React.FormEvent) => { e?.preventDefault(); if (!canSubmit) return; setLoading(true); const input: SnippetInput = { title: title.trim(), language, description: description.trim(), code: code.trim(), visibility, }; try { if (mode === "create") { const res = await apiCreateSnippet(input); toast("创建成功", "ok"); router.push(snippetPath(urlStyle, res.snippet.id)); } else if (snippetId) { const res = await apiUpdateSnippet(snippetId, input); toast("已保存", "ok"); router.push(snippetPath(urlStyle, res.snippet.id)); } } catch (err) { toast(err instanceof Error ? err.message : "操作失败"); } finally { setLoading(false); } }; const handleDelete = async () => { if (!snippetId || deleting) return; setDeleting(true); try { await apiDeleteSnippet(snippetId); toast("已删除", "ok"); router.push(snippetsPath(urlStyle)); } catch (err) { toast(err instanceof Error ? err.message : "删除失败"); } finally { setDeleting(false); setDeleteOpen(false); } }; return (

{mode === "create" ? "新建片段" : "编辑片段"}

{mode === "create" ? "保存一段可复用的代码" : "修改片段内容"}

{mode === "edit" && ( )}
{/* 标题 */}
setTitle(e.target.value.slice(0, TITLE_MAX))} required maxLength={TITLE_MAX} placeholder="片段标题" className="j13-compose-title" disabled={loading} /> {title.length}/{TITLE_MAX}
{/* 语言 + 可见性 */}
{VISIBILITY_OPTIONS.map((opt) => ( ))}
{VISIBILITY_OPTIONS.find((o) => o.value === visibility)?.desc}
{/* 代码编辑区 */}