From 5709ef2966f74983701ee419f78a90c386faa3ed Mon Sep 17 00:00:00 2001 From: cniucheng Date: Wed, 25 Feb 2026 10:33:37 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E7=A7=BB=E5=8A=A8=E7=AB=AF=E7=9A=84UI?= =?UTF-8?q?=E8=BF=9B=E8=A1=8C=E4=BA=86=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/dev.db | Bin 196608 -> 196608 bytes src/app/api/pages/[id]/route.ts | 4 +- src/app/globals.css | 115 ++++++-- src/app/page.tsx | 149 ++++++---- src/app/settings/page.tsx | 55 ++-- src/components/ai-assist.tsx | 6 +- src/components/chat/ai-chat-panel.tsx | 38 ++- src/components/confirm-provider.tsx | 3 +- src/components/editor.tsx | 4 +- src/components/editor/code-block.tsx | 108 ++----- src/components/editor/command-list.tsx | 4 +- src/components/editor/toolbar.tsx | 229 +++++---------- src/components/search-command.tsx | 30 +- src/components/sidebar.tsx | 386 ++++++++++++++----------- src/components/sidebar/tree-view.tsx | 117 +++++--- src/lib/store.ts | 3 +- 16 files changed, 689 insertions(+), 562 deletions(-) diff --git a/prisma/dev.db b/prisma/dev.db index 527797c577a296e45a9e93c81b89d69ba42157ec..b4c705153a2e2959ac4627d3c075ee214ef5a8bc 100644 GIT binary patch delta 2849 zcmeHJeN0nV6o2QIQfXiN+5w_K`4T3ZDPJPW0=g`5CX1U}LgSE;wa7Dmz*_lGk)f?K z>YU8tb6mzc0Z}9Ysq86Fluhtso1!>l;*5)#5?jR06|%X_8D;mq>LeLDm+X)IVVZM# z?tSO}&UxpZbMCR(Xq%08glgz;ilUMT$q6Z{;?ocv-fr46OYcwCG@&>`;|T>53M3@z z>iO6guOvp%%yF`y5|}&8Mdm#7D>K0KGsl_B%rMi({K%Z-qpZciy zmOHoA%;xzK7fKSq1S1SBdIV?}g?2%EyL}HJzYn_fL!qUi$yrP*Ii3=_HEcvq%Dp*2TX~9gN zLwJn|;&A^_Sin5gDk8e2oUIH*8e3Ll>U ztxszG5+(!JxJ!>Od8=R@n=Mej*kg>4hcI@xs%v$+z{#X(*SrR-b|G zzOBGMs8-yE!H#$o#<|+$_{bEPhwt=3w`x?RuufQyoFwg|$oJCQKI zZeXRYf^Kk0LPQTNl7x*tuuO_7@ixVeq+2kqFNHzz*%)DMSdSub=L!gu&R$vpxss5% z63U)1wuWn8eBj>eNcMKsx+{f&_v)@Yb`Ng1xi@X_>~M^Vlax`uOu4ZVnj|xL6?73H z0>4xaLD*6T=D!8~K75J@-?r1UN$@x;-J2U82mgB+a1wmgYSjDy{+&l5PbJ9L4s!0w z>VdX~0mp{NAzv(q_a)@T?}GgSauAkR!1B@XL0G*2bW+NcRlu81R96DM27Up-FA8)r zsY@VPEG3K8Xshg<+=-z&@+Ik3e(`W8kUpurl}&4mk@mq{?$Fga*vmpRH>rORVg%iJ zbQsYO3!sR_b`!*Kciq!vG1FqO(h4nOrd_8MBE(k{E?EId1&T@K-jv>!Y!tG=zIJ;PAkFJ||2fnfK-E@+l0jpCG@f$-jX9T)NyACT zjC(51+Z^&tPL2oHd+Ik1?yO19%@sR7c9>CW@RiI`lCjnSR(x|5V5b7?8g0AX3y2O8 z?gCn`5cZuzTm1yB1`WRv27QzQvM7yKbxD;6S;`PJS6S`*wcBR^&N8FbngBDkCOS+P z9Bh=!VH|gEOKe9TTBtx&2WFR`&omW5v@u#g`gwI8QbQtE=b?0e=eCmB-7=wiDYCNu zw<*~m+-|qu1VMWb4S{eQPzc>|1u3;i-NEa$d)4z7#Hn>*6DiTwlZ=Shx@yVof4yy= i`}2=-a);{hI`6@)i!25CY!Qh>&E-DAoys1$&)}bRY)+B@ delta 2172 zcmeHHZ%kWN6o2RT^|joVzA}ag$O`gzYS;2t2vDXIji_W8>KCKZwJ!`r=tg17rWtiv zG_eG*32!nramj8zf;A!6ag48$UcB@xpS zd&Hl(al*7t!=57!WoGZNKd_hCdG;E6mHmmGVSi;Wu;+a4dhqkxQg!J{wK~)8u&3uZ z$m#f4vTTSUi-sW!BkJmPWMM_-NKBTvDF4hJzm_I&J^ynhN`wgic~5RQ(@8<6Vye|` z_1iiNLEZiu6KahWD*Q(eDiC0Q-)ALSIVw|=i&I>n7 zX`D_#?adcxoI${ut^yio5|B1mLgOp~+UN3VoK3)Z_hU4El7RgA3>xPU;F(XMaV`O4 z^M(kFr~13E_jXNr-U~GGzYTvZDLmCff<%RmTjZ>$@O_U_RxmEClNCyjBu0x0lf(N1 zp0=ru?&*O;Q;j{-drwPp-yDem?44Oyn}Bh0wJVR?X?x3A&DAAWxbp7ZkbH0PqLG}A z>y7m8KKkhE$q5;`c>xbUd8#jP^4%oKm10am7AfdoEYl<2z`kyB2RWI5XJ4S@Sn{^| zT36oW$^L-PCu1-4#z8aQ$&TNydtQp4@^wsq(0r}keLn{A`gb#z z?=PiZQ|scSi-bR|d$14KlmtY1w@l_~#0a<#Fkyu2Leo9wj?&XR9(Zs#nR ze`5rh!Uy1hr)%_L!Z8>xZWp&V}74$VQ2Pc%UVt_Tm%rLMDVa;B2 zLwnc^dMw6)s)$k*`P_@N4zF5`?%otr1f^60 z`u^q89<`ce_&70&|HKT*LSY;l)rI9tg?RPvg7Az9Euc*w9-7dLgCa^65U>-96};TeMTCYa1C^liy=e_~L^~)%dG(d-DN@GH7lnTnoJI#-p zEl>s*(2KZDethm6;LB`i7xVU7tu;P|kF7$vLg*IsiV8s%&sCx?w3+4{&TiZhp|u)f z_$!qtOOfxi@M)i-G(2*r2-=(W|MC$nv{fM|#~c?Y+i{<}3j}uvYNpj*OSCk?`1fbw r!3=va!@U3V42!z&4C7A?Ad9M={6P~1^@9@#>-}MRMkg#@LchbGT=&~d 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() {
-
-

备份与恢复

+
+

备份与恢复

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

-