diff --git a/prisma/dev.db b/prisma/dev.db index 527797c..b4c7051 100644 Binary files a/prisma/dev.db and b/prisma/dev.db differ diff --git a/src/app/api/pages/[id]/route.ts b/src/app/api/pages/[id]/route.ts index 32b895f..1fe7cc4 100644 --- a/src/app/api/pages/[id]/route.ts +++ b/src/app/api/pages/[id]/route.ts @@ -113,10 +113,10 @@ export async function PUT( return NextResponse.json({ error: 'Invalid title' }, { status: 400 }); } const trimmed = body.title.trim(); - if (!trimmed || trimmed.length > MAX_TITLE_LENGTH) { + if (trimmed.length > MAX_TITLE_LENGTH) { return NextResponse.json({ error: 'Invalid title' }, { status: 400 }); } - updateData.title = trimmed; + updateData.title = trimmed || '无标题'; } if (body.content !== undefined) { diff --git a/src/app/globals.css b/src/app/globals.css index 67f0062..e7af1ed 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,4 +1,4 @@ -@import 'highlight.js/styles/atom-one-dark.css'; +@import 'highlight.js/styles/atom-one-dark.css'; @tailwind base; @tailwind components; @tailwind utilities; @@ -26,6 +26,7 @@ --input: 35 15% 85%; --ring: 223 21% 30%; --radius: 0.75rem; + --code-bg: 220 16% 21%; } .dark { @@ -57,6 +58,7 @@ --border: 220 8% 28%; --input: 220 8% 28%; --ring: 210 16% 78%; + --code-bg: 220 16% 20%; } * { @@ -76,7 +78,8 @@ @layer components { .ui-card { - @apply rounded-2xl border border-border/70 bg-card/95 shadow-sm backdrop-blur-sm; + @apply rounded-xl border border-border/65 bg-card shadow-sm backdrop-blur-sm; + background-color: hsl(var(--card) / 0.92); } .ui-input { @@ -88,23 +91,23 @@ } .ui-btn { - @apply inline-flex items-center justify-center gap-2 rounded-xl px-4 py-2 text-sm font-medium transition-all active:scale-[0.98]; + @apply inline-flex items-center justify-center gap-2 rounded-xl border border-transparent px-4 py-2 text-sm font-semibold transition-all active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-55 disabled:active:scale-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/25; } .ui-btn-primary { - @apply ui-btn bg-primary text-primary-foreground hover:opacity-90 shadow-sm; + @apply ui-btn border-primary/20 bg-primary text-primary-foreground shadow-sm hover:opacity-90; } .ui-btn-secondary { - @apply ui-btn bg-secondary text-secondary-foreground hover:bg-secondary/80; + @apply ui-btn border-border/70 bg-secondary/85 text-secondary-foreground hover:bg-secondary; } .ui-btn-danger { - @apply ui-btn bg-destructive text-destructive-foreground hover:opacity-90; + @apply ui-btn border-destructive/25 bg-destructive text-destructive-foreground hover:opacity-90; } .ui-icon-btn { - @apply inline-flex h-9 w-9 items-center justify-center rounded-lg border border-transparent text-muted-foreground transition-colors hover:border-border hover:bg-muted/60 hover:text-foreground; + @apply inline-flex h-9 w-9 items-center justify-center rounded-lg border border-border/40 bg-background/30 text-muted-foreground transition-colors active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-55 disabled:active:scale-100 hover:border-border hover:bg-muted/60 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/25; } .ui-kbd { @@ -159,11 +162,72 @@ .ProseMirror { outline: none; min-height: 300px; - padding-bottom: 50px; + padding-bottom: 56px; font-size: 1.05rem; line-height: var(--editor-line-height, 1.5); - /* Use CSS variable with fallback */ - /* Reduced from 1.75 */ + letter-spacing: 0.003em; +} + +@media (max-width: 768px) { + .ProseMirror { + font-size: 1rem; + line-height: max(1.55, var(--editor-line-height, 1.5)); + } +} + +.ProseMirror > * + * { + margin-top: 0.7em; +} + +.ProseMirror h1, +.ProseMirror h2, +.ProseMirror h3 { + color: hsl(var(--foreground)); + font-weight: 750; + line-height: 1.28; + letter-spacing: -0.01em; +} + +.ProseMirror h1 { + margin-top: 1.2em; + font-size: clamp(1.7rem, 1.3rem + 1.3vw, 2.15rem); +} + +.ProseMirror h2 { + margin-top: 1.05em; + font-size: clamp(1.35rem, 1.18rem + 0.8vw, 1.7rem); +} + +.ProseMirror h3 { + margin-top: 0.9em; + font-size: clamp(1.12rem, 1.02rem + 0.45vw, 1.3rem); +} + +.ProseMirror p { + color: hsl(var(--foreground)); +} + +.ProseMirror ul, +.ProseMirror ol { + margin: 0.5em 0; + padding-left: 1.2em; +} + +.ProseMirror li { + margin: 0.22em 0; +} + +.ProseMirror a { + color: hsl(var(--primary)); + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 2px; +} + +.ProseMirror hr { + margin: 1.1em 0; + border: 0; + border-top: 1px solid hsl(var(--border)); } .ProseMirror p.is-editor-empty:first-child::before { @@ -176,20 +240,15 @@ .ProseMirror blockquote { border-left: 3px solid hsl(var(--primary)); - /* Make border color slightly more visible (primary) */ - padding-left: 1rem; + padding-left: 0.95rem; color: hsl(var(--muted-foreground)); font-family: "Georgia", "Cambria", "Times New Roman", serif !important; - /* Force serif font */ font-style: italic; - margin: 1rem 0; + margin: 0.95rem 0; background: hsl(var(--muted) / 0.3); - /* Subtle background */ - padding-top: 0.5rem; - padding-bottom: 0.5rem; + padding-top: 0.45rem; + padding-bottom: 0.45rem; border-radius: 0 0.5rem 0.5rem 0; - /* Rounded right corners */ - /* Rounded right corners */ } .ProseMirror code { @@ -217,19 +276,16 @@ /* Table Styles - Ensure content is compact */ .ProseMirror table p { margin: 0; - /* Remove paragraph margin inside tables */ line-height: var(--table-line-height, 1.2); - /* Use CSS variable with fallback */ - /* Tighter line height for table content */ } .ProseMirror table { border-collapse: collapse; margin: 1em 0; - /* Add some vertical space around the table itself */ overflow: hidden; table-layout: fixed; width: 100%; + border-radius: 0.5rem; } .ProseMirror td, @@ -240,7 +296,6 @@ box-sizing: border-box; min-width: 1em; padding: 2px 4px; - /* Further reduced padding */ position: relative; vertical-align: top; } @@ -249,13 +304,12 @@ background-color: hsl(var(--muted)); font-weight: bold; text-align: left; - /* Make header borders visible by using a contrasting color if bg matches border */ border-color: hsl(var(--foreground) / 0.1); } /* AI Content Style */ .ai-content { - font-family: "KaiTi", "STKaiti", "楷体", "Georgia", serif; + font-family: "KaiTi", "STKaiti", "Kaiti SC", "Noto Serif SC", "Georgia", serif; font-style: italic; font-weight: normal; color: hsl(var(--foreground)); @@ -265,7 +319,6 @@ /* Specific fix for dark mode if needed, but opacity trick usually works */ .dark .ProseMirror th { border-color: hsl(var(--background)); - /* Use background color for borders in dark mode header to show separation */ } /* Task List Styles */ @@ -279,7 +332,6 @@ ul[data-type="taskList"], .ProseMirror li[data-type="taskItem"] div, .ProseMirror li[data-type="taskItem"] label { line-height: normal !important; - /* Force tight line height for checkbox items */ margin-top: 2px; margin-bottom: 2px; } @@ -289,12 +341,15 @@ ul[data-type="taskList"], /* Override Highlight.js background for a softer look */ .ProseMirror pre { - background: #2b313a !important; - /* Slightly lifted code block background for comfortable reading */ + background: hsl(var(--code-bg)) !important; + border: 1px solid hsl(var(--border) / 0.5); border-radius: 0.5rem; + padding: 0.65rem 0.75rem; + margin: 0.95rem 0; } .hljs { background: transparent !important; /* Let pre handle the background */ } + diff --git a/src/app/page.tsx b/src/app/page.tsx index ddb9d7a..d50f74e 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -3,7 +3,7 @@ import { SidebarContent, ResizableSidebar } from "@/components/sidebar"; import { Editor } from "@/components/editor"; import { useEditorStore } from "@/lib/store"; -import { Hash, X, Plus, ChevronLeft, ChevronRight, Sparkles, Lock, Unlock, FolderOpen, History, RotateCcw } from "lucide-react"; +import { Hash, X, Plus, ChevronLeft, ChevronRight, ChevronDown, Sparkles, Lock, Unlock, FolderOpen, History, RotateCcw } from "lucide-react"; import { useSettingsStore } from "@/lib/settings-store"; import { exportPageAsMarkdown } from "@/lib/export"; import { useMemo, useState } from "react"; @@ -12,6 +12,7 @@ import { useSearchStore } from "@/lib/search-store"; import { type Editor as TiptapEditor } from "@tiptap/react"; import { ImportProvider } from "@/components/import-context"; import { AIChatPanel } from "@/components/chat/ai-chat-panel"; +import { SearchCommand } from "@/components/search-command"; import { getBacklinks, getOutgoingLinks, getUnresolvedLinkTitles } from "@/lib/wiki-links"; import { getPageHistory } from "@/lib/page-history"; @@ -34,6 +35,8 @@ export default function Home() { const [isAddingTag, setIsAddingTag] = useState(false); const [tagInput, setTagInput] = useState(""); const [isIconPickerOpen, setIsIconPickerOpen] = useState(false); + const [isHistoryOpen, setIsHistoryOpen] = useState(false); + const [selectedHistoryIndex, setSelectedHistoryIndex] = useState(0); const breadcrumbs = useMemo(() => { if (!activePage) return []; @@ -68,6 +71,7 @@ export default function Home() { if (!activePage || activePage.type !== "file") return []; return getPageHistory(activePage.id); }, [activePage]); + const effectiveHistoryIndex = Math.min(selectedHistoryIndex, Math.max(localHistory.length - 1, 0)); const addTag = () => { if (!activePage) return; @@ -93,6 +97,8 @@ export default function Home() { return (
+ +
@@ -102,50 +108,50 @@ export default function Home() {
{activePage ? (
-
-
+
+
-
+
{breadcrumbs.map((crumb, index) => ( -
+
{index > 0 && }
))}
-
- {activePage.icon && {activePage.icon}} +
+ {activePage.icon && {activePage.icon}} updatePage(activePage.id, { title: e.target.value })} placeholder="无标题" disabled={activePage.isLocked} className={cn( - "w-full border-none bg-transparent text-3xl font-bold text-foreground outline-none placeholder:text-muted-foreground/30", + "w-full border-none bg-transparent text-2xl font-bold leading-tight text-foreground outline-none placeholder:text-muted-foreground/30 md:text-3xl", activePage.isLocked && "cursor-not-allowed select-none opacity-80" )} />
-
+
{activePage.tags?.map((tag) => { const colors = getTagColor(tag); return ( @@ -167,7 +173,7 @@ export default function Home() { const newTags = activePage.tags?.filter((t) => t !== tag) || []; updatePage(activePage.id, { tags: newTags }); }} - className="ml-1 rounded-full p-0.5 opacity-0 transition-all group-hover/tag:opacity-100 hover:bg-black/10 dark:hover:bg-white/10" + className="ml-1 rounded-full p-1 opacity-100 transition-all hover:bg-black/10 md:p-0.5 md:opacity-0 md:group-hover/tag:opacity-100 dark:hover:bg-white/10" title="移除标签" > @@ -205,7 +211,7 @@ export default function Home() { )} -
+
{isIconPickerOpen && ( -
-
+
+
{ICONS.map((icon) => (

自动保存最近修改快照(当前浏览器本地)。

-
- {localHistory.length > 0 ? ( - localHistory.slice(0, 8).map((snapshot, index) => ( -
-
+ {isHistoryOpen && ( +
+ {localHistory.length > 0 ? ( + <> +
+ + +
+
- {snapshot.title || "无标题"} + {localHistory[effectiveHistoryIndex]?.title || "无标题"}
- {new Date(snapshot.timestamp).toLocaleString("zh-CN", { - timeZone: timezone || "Asia/Shanghai", - hour12: false, - })} + {localHistory[effectiveHistoryIndex] + ? new Date(localHistory[effectiveHistoryIndex].timestamp).toLocaleString("zh-CN", { + timeZone: timezone || "Asia/Shanghai", + hour12: false, + }) + : "-"}
-
- )) - ) : ( - 暂无历史记录 - )} -
+ + ) : ( + 暂无历史记录 + )} +
+ )}
@@ -401,13 +438,15 @@ export default function Home() {
) : ( -
-
- 🧠 -
-
-

欢迎使用 NoteAI

-

在左侧选择一个页面,或新建文档开始写作。

+
+
+
+ 🧠 +
+
+

欢迎使用 NoteAI

+

在左侧选择一个页面,或新建文档开始写作。

+
)} diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 1e2108e..29f004b 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { Lock, Save, Sparkles, Type, Upload, Download } from "lucide-react"; +import { Lock, Save, Sparkles, Type, Upload, Download, Loader2 } from "lucide-react"; import { ResizableSidebar } from "@/components/sidebar"; import { PromptManagement } from "@/components/settings/prompt-management"; import { ImportProvider } from "@/components/import-context"; @@ -28,6 +28,8 @@ export default function SettingsPage() { const [currentPassword, setCurrentPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); + const [isExporting, setIsExporting] = useState(false); + const [isRestoring, setIsRestoring] = useState(false); const [msg, setMsg] = useState<{ type: "success" | "error"; text: string } | null>(null); const handlePasswordChange = async (e: React.FormEvent) => { @@ -62,7 +64,7 @@ export default function SettingsPage() { return ( -
+
@@ -71,10 +73,10 @@ export default function SettingsPage() {

管理外观、AI、安全与备份。

-
+
-

外观设置

+

外观设置

@@ -149,10 +151,10 @@ export default function SettingsPage() {
-
+
-

AI 配置

+

AI 配置

@@ -191,14 +193,14 @@ export default function SettingsPage() {
-
+
-
+
-

安全设置

+

安全设置

@@ -243,30 +245,43 @@ export default function SettingsPage() {
-
-

备份与恢复

+
+

备份与恢复

恢复会覆盖当前全部文档,请谨慎操作。

-