修复主题一些细节优化问题
This commit is contained in:
1 parent
578bac0e12
commit
a8f49d8651
24 files changed
+645
-67
No files matched your search
@@ -76,7 +76,7 @@ export default function AIPanel({ selectedText, onApplyText, title, content, onA
|
||||
<div>
|
||||
<h3 className="text-[11px] font-medium text-muted uppercase tracking-wider mb-3">AI 助手</h3>
|
||||
|
||||
<div className="bg-white border border-line rounded-lg overflow-hidden">
|
||||
<div className="bg-card border border-line rounded-lg overflow-hidden">
|
||||
{/* Text operations */}
|
||||
<div className="p-3 border-b border-line/50">
|
||||
<div className="text-[10px] font-medium text-muted/70 uppercase mb-2">文本处理</div>
|
||||
@@ -135,7 +135,7 @@ export default function AIPanel({ selectedText, onApplyText, title, content, onA
|
||||
|
||||
{/* Result panel */}
|
||||
{(result || error) && (
|
||||
<div className="mt-3 p-3 bg-white border border-line rounded-lg">
|
||||
<div className="mt-3 p-3 bg-card border border-line rounded-lg">
|
||||
{error ? (
|
||||
<div className="text-[12px] text-danger">{error}</div>
|
||||
) : (
|
||||
|
||||
@@ -36,6 +36,7 @@ export default function PostEditor({ initialData, isEditing }: PostEditorProps)
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [selectedText, setSelectedText] = useState("");
|
||||
const [autoSaved, setAutoSaved] = useState(false);
|
||||
const [showPublishConfirm, setShowPublishConfirm] = useState(false);
|
||||
|
||||
// Auto-save: restore from localStorage on mount
|
||||
const cacheKey = isEditing ? `draft-edit-${initialData?.frontMatter.slug}` : "draft-new";
|
||||
@@ -84,7 +85,45 @@ export default function PostEditor({ initialData, isEditing }: PostEditorProps)
|
||||
}
|
||||
};
|
||||
|
||||
const handlePaste = (e: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
||||
const handlePaste = async (e: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
||||
const imageFile = Array.from(e.clipboardData.files).find((file) => file.type.startsWith("image/"));
|
||||
if (imageFile) {
|
||||
e.preventDefault();
|
||||
if (!dirPath) {
|
||||
setMessage({ type: "error", text: "请先保存文章,再直接粘贴图片。这样图片才能放到文章同级文件夹。" });
|
||||
return;
|
||||
}
|
||||
|
||||
const el = e.currentTarget;
|
||||
const start = el.selectionStart;
|
||||
const end = el.selectionEnd;
|
||||
setUploading(true);
|
||||
setMessage(null);
|
||||
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append("file", imageFile);
|
||||
fd.append("dirPath", dirPath);
|
||||
const res = await fetch("/api/upload", { method: "POST", body: fd });
|
||||
if (!res.ok) throw new Error("上传失败");
|
||||
const data = await res.json();
|
||||
const markdown = ``;
|
||||
const nextContent = content.slice(0, start) + markdown + content.slice(end);
|
||||
const nextCursor = start + markdown.length;
|
||||
setContent(nextContent);
|
||||
requestAnimationFrame(() => {
|
||||
el.focus();
|
||||
el.selectionStart = nextCursor;
|
||||
el.selectionEnd = nextCursor;
|
||||
});
|
||||
} catch {
|
||||
setMessage({ type: "error", text: "粘贴图片上传失败" });
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const text = e.clipboardData.getData("text/plain");
|
||||
if (!text) return;
|
||||
|
||||
@@ -179,6 +218,7 @@ export default function PostEditor({ initialData, isEditing }: PostEditorProps)
|
||||
};
|
||||
|
||||
const handlePublish = async () => {
|
||||
setShowPublishConfirm(false);
|
||||
setSaving(true); setMessage(null);
|
||||
try {
|
||||
// First save with draft:false
|
||||
@@ -221,9 +261,10 @@ export default function PostEditor({ initialData, isEditing }: PostEditorProps)
|
||||
};
|
||||
|
||||
const inputCls =
|
||||
"w-full bg-white border border-line rounded-md px-3 py-2 text-[13px] text-text placeholder:text-muted/35 focus:outline-none focus:border-accent/30 transition-colors";
|
||||
"w-full bg-card border border-line rounded-md px-3 py-2 text-[13px] text-text placeholder:text-muted/35 focus:outline-none focus:border-accent/30 transition-colors";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex gap-8">
|
||||
<div className="flex-1 min-w-0 space-y-8">
|
||||
{message && (
|
||||
@@ -355,7 +396,7 @@ export default function PostEditor({ initialData, isEditing }: PostEditorProps)
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePublish}
|
||||
onClick={() => setShowPublishConfirm(true)}
|
||||
disabled={saving}
|
||||
className="flex items-center gap-1.5 px-5 py-2 text-[13px] font-medium rounded-md bg-accent text-white hover:bg-accent-hover disabled:opacity-50 transition-colors"
|
||||
>
|
||||
@@ -382,6 +423,46 @@ export default function PostEditor({ initialData, isEditing }: PostEditorProps)
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showPublishConfirm && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-[#223d66]/25 px-4 backdrop-blur-sm">
|
||||
<div className="w-full max-w-[420px] rounded-2xl border border-line bg-bg p-6 shadow-[0_24px_80px_rgba(34,61,102,0.22)]">
|
||||
<div className="mb-4 flex items-start gap-3">
|
||||
<div className="mt-0.5 flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-accent/10 text-accent">
|
||||
<Send size={18} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-[17px] font-semibold text-text">确认发布到公网?</h3>
|
||||
<p className="mt-1 text-[13px] leading-6 text-muted">
|
||||
发布后会自动保存文章、生成 Git 提交,并推送到 GitHub。公网会同步更新,请确认内容已经检查无误。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-xl border border-line bg-surface/70 px-4 py-3 text-[12px] leading-5 text-muted">
|
||||
当前文章:<span className="font-medium text-text">{form.title || "未命名文章"}</span>
|
||||
</div>
|
||||
<div className="mt-6 flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPublishConfirm(false)}
|
||||
className="rounded-md border border-line px-4 py-2 text-[13px] text-muted transition-colors hover:bg-surface hover:text-text"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePublish}
|
||||
disabled={saving}
|
||||
className="flex items-center gap-1.5 rounded-md bg-accent px-4 py-2 text-[13px] font-medium text-white transition-colors hover:bg-accent-hover disabled:opacity-50"
|
||||
>
|
||||
<Send size={13} />
|
||||
确认发布
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -43,7 +43,7 @@ export default function ShortcutPanel({ textareaRef, value, onChange }: Shortcut
|
||||
return (
|
||||
<div>
|
||||
<h3 className="text-[11px] font-medium text-muted uppercase tracking-wider mb-3">快捷插入</h3>
|
||||
<div className="bg-white border border-line rounded-lg p-3 space-y-1">
|
||||
<div className="bg-card border border-line rounded-lg p-3 space-y-1">
|
||||
{shortcuts.map((s) => (
|
||||
<button
|
||||
key={s.label}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { FileText, PenSquare, MessageSquare, Trash2, Rss, Link2, MessageCircle, Image } from "lucide-react";
|
||||
import ThemeToggle from "./ThemeToggle";
|
||||
|
||||
const navItems = [
|
||||
{ href: "/", label: "文章", icon: FileText },
|
||||
@@ -56,6 +57,7 @@ export default function Sidebar() {
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-4 py-3 border-t border-line">
|
||||
<ThemeToggle />
|
||||
<p className="text-[10px] text-muted/50">localhost:8016</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
|
||||
type Theme = "light" | "dark";
|
||||
|
||||
export default function ThemeToggle() {
|
||||
const [theme, setTheme] = useState<Theme>("light");
|
||||
|
||||
useEffect(() => {
|
||||
const saved = localStorage.getItem("write-theme");
|
||||
const nextTheme: Theme = saved === "dark" ? "dark" : "light";
|
||||
setTheme(nextTheme);
|
||||
if (nextTheme === "dark") {
|
||||
document.documentElement.dataset.theme = "dark";
|
||||
} else {
|
||||
delete document.documentElement.dataset.theme;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const toggleTheme = () => {
|
||||
const nextTheme: Theme = theme === "dark" ? "light" : "dark";
|
||||
setTheme(nextTheme);
|
||||
localStorage.setItem("write-theme", nextTheme);
|
||||
if (nextTheme === "dark") {
|
||||
document.documentElement.dataset.theme = "dark";
|
||||
} else {
|
||||
delete document.documentElement.dataset.theme;
|
||||
}
|
||||
};
|
||||
|
||||
const isDark = theme === "dark";
|
||||
const Icon = isDark ? Sun : Moon;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleTheme}
|
||||
className="mb-3 flex w-full items-center justify-between rounded-md border border-line px-3 py-2 text-[12px] text-muted transition-colors hover:bg-bg hover:text-accent"
|
||||
title={isDark ? "切换到浅色模式" : "切换到夜间模式"}
|
||||
aria-label={isDark ? "切换到浅色模式" : "切换到夜间模式"}
|
||||
>
|
||||
<span>{isDark ? "浅色模式" : "夜间模式"}</span>
|
||||
<Icon size={14} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user