2025-11-23 10:55:04 +08:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { useState, type ComponentType } from "react";
|
2026-02-01 08:47:40 +08:00
|
|
|
import { BookOpenCheck, Focus, ListOrdered, ListTree, Maximize2, ShieldCheck, Type, MessageSquare } from "lucide-react";
|
2025-11-23 10:55:04 +08:00
|
|
|
import { cn } from "@/lib/utils";
|
2026-02-01 08:47:40 +08:00
|
|
|
import type { BooleanPageOptionKey, DocumentStats, PageFont, PageLayoutDensity, PageOptionsState } from "@/types/page-options";
|
2025-11-23 10:55:04 +08:00
|
|
|
import { DocumentTaskPanel } from "@/components/document-task-panel";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
2026-02-01 08:47:40 +08:00
|
|
|
import { useAppPreferencesStore, type ThemeMode } from "@/store/app-preferences";
|
2025-11-23 10:55:04 +08:00
|
|
|
|
|
|
|
|
type TabId = "page" | "custom" | "global";
|
|
|
|
|
|
|
|
|
|
const TABS: Array<{ id: TabId; label: string }> = [
|
|
|
|
|
{ id: "page", label: "页面选项" },
|
|
|
|
|
{ id: "custom", label: "自定义页面" },
|
|
|
|
|
{ id: "global", label: "全局选项" },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const OPTION_META: Record<
|
2026-02-01 08:47:40 +08:00
|
|
|
BooleanPageOptionKey,
|
2025-11-23 10:55:04 +08:00
|
|
|
{ label: string; description: string; icon: ComponentType<{ className?: string }> }
|
|
|
|
|
> = {
|
|
|
|
|
wideLayout: {
|
|
|
|
|
label: "自适应宽度",
|
|
|
|
|
description: "让编辑区域根据屏幕自动铺满",
|
|
|
|
|
icon: Maximize2,
|
|
|
|
|
},
|
|
|
|
|
smallText: {
|
|
|
|
|
label: "小字体",
|
|
|
|
|
description: "使用更紧凑的字号排版",
|
|
|
|
|
icon: Type,
|
|
|
|
|
},
|
|
|
|
|
showHeadingNumbers: {
|
|
|
|
|
label: "标题编号",
|
|
|
|
|
description: "自动为标题添加编号",
|
|
|
|
|
icon: ListOrdered,
|
|
|
|
|
},
|
|
|
|
|
showToc: {
|
|
|
|
|
label: "显示目录",
|
|
|
|
|
description: "在右侧展示目录导航",
|
|
|
|
|
icon: ListTree,
|
|
|
|
|
},
|
|
|
|
|
protectEditing: {
|
|
|
|
|
label: "编辑保护",
|
|
|
|
|
description: "保护内容避免误触修改",
|
|
|
|
|
icon: ShieldCheck,
|
|
|
|
|
},
|
|
|
|
|
showWordCount: {
|
|
|
|
|
label: "字数提示",
|
|
|
|
|
description: "实时展示字数和块统计",
|
|
|
|
|
icon: BookOpenCheck,
|
|
|
|
|
},
|
2026-02-01 08:47:40 +08:00
|
|
|
collapseBacklinks: {
|
|
|
|
|
label: "折叠反向引用",
|
|
|
|
|
description: "默认折叠页面底部的反向引用列表",
|
|
|
|
|
icon: Focus,
|
|
|
|
|
},
|
|
|
|
|
hideChildPages: {
|
|
|
|
|
label: "隐藏子页面",
|
|
|
|
|
description: "隐藏通过 /ym 创建的子页面块(不会删除内容)",
|
|
|
|
|
icon: Focus,
|
|
|
|
|
},
|
|
|
|
|
showBlockRefCount: {
|
|
|
|
|
label: "显示块引用数字",
|
|
|
|
|
description: "显示块被引用次数(当前为占位,后续补齐)",
|
|
|
|
|
icon: Focus,
|
|
|
|
|
},
|
2025-11-23 10:55:04 +08:00
|
|
|
};
|
|
|
|
|
|
2026-02-01 08:47:40 +08:00
|
|
|
const PAGE_OPTIONS: BooleanPageOptionKey[] = [
|
|
|
|
|
"wideLayout",
|
|
|
|
|
"smallText",
|
|
|
|
|
"showHeadingNumbers",
|
|
|
|
|
"showToc",
|
|
|
|
|
"protectEditing",
|
|
|
|
|
"showWordCount",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const CUSTOM_PAGE_OPTIONS: BooleanPageOptionKey[] = ["collapseBacklinks", "hideChildPages", "showBlockRefCount"];
|
2025-11-23 10:55:04 +08:00
|
|
|
|
|
|
|
|
interface PageOptionsSidebarProps {
|
|
|
|
|
documentId: string;
|
|
|
|
|
options: PageOptionsState;
|
|
|
|
|
stats?: DocumentStats;
|
2026-02-01 08:47:40 +08:00
|
|
|
onToggle: (key: BooleanPageOptionKey) => void;
|
|
|
|
|
onSetPageFont?: (font: PageFont) => void;
|
|
|
|
|
onSetLayoutDensity?: (density: PageLayoutDensity) => void;
|
|
|
|
|
onSetEmbedDefaultToCursor?: () => void;
|
|
|
|
|
onClearEmbedDefault?: () => void;
|
2025-11-23 10:55:04 +08:00
|
|
|
onExport: () => void;
|
|
|
|
|
onOpenHistory: () => void;
|
2026-02-01 08:47:40 +08:00
|
|
|
onOpenComments?: () => void;
|
|
|
|
|
onUndo?: () => void;
|
|
|
|
|
onRedo?: () => void;
|
|
|
|
|
onDeletePage?: () => void;
|
|
|
|
|
onOpenMoveEmbedPicker?: () => void;
|
|
|
|
|
onCopyPageLink?: (includeTitle: boolean) => void;
|
|
|
|
|
onCopyPageReference?: (mode: "inline" | "embed") => void;
|
|
|
|
|
onAddToTemplates?: () => void;
|
2025-11-23 10:55:04 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function PageOptionsSidebar({
|
|
|
|
|
documentId,
|
|
|
|
|
options,
|
|
|
|
|
stats,
|
|
|
|
|
onToggle,
|
2026-02-01 08:47:40 +08:00
|
|
|
onSetPageFont,
|
|
|
|
|
onSetLayoutDensity,
|
|
|
|
|
onSetEmbedDefaultToCursor,
|
|
|
|
|
onClearEmbedDefault,
|
2025-11-23 10:55:04 +08:00
|
|
|
onExport,
|
|
|
|
|
onOpenHistory,
|
2026-02-01 08:47:40 +08:00
|
|
|
onOpenComments,
|
|
|
|
|
onUndo,
|
|
|
|
|
onRedo,
|
|
|
|
|
onDeletePage,
|
|
|
|
|
onOpenMoveEmbedPicker,
|
|
|
|
|
onCopyPageLink,
|
|
|
|
|
onCopyPageReference,
|
|
|
|
|
onAddToTemplates,
|
2025-11-23 10:55:04 +08:00
|
|
|
}: PageOptionsSidebarProps) {
|
|
|
|
|
const [activeTab, setActiveTab] = useState<TabId>("page");
|
2026-02-01 08:47:40 +08:00
|
|
|
const theme = useAppPreferencesStore((s) => s.theme);
|
|
|
|
|
const showStructure = useAppPreferencesStore((s) => s.showStructure);
|
|
|
|
|
const spellCheck = useAppPreferencesStore((s) => s.spellCheck);
|
|
|
|
|
const flightMode = useAppPreferencesStore((s) => s.flightMode);
|
|
|
|
|
const setTheme = useAppPreferencesStore((s) => s.setTheme);
|
|
|
|
|
const setShowStructure = useAppPreferencesStore((s) => s.setShowStructure);
|
|
|
|
|
const setSpellCheck = useAppPreferencesStore((s) => s.setSpellCheck);
|
|
|
|
|
const setFlightMode = useAppPreferencesStore((s) => s.setFlightMode);
|
2025-11-23 10:55:04 +08:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<aside className="flex h-full w-80 shrink-0 flex-col border-l border-[#f0f0f0] bg-white/95">
|
|
|
|
|
<div className="flex border-b border-[#f5f5f5]">
|
|
|
|
|
{TABS.map((tab) => (
|
|
|
|
|
<button
|
|
|
|
|
key={tab.id}
|
|
|
|
|
type="button"
|
|
|
|
|
className={cn(
|
|
|
|
|
"flex-1 border-b-2 px-4 py-3 text-sm font-medium text-gray-500",
|
|
|
|
|
activeTab === tab.id ? "border-[#2563eb] text-[#2563eb]" : "border-transparent hover:text-gray-700",
|
|
|
|
|
)}
|
|
|
|
|
onClick={() => setActiveTab(tab.id)}
|
|
|
|
|
>
|
|
|
|
|
{tab.label}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex-1 overflow-y-auto p-4">
|
|
|
|
|
{activeTab === "page" && (
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
{options.showWordCount && stats && (
|
|
|
|
|
<section className="rounded-2xl border border-[#eef1f6] p-4">
|
|
|
|
|
<div className="text-sm font-semibold text-gray-800">页面数据</div>
|
|
|
|
|
<div className="mt-3 grid grid-cols-3 gap-2 text-center text-xs text-gray-500">
|
|
|
|
|
<StatsCell label="字数" value={stats.wordCount} />
|
|
|
|
|
<StatsCell label="字符" value={stats.characterCount} />
|
|
|
|
|
<StatsCell label="块数" value={stats.blockCount} />
|
|
|
|
|
</div>
|
2026-02-01 08:47:40 +08:00
|
|
|
<div className="mt-3 grid grid-cols-2 gap-2 text-center text-xs text-gray-500">
|
|
|
|
|
<StatsCell label="待办总数" value={stats.todoTotal} />
|
|
|
|
|
<StatsCell label="已完成" value={stats.todoDone} />
|
|
|
|
|
</div>
|
2025-11-23 10:55:04 +08:00
|
|
|
</section>
|
|
|
|
|
)}
|
2026-02-01 08:47:40 +08:00
|
|
|
<OptionToggleGroup title="页面选项" optionKeys={PAGE_OPTIONS} options={options} onToggle={onToggle} />
|
2025-11-23 10:55:04 +08:00
|
|
|
<section className="rounded-2xl border border-[#eef1f6] p-4 text-sm text-gray-600">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<span className="font-semibold text-gray-800">页面操作</span>
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<Button type="button" variant="outline" size="sm" onClick={onExport}>
|
|
|
|
|
导出
|
|
|
|
|
</Button>
|
|
|
|
|
<Button type="button" variant="outline" size="sm" onClick={onOpenHistory}>
|
|
|
|
|
历史
|
|
|
|
|
</Button>
|
2026-02-01 08:47:40 +08:00
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={onOpenComments}
|
|
|
|
|
disabled={!onOpenComments}
|
|
|
|
|
title={onOpenComments ? "打开评论" : "评论功能未启用"}
|
|
|
|
|
>
|
|
|
|
|
<MessageSquare className="mr-1 h-4 w-4" />
|
|
|
|
|
评论
|
|
|
|
|
</Button>
|
2025-11-23 10:55:04 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="mt-2 text-xs text-gray-400">导出最新快照或打开历史版本面板。</p>
|
|
|
|
|
</section>
|
2026-02-01 08:47:40 +08:00
|
|
|
|
|
|
|
|
<section className="rounded-2xl border border-[#eef1f6] p-4 text-sm text-gray-600">
|
|
|
|
|
<div className="text-sm font-semibold text-gray-800">快捷操作</div>
|
|
|
|
|
<div className="mt-3 flex flex-wrap gap-2">
|
|
|
|
|
<Button type="button" size="sm" variant="outline" onClick={onUndo} disabled={!onUndo}>
|
|
|
|
|
撤回
|
|
|
|
|
</Button>
|
|
|
|
|
<Button type="button" size="sm" variant="outline" onClick={onRedo} disabled={!onRedo}>
|
|
|
|
|
重做
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={onOpenMoveEmbedPicker}
|
|
|
|
|
disabled={!onOpenMoveEmbedPicker}
|
|
|
|
|
>
|
|
|
|
|
移动/嵌入到...
|
|
|
|
|
</Button>
|
|
|
|
|
<Button type="button" size="sm" variant="outline" onClick={onAddToTemplates} disabled={!onAddToTemplates}>
|
|
|
|
|
添加为模板
|
|
|
|
|
</Button>
|
|
|
|
|
<Button type="button" size="sm" variant="outline" onClick={() => onCopyPageLink?.(false)} disabled={!onCopyPageLink}>
|
|
|
|
|
复制页面链接
|
|
|
|
|
</Button>
|
|
|
|
|
<Button type="button" size="sm" variant="outline" onClick={() => onCopyPageLink?.(true)} disabled={!onCopyPageLink}>
|
|
|
|
|
复制链接(带标题)
|
|
|
|
|
</Button>
|
|
|
|
|
<Button type="button" size="sm" variant="outline" onClick={() => onCopyPageReference?.("inline")} disabled={!onCopyPageReference}>
|
|
|
|
|
行内页面引用
|
|
|
|
|
</Button>
|
|
|
|
|
<Button type="button" size="sm" variant="outline" onClick={() => onCopyPageReference?.("embed")} disabled={!onCopyPageReference}>
|
|
|
|
|
嵌入页面引用
|
|
|
|
|
</Button>
|
|
|
|
|
<Button type="button" size="sm" variant="destructive" onClick={onDeletePage} disabled={!onDeletePage}>
|
|
|
|
|
删除页面
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="mt-2 text-xs text-gray-400">
|
|
|
|
|
提示:标题目录快捷键为 <span className="font-mono">Ctrl/Cmd + Shift + L</span>。
|
|
|
|
|
</p>
|
|
|
|
|
</section>
|
2025-11-23 10:55:04 +08:00
|
|
|
<DocumentTaskPanel documentId={documentId} />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{activeTab === "custom" && (
|
|
|
|
|
<div className="space-y-5">
|
2026-02-01 08:47:40 +08:00
|
|
|
<section className="rounded-2xl border border-[#eef1f6] p-4">
|
|
|
|
|
<div className="text-sm font-semibold text-gray-800">字体</div>
|
|
|
|
|
<p className="mt-1 text-xs text-gray-400">仅对当前页面生效。</p>
|
|
|
|
|
<div className="mt-3 flex flex-wrap gap-2">
|
|
|
|
|
{([
|
|
|
|
|
{ id: "default", label: "默认" },
|
|
|
|
|
{ id: "song", label: "宋体" },
|
|
|
|
|
{ id: "kai", label: "楷体" },
|
|
|
|
|
] as Array<{ id: PageFont; label: string }>).map((item) => (
|
|
|
|
|
<Button
|
|
|
|
|
key={item.id}
|
|
|
|
|
type="button"
|
|
|
|
|
size="sm"
|
|
|
|
|
variant={options.pageFont === item.id ? "default" : "outline"}
|
|
|
|
|
onClick={() => onSetPageFont?.(item.id)}
|
|
|
|
|
disabled={!onSetPageFont}
|
|
|
|
|
>
|
|
|
|
|
{item.label}
|
|
|
|
|
</Button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section className="rounded-2xl border border-[#eef1f6] p-4">
|
|
|
|
|
<div className="text-sm font-semibold text-gray-800">布局</div>
|
|
|
|
|
<p className="mt-1 text-xs text-gray-400">控制段落行高与标题间距。</p>
|
|
|
|
|
<div className="mt-3 flex flex-wrap gap-2">
|
|
|
|
|
{([
|
|
|
|
|
{ id: "compact", label: "紧凑" },
|
|
|
|
|
{ id: "normal", label: "默认" },
|
|
|
|
|
{ id: "spacious", label: "宽容" },
|
|
|
|
|
] as Array<{ id: PageLayoutDensity; label: string }>).map((item) => (
|
|
|
|
|
<Button
|
|
|
|
|
key={item.id}
|
|
|
|
|
type="button"
|
|
|
|
|
size="sm"
|
|
|
|
|
variant={options.layoutDensity === item.id ? "default" : "outline"}
|
|
|
|
|
onClick={() => onSetLayoutDensity?.(item.id)}
|
|
|
|
|
disabled={!onSetLayoutDensity}
|
|
|
|
|
>
|
|
|
|
|
{item.label}
|
|
|
|
|
</Button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<OptionToggleGroup title="反向链接" optionKeys={CUSTOM_PAGE_OPTIONS} options={options} onToggle={onToggle} />
|
|
|
|
|
|
|
|
|
|
<section className="rounded-2xl border border-[#eef1f6] p-4">
|
|
|
|
|
<div className="text-sm font-semibold text-gray-800">嵌入默认位置</div>
|
|
|
|
|
<p className="mt-1 text-xs text-gray-400">
|
|
|
|
|
当其它页面/块“嵌入到...”当前页面时,默认插入到指定块之后。
|
|
|
|
|
</p>
|
|
|
|
|
<div className="mt-3 rounded-xl bg-[#f9fafc] px-3 py-2 text-xs text-gray-600">
|
|
|
|
|
当前:{options.embedDefaultBlockId ? options.embedDefaultBlockId : "未设置"}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-3 flex gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={onSetEmbedDefaultToCursor}
|
|
|
|
|
disabled={!onSetEmbedDefaultToCursor}
|
|
|
|
|
>
|
|
|
|
|
使用当前光标块
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={onClearEmbedDefault}
|
|
|
|
|
disabled={!onClearEmbedDefault || !options.embedDefaultBlockId}
|
|
|
|
|
>
|
|
|
|
|
清除
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section className="rounded-2xl border border-[#eef1f6] p-4 text-xs text-gray-500">
|
|
|
|
|
<div className="text-sm font-semibold text-gray-800">更多自定义项(待补齐)</div>
|
|
|
|
|
<ul className="mt-2 list-disc space-y-1 pl-4">
|
|
|
|
|
<li>显示块引用数字:需要补齐“块被引用统计”数据源</li>
|
|
|
|
|
</ul>
|
|
|
|
|
</section>
|
2025-11-23 10:55:04 +08:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{activeTab === "global" && (
|
|
|
|
|
<div className="space-y-5">
|
2026-02-01 08:47:40 +08:00
|
|
|
<section className="rounded-2xl border border-[#eef1f6] p-4">
|
|
|
|
|
<div className="text-sm font-semibold text-gray-800">辅助提示</div>
|
|
|
|
|
<PreferenceRow
|
|
|
|
|
title="显示块结构"
|
|
|
|
|
description="显示块级元素的结构边界(虚线框)。快捷键:Ctrl/Cmd + Shift + U。"
|
|
|
|
|
enabled={showStructure}
|
|
|
|
|
onToggle={() => setShowStructure(!showStructure)}
|
|
|
|
|
/>
|
|
|
|
|
<PreferenceRow
|
|
|
|
|
title="拼写检查"
|
|
|
|
|
description="控制编辑器的浏览器拼写检查(spellcheck)。"
|
|
|
|
|
enabled={spellCheck}
|
|
|
|
|
onToggle={() => setSpellCheck(!spellCheck)}
|
|
|
|
|
/>
|
|
|
|
|
</section>
|
|
|
|
|
<section className="rounded-2xl border border-[#eef1f6] p-4">
|
|
|
|
|
<div className="text-sm font-semibold text-gray-800">风格</div>
|
|
|
|
|
<p className="mt-1 text-xs text-gray-400">Good Night(深色主题)与跟随系统。快捷键:Ctrl/Cmd + Alt/Opt + G。</p>
|
|
|
|
|
<div className="mt-3 flex gap-2">
|
|
|
|
|
{(["system", "light", "dark"] as ThemeMode[]).map((mode) => (
|
|
|
|
|
<Button
|
|
|
|
|
key={mode}
|
|
|
|
|
type="button"
|
|
|
|
|
size="sm"
|
|
|
|
|
variant={theme === mode ? "default" : "outline"}
|
|
|
|
|
onClick={() => setTheme(mode)}
|
|
|
|
|
>
|
|
|
|
|
{mode === "system" ? "跟随系统" : mode === "dark" ? "深色" : "浅色"}
|
|
|
|
|
</Button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section className="rounded-2xl border border-[#eef1f6] p-4">
|
|
|
|
|
<div className="text-sm font-semibold text-gray-800">其它</div>
|
|
|
|
|
<PreferenceRow
|
|
|
|
|
title="飞行模式"
|
|
|
|
|
description="开启后默认关闭 AI 面板的“联网”开关(避免误触外部网络)。"
|
|
|
|
|
enabled={flightMode}
|
|
|
|
|
onToggle={() => setFlightMode(!flightMode)}
|
|
|
|
|
/>
|
2025-11-23 10:55:04 +08:00
|
|
|
</section>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</aside>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function OptionToggleGroup({
|
|
|
|
|
title,
|
|
|
|
|
optionKeys,
|
|
|
|
|
options,
|
|
|
|
|
onToggle,
|
|
|
|
|
}: {
|
|
|
|
|
title: string;
|
2026-02-01 08:47:40 +08:00
|
|
|
optionKeys: BooleanPageOptionKey[];
|
2025-11-23 10:55:04 +08:00
|
|
|
options: PageOptionsState;
|
2026-02-01 08:47:40 +08:00
|
|
|
onToggle: (key: BooleanPageOptionKey) => void;
|
2025-11-23 10:55:04 +08:00
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<section>
|
|
|
|
|
<div className="text-xs font-semibold uppercase tracking-wide text-gray-400">{title}</div>
|
|
|
|
|
<div className="mt-3 space-y-2">
|
|
|
|
|
{optionKeys.map((key) => (
|
|
|
|
|
<OptionToggle key={key} optionKey={key} options={options} onToggle={onToggle} />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function OptionToggle({
|
|
|
|
|
optionKey,
|
|
|
|
|
options,
|
|
|
|
|
onToggle,
|
|
|
|
|
}: {
|
2026-02-01 08:47:40 +08:00
|
|
|
optionKey: BooleanPageOptionKey;
|
2025-11-23 10:55:04 +08:00
|
|
|
options: PageOptionsState;
|
2026-02-01 08:47:40 +08:00
|
|
|
onToggle: (key: BooleanPageOptionKey) => void;
|
2025-11-23 10:55:04 +08:00
|
|
|
}) {
|
|
|
|
|
const meta = OPTION_META[optionKey];
|
|
|
|
|
const Icon = meta.icon;
|
|
|
|
|
const active = options[optionKey];
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="flex w-full items-center justify-between rounded-2xl border border-transparent bg-[#f9fafc] px-3 py-2 text-left shadow-sm transition hover:border-[#dbe7ff]"
|
|
|
|
|
onClick={() => onToggle(optionKey)}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"rounded-xl border p-2",
|
|
|
|
|
active ? "border-[#cddfff] bg-[#eef4ff]" : "border-transparent bg-white",
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<Icon className={cn("h-4 w-4", active ? "text-[#2563eb]" : "text-gray-500")} />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div className="text-sm font-medium text-gray-900">{meta.label}</div>
|
|
|
|
|
<div className="text-xs text-gray-400">{meta.description}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<span className={cn("text-xs font-semibold", active ? "text-[#2563eb]" : "text-gray-400")}>
|
|
|
|
|
{active ? "已开启" : "已关闭"}
|
|
|
|
|
</span>
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function StatsCell({ label, value }: { label: string; value: number }) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="rounded-xl bg-white py-3 text-center shadow-sm">
|
|
|
|
|
<div className="text-xs text-gray-400">{label}</div>
|
|
|
|
|
<div className="mt-1 text-lg font-semibold text-gray-900">{value}</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-02-01 08:47:40 +08:00
|
|
|
|
|
|
|
|
function PreferenceRow({
|
|
|
|
|
title,
|
|
|
|
|
description,
|
|
|
|
|
enabled,
|
|
|
|
|
onToggle,
|
|
|
|
|
}: {
|
|
|
|
|
title: string;
|
|
|
|
|
description: string;
|
|
|
|
|
enabled: boolean;
|
|
|
|
|
onToggle: () => void;
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="mt-3 flex items-start justify-between gap-3 rounded-xl bg-[#f9fafc] px-3 py-2">
|
|
|
|
|
<div className="min-w-0">
|
|
|
|
|
<div className="text-sm font-medium text-gray-900">{title}</div>
|
|
|
|
|
<div className="text-xs text-gray-400">{description}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<Button type="button" size="sm" variant={enabled ? "default" : "outline"} onClick={onToggle}>
|
|
|
|
|
{enabled ? "已开启" : "已关闭"}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|