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 ( -
+ -
- - - - -