对移动端的UI进行了重构

This commit is contained in:
2026-02-25 10:33:37 +08:00
parent ee3de1968c
commit 5709ef2966
16 changed files with 689 additions and 562 deletions
+94 -55
View File
@@ -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 (
<ImportProvider>
<div className="relative flex h-[100dvh] w-full overflow-hidden bg-background">
<SearchCommand />
<div className={cn(activePageId ? "hidden" : "block h-full flex-1 md:hidden")}>
<SidebarContent />
</div>
@@ -102,50 +108,50 @@ export default function Home() {
<main className={cn("relative z-0 flex h-full flex-1 flex-col overflow-hidden", !activePageId && "hidden md:flex")}>
{activePage ? (
<div className="flex-1 overflow-y-auto scroll-smooth">
<div className="mx-auto min-h-screen max-w-7xl px-4 py-6 md:px-16">
<div className="ui-enter group relative mb-8">
<div className="mx-auto min-h-screen max-w-7xl px-3 py-4 md:px-16 md:py-6">
<div className="ui-enter group relative mb-6 pt-10 md:mb-8 md:pt-0">
<button
className="absolute -top-12 left-0 flex items-center gap-1 py-2 text-sm text-muted-foreground transition-colors hover:text-foreground md:hidden"
className="absolute left-0 top-0 flex h-9 items-center gap-1 py-2 text-sm text-muted-foreground transition-colors hover:text-foreground md:hidden"
onClick={() => useEditorStore.getState().setActivePageId(null)}
>
<ChevronLeft size={18} />
</button>
<div className="mb-4 flex flex-wrap items-center gap-1 text-sm text-muted-foreground">
<div className="mb-3 flex flex-wrap items-center gap-1 text-xs text-muted-foreground md:mb-4 md:text-sm">
{breadcrumbs.map((crumb, index) => (
<div key={crumb.id} className="flex items-center gap-1">
<div key={crumb.id} className="flex min-w-0 items-center gap-1">
{index > 0 && <ChevronRight size={14} className="opacity-50" />}
<button
onClick={() => useEditorStore.getState().setActivePageId(crumb.id)}
className={cn(
"flex items-center gap-1 transition-colors hover:text-foreground hover:underline",
"flex min-w-0 items-center gap-1 transition-colors hover:text-foreground hover:underline",
crumb.id === activePage.id && "pointer-events-none font-medium text-foreground"
)}
>
{crumb.icon && <span>{crumb.icon}</span>}
<span>{crumb.title || "无标题"}</span>
<span className="truncate">{crumb.title || "无标题"}</span>
</button>
</div>
))}
</div>
<div className="flex items-center gap-2">
{activePage.icon && <span className="select-none text-3xl">{activePage.icon}</span>}
<div className="flex items-start gap-2">
{activePage.icon && <span className="select-none pt-0.5 text-[28px] md:text-3xl">{activePage.icon}</span>}
<input
value={activePage.title}
onChange={(e) => 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"
)}
/>
</div>
</div>
<div className="ui-enter-delayed mb-6 flex flex-wrap items-center gap-2">
<div className="ui-enter-delayed relative z-20 mb-6 flex flex-wrap items-center gap-2">
{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="移除标签"
>
<X size={10} />
@@ -205,7 +211,7 @@ export default function Home() {
</button>
)}
<div className="relative">
<div className="relative z-30">
<button
onClick={() => setIsIconPickerOpen((v) => !v)}
className="ui-btn-secondary px-2 py-1 text-xs text-muted-foreground"
@@ -213,8 +219,8 @@ export default function Home() {
<Sparkles size={12} /> {activePage.icon ? "更换图标" : "添加图标"}
</button>
{isIconPickerOpen && (
<div className="ui-card absolute left-0 top-full z-50 mt-1 w-80 p-2 shadow-xl animate-in fade-in zoom-in-95">
<div className="grid h-48 grid-cols-8 gap-1 overflow-y-auto pr-1">
<div className="ui-card absolute left-0 top-full z-[120] mt-1 w-[min(92vw,20rem)] p-2 shadow-xl animate-in fade-in zoom-in-95 sm:w-80">
<div className="grid h-48 grid-cols-6 gap-1 overflow-y-auto pr-1 sm:grid-cols-8">
{ICONS.map((icon) => (
<button
key={icon}
@@ -258,7 +264,7 @@ export default function Home() {
</button>
{activePage.updatedAt && (
<div className="ml-1 h-4 border-l pl-2 text-xs text-muted-foreground/50">
<div className="w-full pt-0.5 text-[11px] text-muted-foreground/60 md:ml-1 md:h-4 md:w-auto md:border-l md:pl-2 md:pt-0 md:text-xs md:text-muted-foreground/50">
{new Date(activePage.updatedAt).toLocaleString("zh-CN", {
timeZone: timezone || "Asia/Shanghai",
hour12: false,
@@ -268,7 +274,7 @@ export default function Home() {
</div>
{activePage.type === "folder" ? (
<div className="ui-card ui-enter flex h-[50vh] flex-col items-center justify-center gap-3 text-muted-foreground">
<div className="ui-card ui-enter flex h-[45vh] flex-col items-center justify-center gap-3 text-muted-foreground md:h-[50vh]">
<div className="ui-float rounded-2xl border border-border/70 bg-muted/40 p-4">
<FolderOpen size={34} className="text-primary/80" />
</div>
@@ -287,8 +293,8 @@ export default function Home() {
editable={!activePage.isLocked}
/>
<section className="ui-enter mt-8 space-y-4">
<div className="ui-card p-4">
<section className="ui-enter mt-6 space-y-3 md:mt-8 md:space-y-4">
<div className="ui-card p-3 md:p-4">
<h3 className="text-sm font-semibold text-foreground"></h3>
<p className="mt-1 text-xs text-muted-foreground">使 `[[页面名]]` `Ctrl/Cmd + 点击` </p>
<div className="mt-3 flex flex-wrap gap-2">
@@ -309,7 +315,7 @@ export default function Home() {
</div>
</div>
<div className="ui-card p-4">
<div className="ui-card p-3 md:p-4">
<h3 className="text-sm font-semibold text-foreground"></h3>
<p className="mt-1 text-xs text-muted-foreground"></p>
<div className="mt-3 flex flex-wrap gap-2">
@@ -330,7 +336,7 @@ export default function Home() {
</div>
</div>
<div className="ui-card p-4">
<div className="ui-card p-3 md:p-4">
<h3 className="text-sm font-semibold text-foreground"></h3>
<p className="mt-1 text-xs text-muted-foreground"> `[[页面名]]` </p>
<div className="mt-3 flex flex-wrap gap-2">
@@ -353,47 +359,78 @@ export default function Home() {
</div>
</div>
<div className="ui-card p-4">
<div className="flex items-center gap-2">
<History size={14} className="text-muted-foreground" />
<h3 className="text-sm font-semibold text-foreground"></h3>
</div>
<div className="ui-card p-3 md:p-4">
<button
onClick={() => setIsHistoryOpen((v) => !v)}
className="flex w-full items-center justify-between rounded-md px-1 py-1 text-left transition-colors hover:bg-muted/45"
aria-expanded={isHistoryOpen}
aria-label="切换本地历史版本面板"
>
<div className="flex items-center gap-2">
<History size={14} className="text-muted-foreground" />
<h3 className="text-sm font-semibold text-foreground"></h3>
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">{localHistory.length}</span>
</div>
<ChevronDown size={15} className={cn("text-muted-foreground transition-transform", isHistoryOpen && "rotate-180")} />
</button>
<p className="mt-1 text-xs text-muted-foreground"></p>
<div className="mt-3 space-y-2">
{localHistory.length > 0 ? (
localHistory.slice(0, 8).map((snapshot, index) => (
<div key={`${snapshot.timestamp}-${index}`} className="flex items-center justify-between rounded-md border border-border/60 bg-muted/20 px-2.5 py-2">
<div className="min-w-0">
{isHistoryOpen && (
<div className="mt-3 space-y-3">
{localHistory.length > 0 ? (
<>
<div className="rounded-lg border border-border/60 bg-muted/20 p-2">
<label className="mb-1 block text-[11px] text-muted-foreground"></label>
<select
value={selectedHistoryIndex}
onChange={(e) => setSelectedHistoryIndex(parseInt(e.target.value, 10))}
className="ui-select h-9 py-1 text-xs"
>
{localHistory.slice(0, 20).map((snapshot, index) => (
<option key={`${snapshot.timestamp}-${index}`} value={index}>
{new Date(snapshot.timestamp).toLocaleString("zh-CN", {
timeZone: timezone || "Asia/Shanghai",
hour12: false,
})}
{` · ${snapshot.title || "无标题"}`}
</option>
))}
</select>
</div>
<div className="rounded-md border border-border/60 bg-muted/15 px-2.5 py-2">
<div className="truncate text-xs text-foreground">
{snapshot.title || "无标题"}
{localHistory[effectiveHistoryIndex]?.title || "无标题"}
</div>
<div className="text-[11px] text-muted-foreground">
{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,
})
: "-"}
</div>
</div>
<button
onClick={async () => {
const target = localHistory[effectiveHistoryIndex];
if (!target || !activePage) return;
const ok = window.confirm("恢复该历史版本后将覆盖当前内容,是否继续?");
if (!ok || !activePage) return;
if (!ok) return;
await updatePage(activePage.id, {
title: snapshot.title,
content: snapshot.content,
title: target.title,
content: target.content,
});
}}
className="ui-btn-secondary px-2 py-1 text-xs"
className="ui-btn-secondary h-9 w-full justify-center px-2 py-1 text-xs sm:h-auto sm:w-auto sm:justify-start"
>
<RotateCcw size={12} />
</button>
</div>
))
) : (
<span className="text-xs text-muted-foreground"></span>
)}
</div>
</>
) : (
<span className="text-xs text-muted-foreground"></span>
)}
</div>
)}
</div>
</section>
</div>
@@ -401,13 +438,15 @@ export default function Home() {
</div>
</div>
) : (
<div className="ui-enter flex h-full flex-col items-center justify-center gap-4 text-muted-foreground">
<div className="ui-float rounded-2xl border border-border/70 bg-muted/40 p-4">
<span className="text-4xl">🧠</span>
</div>
<div className="space-y-1 text-center">
<h3 className="text-lg font-semibold text-foreground">使 NoteAI</h3>
<p className="text-sm text-muted-foreground/90"></p>
<div className="ui-enter flex h-full items-center justify-center p-4 md:p-8">
<div className="ui-card flex w-full max-w-md flex-col items-center gap-3 px-6 py-8 text-center text-muted-foreground">
<div className="ui-float rounded-2xl border border-border/70 bg-muted/40 p-4">
<span className="text-4xl">🧠</span>
</div>
<div className="space-y-1">
<h3 className="text-lg font-semibold text-foreground">使 NoteAI</h3>
<p className="text-sm text-muted-foreground/90"></p>
</div>
</div>
</div>
)}