From 51f528f516d7dcb113b2ffa47b499a578a77b21a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A0=81=E5=86=9C=E9=98=BF=E6=A5=A0?= Date: Tue, 8 Sep 2026 21:41:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E9=87=8D=E6=9E=84=209=20=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E7=95=8C=E9=9D=A2=E5=B9=B6=E7=BB=9F=E4=B8=80=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1=E4=BB=A4=E7=89=8C=E4=B8=8E=E5=93=8D=E5=BA=94=E5=BC=8F?= =?UTF-8?q?=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - index.css 以 Tailwind 4 @theme 落地设计令牌,全部页面按新设计稿重写 样式:首页/笔记详情/标签页/登录/后台列表/编辑/回收站/设置/404 - Layout 重构导航与主题切换;新增 Toast 轻提示组件 - 移除 vite 手动 codeSplitting:分组 chunk 会被提升进入口静态依赖并对 HTML 发出 modulepreload,lazy 路由的 CodeMirror 在所有页面被预加载 (Lighthouse unused-javascript 549KiB 的根因) - vite dev 代理补 changeOrigin:false,保留浏览器原始 Host,避免后端 Origin 校验误判跨源 403 --- web/src/App.tsx | 5 +- web/src/components/Editor.tsx | 19 +- web/src/components/Layout.tsx | 197 +- web/src/components/MarkdownViewer.tsx | 7 +- web/src/components/Toast.tsx | 30 + web/src/features/site.ts | 15 + web/src/index.css | 2684 ++++++++++++++++++++++++- web/src/lib/utils.ts | 18 + web/src/pages/AdminEdit.tsx | 380 ++-- web/src/pages/AdminList.tsx | 314 ++- web/src/pages/AdminSettings.tsx | 237 ++- web/src/pages/AdminTrash.tsx | 118 +- web/src/pages/Home.tsx | 157 +- web/src/pages/Login.tsx | 91 +- web/src/pages/NotFound.tsx | 19 +- web/src/pages/Note.tsx | 83 +- web/src/pages/TagPage.tsx | 107 +- web/vite.config.ts | 22 +- 18 files changed, 3820 insertions(+), 683 deletions(-) create mode 100644 web/src/components/Toast.tsx diff --git a/web/src/App.tsx b/web/src/App.tsx index 7f94747..84d0b75 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -20,7 +20,7 @@ const AdminEdit = lazy(() => import('./pages/AdminEdit')) function RequireAuth({ children }: { children: ReactNode }) { const { authenticated, loading } = useAuth() if (loading) { - return
加载中…
+ return
加载中…
} if (!authenticated) { return @@ -30,11 +30,12 @@ function RequireAuth({ children }: { children: ReactNode }) { export default function App() { return ( - 加载中…}> + 加载中…}> }> } /> } /> + } /> } /> } /> diff --git a/web/src/components/Editor.tsx b/web/src/components/Editor.tsx index 523b7f4..51c4491 100644 --- a/web/src/components/Editor.tsx +++ b/web/src/components/Editor.tsx @@ -3,6 +3,7 @@ import CodeMirror from '@uiw/react-codemirror' import { markdown, markdownLanguage } from '@codemirror/lang-markdown' import { EditorView } from '@codemirror/view' import { api, ApiError } from '../lib/api' +import { useTheme } from '../lib/theme' /** * Editor(编辑态,§8.2):CodeMirror 6 源码编辑 + 图片粘贴/拖拽上传。 @@ -22,6 +23,12 @@ async function uploadImage(file: File): Promise { return api('/api/admin/images', { method: 'POST', body: form }) } +function isDarkMode(mode: 'light' | 'dark' | 'system'): boolean { + if (mode === 'dark') return true + if (mode === 'light') return false + return typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches +} + export default function Editor({ value, onChange, @@ -35,6 +42,7 @@ export default function Editor({ }) { const viewRef = useRef<{ view?: EditorView } | null>(null) const [dragOver, setDragOver] = useState(false) + const { mode } = useTheme() const extensions = useMemo( () => [ @@ -81,9 +89,8 @@ export default function Editor({ return (
{ e.preventDefault() setDragOver(true) @@ -100,9 +107,9 @@ export default function Editor({ onChange={onChange} extensions={extensions} basicSetup={{ lineNumbers: true, foldGutter: false, highlightActiveLine: true }} - theme="light" + theme={isDarkMode(mode) ? 'dark' : 'light'} height="100%" - style={{ height: '100%' }} + style={{ height: '100%', minHeight: 420 }} onCreateEditor={(view) => { viewRef.current = { view } }} @@ -115,7 +122,7 @@ export default function Editor({ }} /> {dragOver && ( -
+
松开以上传图片(≤5MB)
)} diff --git a/web/src/components/Layout.tsx b/web/src/components/Layout.tsx index cbb192e..993a408 100644 --- a/web/src/components/Layout.tsx +++ b/web/src/components/Layout.tsx @@ -1,101 +1,160 @@ -import { Link, NavLink, Outlet } from 'react-router' -import { Moon, Sun, Monitor } from 'lucide-react' +import { Link, NavLink, Outlet, useLocation, useNavigate } from 'react-router' +import { Moon, Sun, Monitor, ExternalLink, House, PenLine, Trash2, Settings, LogOut } from 'lucide-react' import { useSiteTitle } from '../features/site' import { useTheme } from '../lib/theme' +import { useAuth } from '../lib/auth' -export function SiteHeader() { - const { data } = useSiteTitle() +/* ── 博客端:居中单栏 720px 布局(博客系列设计稿) ─────────── */ + +function ThemeButton() { const { mode, setMode } = useTheme() - return ( -
-
- - {data?.site_title ?? 'Pure Note'} - -
- -
-
-
- ) -} - -function ThemeButton({ - mode, - setMode, -}: { - mode: 'light' | 'dark' | 'system' - setMode: (m: 'light' | 'dark' | 'system') => void -}) { const next = mode === 'light' ? 'dark' : mode === 'dark' ? 'system' : 'light' const title = `主题:${mode === 'light' ? '浅色' : mode === 'dark' ? '深色' : '跟随系统'}(点击切换)` return ( ) } -export function SiteFooter() { - return ( - - ) -} - export function BlogLayout() { + const { data } = useSiteTitle() + const title = data?.site_title ?? 'Pure Note' return ( -
- -
+
+
+
+ {title} + +
+
+
- +
+
+ © {new Date().getFullYear()} {title} · 纯文本驱动 · RSS +
+
) } -const adminLinks = [ - { to: '/admin', label: '笔记', end: true }, - { to: '/admin/trash', label: '回收站', end: true }, - { to: '/admin/settings', label: '设置', end: true }, +/* ── 管理端:sidebar + topbar + 移动端 tabbar(dashboard 设计稿) ── */ + +export function AdminTopbar({ title, children }: { title: string; children?: React.ReactNode }) { + return ( +
+

{title}

+ {children &&
{children}
} +
+ ) +} + +const adminNav = [ + { to: '/admin', label: '笔记管理', icon: House, end: true, match: 'exact' as const }, + { to: '/admin/notes/new', label: '新建笔记', icon: PenLine, end: false, match: 'prefix' as const }, + { to: '/admin/trash', label: '回收站', icon: Trash2, end: true, match: 'exact' as const }, + { to: '/admin/settings', label: '设置', icon: Settings, end: true, match: 'exact' as const }, ] +function AdminSidebar() { + const { logout } = useAuth() + const navigate = useNavigate() + const location = useLocation() + // 新建/编辑共用「新建笔记」高亮(设计稿:编辑页高亮新建入口) + const editing = location.pathname.startsWith('/admin/notes') + return ( + + ) +} + +function AdminTabbar() { + const location = useLocation() + const editing = location.pathname.startsWith('/admin/notes') + return ( + + ) +} + export function AdminLayout() { return ( -
- -
- -
- -
+
+ +
+
+
) } diff --git a/web/src/components/MarkdownViewer.tsx b/web/src/components/MarkdownViewer.tsx index 0c00810..96edb7e 100644 --- a/web/src/components/MarkdownViewer.tsx +++ b/web/src/components/MarkdownViewer.tsx @@ -6,6 +6,7 @@ import { sanitizePlugin } from '../lib/sanitize' /** * MarkdownViewer:浏览态渲染管线(§8.2),组件边界独立、可整体替换。 + * 排版样式由 index.css 的 .pn-note-body 提供(设计稿正文排版契约)。 * * Markdown 原文 * → remark-gfm(表格/任务列表/删除线/自动链接) @@ -16,7 +17,7 @@ import { sanitizePlugin } from '../lib/sanitize' */ function CodeBlock(props: React.HTMLAttributes) { return ( -
+
       
     
@@ -28,7 +29,7 @@ function CopyButton() { return ( -
+ -
- - - - -