polish(admin): 抽出后台控件包并统一各页布局与交互
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
Palette,
|
||||
@@ -10,9 +9,9 @@ import {
|
||||
Save,
|
||||
Undo2,
|
||||
RotateCcw,
|
||||
ChevronRight,
|
||||
Pipette,
|
||||
} from "lucide-react";
|
||||
import { AdminPageHeader, AdminSection } from "@/components/admin";
|
||||
import { apiUpdateSiteSettings } from "@/lib/api";
|
||||
import { toast } from "@/lib/toast";
|
||||
import {
|
||||
@@ -78,181 +77,184 @@ export default function AppearanceAdmin({ initialAccent }: { initialAccent: stri
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto">
|
||||
{/* 面包屑 */}
|
||||
<nav className="meta mb-5 flex items-center gap-1.5 text-[13px]">
|
||||
<Link href="/" className="hover:text-[var(--accent)] transition-colors">首页</Link>
|
||||
<ChevronRight size={13} />
|
||||
<span style={{ color: "var(--accent)" }}>外观设置</span>
|
||||
</nav>
|
||||
<div className="max-w-3xl">
|
||||
<AdminPageHeader
|
||||
icon={Palette}
|
||||
title="外观设置"
|
||||
description="配置站点主题色,全站按钮、链接、选中态会统一联动"
|
||||
/>
|
||||
|
||||
<header className="panel p-6 sm:p-7 mb-5 flex items-center gap-4">
|
||||
<span
|
||||
className="w-12 h-12 rounded-2xl flex items-center justify-center shrink-0"
|
||||
style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
|
||||
<div className="flex flex-col gap-4 mb-6">
|
||||
<AdminSection
|
||||
title="预设主题色"
|
||||
description="浅/暗两套色板均已调校,保证文字对比度达标"
|
||||
>
|
||||
<Palette size={24} />
|
||||
</span>
|
||||
<div>
|
||||
<h1 className="text-xl font-extrabold" style={{ color: "var(--ink)" }}>外观设置</h1>
|
||||
<p className="meta mt-1 text-[13px]">配置站点主题色,全站按钮、链接、选中态会统一联动</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* 预设色板 */}
|
||||
<section className="j13-module p-5 sm:p-6 mb-5">
|
||||
<h2 className="text-[15px] font-semibold mb-1" style={{ color: "var(--ink)" }}>
|
||||
预设主题色
|
||||
</h2>
|
||||
<p className="meta text-[12.5px] mb-4">浅/暗两套色板均已调校,保证文字对比度达标</p>
|
||||
<div className="grid grid-cols-3 sm:grid-cols-6 gap-3">
|
||||
{ACCENT_PRESETS.map((p) => {
|
||||
const active = normalized === p.light;
|
||||
const isDefault = p.light === DEFAULT_ACCENT;
|
||||
return (
|
||||
<button
|
||||
key={p.key}
|
||||
type="button"
|
||||
onClick={() => setDraft(p.light)}
|
||||
className="group flex flex-col items-center gap-2 py-3 rounded-[var(--r-sm)] border transition-colors"
|
||||
style={{
|
||||
borderColor: active ? "var(--accent)" : "var(--line)",
|
||||
background: active ? "var(--accent-soft)" : "var(--panel)",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
aria-pressed={active}
|
||||
data-tip={isDefault ? `${p.name}(默认)` : p.name}
|
||||
>
|
||||
<span className="relative flex items-center justify-center">
|
||||
{/* 上半圆=浅色模式,下半圆=暗色模式 */}
|
||||
<span className="w-9 h-9 rounded-full overflow-hidden grid grid-rows-2 border-2 border-[var(--panel)] shadow-sm">
|
||||
<span style={{ background: p.light }} />
|
||||
<span style={{ background: p.dark }} />
|
||||
</span>
|
||||
{active && (
|
||||
<span
|
||||
className="absolute -bottom-1 -right-1 min-w-[18px] min-h-[18px] w-[18px] h-[18px] rounded-full flex items-center justify-center"
|
||||
style={{ background: "var(--accent)", color: "var(--accent-on)" }}
|
||||
>
|
||||
<Check size={11} strokeWidth={3} />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span
|
||||
className="text-[12px] leading-none"
|
||||
style={{ color: active ? "var(--accent)" : "var(--ink-2)" }}
|
||||
<div className="grid grid-cols-3 sm:grid-cols-6 gap-3">
|
||||
{ACCENT_PRESETS.map((p) => {
|
||||
const active = normalized === p.light;
|
||||
const isDefault = p.light === DEFAULT_ACCENT;
|
||||
return (
|
||||
<button
|
||||
key={p.key}
|
||||
type="button"
|
||||
onClick={() => setDraft(p.light)}
|
||||
className="group flex flex-col items-center gap-2 py-3 rounded-[var(--r-sm)] border transition-colors"
|
||||
style={{
|
||||
borderColor: active ? "var(--accent)" : "var(--line)",
|
||||
background: active ? "var(--accent-soft)" : "var(--panel)",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
aria-pressed={active}
|
||||
data-tip={isDefault ? `${p.name}(默认)` : p.name}
|
||||
>
|
||||
{p.name}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 自定义颜色 */}
|
||||
<section className="j13-module p-5 sm:p-6 mb-5">
|
||||
<h2 className="text-[15px] font-semibold mb-1 flex items-center gap-1.5" style={{ color: "var(--ink)" }}>
|
||||
<Pipette size={15} /> 自定义颜色
|
||||
</h2>
|
||||
<p className="meta text-[12.5px] mb-4">
|
||||
取色器选色或输入 HEX 值;浅色下对比度过低时会自动校正,保证按钮与文字可读
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
{/* 原生取色器 */}
|
||||
<label
|
||||
className="relative w-12 h-12 rounded-[var(--r-sm)] overflow-hidden border cursor-pointer shrink-0"
|
||||
style={{ borderColor: "var(--line-2)" }}
|
||||
data-tip="打开取色器"
|
||||
>
|
||||
<input
|
||||
type="color"
|
||||
className="absolute inset-0 w-[200%] h-[200%] -translate-x-1/4 -translate-y-1/4 cursor-pointer border-0 p-0 bg-transparent"
|
||||
value={normalized || DEFAULT_ACCENT}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
aria-label="自定义主题色取色器"
|
||||
/>
|
||||
</label>
|
||||
{/* HEX 文本输入 */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center rounded-[var(--r-sm)] border bg-[var(--panel)] px-3 h-12 font-mono text-[14px] w-40"
|
||||
style={{ borderColor: invalid ? "var(--danger)" : "var(--line-2)" }}
|
||||
>
|
||||
<span style={{ color: "var(--ink-3)" }}>#</span>
|
||||
<input
|
||||
value={draft.startsWith("#") ? draft.slice(1) : draft}
|
||||
onChange={(e) => setDraft("#" + e.target.value.replace(/[^0-9a-fA-F#]/g, ""))}
|
||||
className="flex-1 min-w-0 bg-transparent outline-none ml-1 uppercase"
|
||||
style={{ color: "var(--ink)" }}
|
||||
maxLength={7}
|
||||
spellCheck={false}
|
||||
aria-label="主题色 HEX 值"
|
||||
/>
|
||||
</div>
|
||||
<span className="relative flex items-center justify-center">
|
||||
<span className="w-9 h-9 rounded-full overflow-hidden grid grid-rows-2 border-2 border-[var(--panel)] shadow-sm">
|
||||
<span style={{ background: p.light }} />
|
||||
<span style={{ background: p.dark }} />
|
||||
</span>
|
||||
{active && (
|
||||
<span
|
||||
className="absolute -bottom-1 -right-1 min-w-[18px] min-h-[18px] w-[18px] h-[18px] rounded-full flex items-center justify-center"
|
||||
style={{ background: "var(--accent)", color: "var(--accent-on)" }}
|
||||
>
|
||||
<Check size={11} strokeWidth={3} />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span
|
||||
className="text-[12px] leading-none"
|
||||
style={{ color: active ? "var(--accent)" : "var(--ink-2)" }}
|
||||
>
|
||||
{p.name}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{pair && (
|
||||
<div className="flex items-center gap-2 text-[12px]" style={{ color: "var(--ink-3)" }}>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span className="w-3.5 h-3.5 rounded-full border" style={{ background: pair.light, borderColor: "var(--line)" }} />
|
||||
浅色 {pair.light}
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span className="w-3.5 h-3.5 rounded-full border" style={{ background: pair.dark, borderColor: "var(--line)" }} />
|
||||
暗色 {pair.dark}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{invalid && (
|
||||
<p className="mt-2.5 text-[12.5px]" style={{ color: "var(--danger)" }}>
|
||||
颜色格式无效,请输入 6 位 HEX 值(如 3B5BDB)
|
||||
</p>
|
||||
)}
|
||||
{pair?.adjusted && (
|
||||
<p className="mt-2.5 text-[12.5px]" style={{ color: "var(--gold)" }}>
|
||||
该颜色与白底对比度不足,浅色模式已自动校正为 {pair.light}(暗色模式同步派生 {pair.dark})
|
||||
</p>
|
||||
)}
|
||||
{preset && preset.light === DEFAULT_ACCENT && (
|
||||
<p className="mt-2.5 text-[12.5px] meta">当前为内置默认色,保存时会清除自定义配置</p>
|
||||
)}
|
||||
</section>
|
||||
</AdminSection>
|
||||
|
||||
{/* 实时预览(整页已实时换肤,此处展示典型组件) */}
|
||||
<section className="j13-module p-5 sm:p-6 mb-6">
|
||||
<h2 className="text-[15px] font-semibold mb-4" style={{ color: "var(--ink)" }}>实时预览</h2>
|
||||
<div className="rounded-[var(--r-sm)] border p-4 space-y-4" style={{ borderColor: "var(--line)", background: "var(--bg)" }}>
|
||||
<div className="flex flex-wrap items-center gap-2.5">
|
||||
<button type="button" className="btn btn-primary btn-sm">
|
||||
<Save size={14} /> 主按钮
|
||||
</button>
|
||||
<button type="button" className="btn btn-line btn-sm">次要按钮</button>
|
||||
<a className="text-[13px]" style={{ color: "var(--accent)" }} href="#preview" onClick={(e) => e.preventDefault()}>
|
||||
文字链接示例
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="j13-tab" aria-selected="false">未选中</span>
|
||||
<span className="j13-tab" aria-selected="true">选中标签</span>
|
||||
<span
|
||||
className="inline-flex items-center rounded-full px-3.5 py-2 text-[13.5px]"
|
||||
style={{ background: "var(--nav-active-bg)", color: "var(--nav-active-ink)" }}
|
||||
>
|
||||
导航选中态
|
||||
<AdminSection
|
||||
title={
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<Pipette size={15} /> 自定义颜色
|
||||
</span>
|
||||
}
|
||||
description="取色器选色或输入 HEX 值;浅色下对比度过低时会自动校正,保证按钮与文字可读"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<label
|
||||
className="relative w-11 h-11 rounded-[var(--r-sm)] overflow-hidden border cursor-pointer shrink-0"
|
||||
style={{ borderColor: "var(--line-2)" }}
|
||||
data-tip="打开取色器"
|
||||
>
|
||||
<input
|
||||
type="color"
|
||||
className="absolute inset-0 w-[200%] h-[200%] -translate-x-1/4 -translate-y-1/4 cursor-pointer border-0 p-0 bg-transparent"
|
||||
value={normalized || DEFAULT_ACCENT}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
aria-label="自定义主题色取色器"
|
||||
/>
|
||||
</label>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div
|
||||
className="flex items-center rounded-[var(--r-sm)] border bg-[var(--panel)] px-3 h-11 font-mono text-[14px] w-40"
|
||||
style={{ borderColor: invalid ? "var(--danger)" : "var(--line-2)" }}
|
||||
>
|
||||
<span style={{ color: "var(--ink-3)" }}>#</span>
|
||||
<input
|
||||
value={draft.startsWith("#") ? draft.slice(1) : draft}
|
||||
onChange={(e) => setDraft("#" + e.target.value.replace(/[^0-9a-fA-F#]/g, ""))}
|
||||
className="flex-1 min-w-0 bg-transparent outline-none ml-1 uppercase"
|
||||
style={{ color: "var(--ink)" }}
|
||||
maxLength={7}
|
||||
spellCheck={false}
|
||||
aria-label="主题色 HEX 值"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{pair && (
|
||||
<div className="flex items-center gap-2 text-[12px]" style={{ color: "var(--ink-3)" }}>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span
|
||||
className="w-3.5 h-3.5 rounded-full border"
|
||||
style={{ background: pair.light, borderColor: "var(--line)" }}
|
||||
/>
|
||||
浅色 {pair.light}
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span
|
||||
className="w-3.5 h-3.5 rounded-full border"
|
||||
style={{ background: pair.dark, borderColor: "var(--line)" }}
|
||||
/>
|
||||
暗色 {pair.dark}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="max-w-xs">
|
||||
<div className="j13-bcard" aria-current="page">
|
||||
<span className="j13-bcard-ico" style={{ background: "var(--accent-soft)", color: "var(--accent)" }}>
|
||||
<Palette size={16} />
|
||||
{invalid && (
|
||||
<p className="mt-2.5 text-[12.5px]" style={{ color: "var(--danger)" }}>
|
||||
颜色格式无效,请输入 6 位 HEX 值(如 3B5BDB)
|
||||
</p>
|
||||
)}
|
||||
{pair?.adjusted && (
|
||||
<p className="mt-2.5 text-[12.5px]" style={{ color: "var(--gold)" }}>
|
||||
该颜色与白底对比度不足,浅色模式已自动校正为 {pair.light}(暗色模式同步派生{" "}
|
||||
{pair.dark})
|
||||
</p>
|
||||
)}
|
||||
{preset && preset.light === DEFAULT_ACCENT && (
|
||||
<p className="mt-2.5 text-[12.5px] meta">当前为内置默认色,保存时会清除自定义配置</p>
|
||||
)}
|
||||
</AdminSection>
|
||||
|
||||
<AdminSection title="实时预览">
|
||||
<div
|
||||
className="rounded-[var(--r-sm)] border p-4 space-y-4"
|
||||
style={{ borderColor: "var(--line)", background: "var(--bg)" }}
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2.5">
|
||||
<button type="button" className="btn btn-primary btn-sm">
|
||||
<Save size={14} /> 主按钮
|
||||
</button>
|
||||
<button type="button" className="btn btn-line btn-sm">
|
||||
次要按钮
|
||||
</button>
|
||||
<a
|
||||
className="text-[13px]"
|
||||
style={{ color: "var(--accent)" }}
|
||||
href="#preview"
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
文字链接示例
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="admin-seg">未选中</span>
|
||||
<span className="admin-seg" aria-selected="true">
|
||||
选中标签
|
||||
</span>
|
||||
<span className="j13-bcard-name text-[13px] flex-1">选中的板块卡片</span>
|
||||
<span
|
||||
className="inline-flex items-center rounded-full px-3.5 py-2 text-[13.5px]"
|
||||
style={{ background: "var(--nav-active-bg)", color: "var(--nav-active-ink)" }}
|
||||
>
|
||||
导航选中态
|
||||
</span>
|
||||
</div>
|
||||
<div className="max-w-xs">
|
||||
<div className="j13-bcard" aria-current="page">
|
||||
<span
|
||||
className="j13-bcard-ico"
|
||||
style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
|
||||
>
|
||||
<Palette size={16} />
|
||||
</span>
|
||||
<span className="j13-bcard-name text-[13px] flex-1">选中的板块卡片</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</AdminSection>
|
||||
</div>
|
||||
|
||||
{/* 操作栏 */}
|
||||
<div className="flex items-center justify-between gap-3 sticky bottom-5 panel !p-3.5 pl-5 z-10">
|
||||
<p className="text-[12.5px] meta hidden sm:block">
|
||||
{dirty ? "有未保存的改动,整页正处于实时预览状态" : "设置已是保存状态"}
|
||||
|
||||
Reference in New Issue
Block a user