polish(admin): 抽出后台控件包并统一各页布局与交互
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Code2,
|
||||
Globe,
|
||||
@@ -10,42 +9,16 @@ import {
|
||||
Sparkles,
|
||||
UserPlus,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
apiUpdateSiteSettings,
|
||||
type PublicSettings,
|
||||
} from "@/lib/api";
|
||||
AdminPageHeader,
|
||||
AdminSection,
|
||||
AdminSettingRow,
|
||||
AdminSwitch,
|
||||
} from "@/components/admin";
|
||||
import { apiUpdateSiteSettings, type PublicSettings } from "@/lib/api";
|
||||
import { toast } from "@/lib/toast";
|
||||
|
||||
function Switch({
|
||||
checked,
|
||||
disabled,
|
||||
onChange,
|
||||
label,
|
||||
}: {
|
||||
checked: boolean;
|
||||
disabled?: boolean;
|
||||
onChange: () => void;
|
||||
label: string;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
aria-label={label}
|
||||
disabled={disabled}
|
||||
onClick={onChange}
|
||||
className="shrink-0 relative h-7 w-12 rounded-full transition-colors disabled:opacity-60"
|
||||
style={{ background: checked ? "var(--accent)" : "var(--line-2)" }}
|
||||
>
|
||||
<span
|
||||
className="absolute top-0.5 left-0.5 h-6 w-6 rounded-full bg-[var(--panel)] shadow transition-transform"
|
||||
style={{ transform: checked ? "translateX(1.25rem)" : "translateX(0)" }}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SettingsAdmin({ initial }: { initial: PublicSettings }) {
|
||||
const [siteName, setSiteName] = useState(initial.site_name);
|
||||
const [siteDescription, setSiteDescription] = useState(initial.site_description);
|
||||
@@ -204,229 +177,214 @@ export default function SettingsAdmin({ initial }: { initial: PublicSettings })
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl">
|
||||
<div className="mb-5">
|
||||
<h1
|
||||
className="text-[22px] sm:text-[26px] font-extrabold tracking-tight flex items-center gap-2.5"
|
||||
style={{ color: "var(--ink)" }}
|
||||
<AdminPageHeader
|
||||
icon={Globe}
|
||||
title="站点设置"
|
||||
description="配置站点名称、简介与注册/评论开关;主题色请到「外观」页调整"
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<AdminSection
|
||||
title="基本信息"
|
||||
description="用于页眉品牌、页脚版权与 SEO 元信息"
|
||||
>
|
||||
<span
|
||||
className="w-9 h-9 rounded-xl flex items-center justify-center"
|
||||
style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
|
||||
>
|
||||
<Globe size={18} />
|
||||
</span>
|
||||
站点设置
|
||||
</h1>
|
||||
<p className="meta mt-1.5 text-[12.5px]">
|
||||
配置站点名称、简介与注册/评论开关;主题色请到「外观」页调整
|
||||
</p>
|
||||
</div>
|
||||
<label className="field-label" htmlFor="site-name">
|
||||
站点名称
|
||||
</label>
|
||||
<input
|
||||
id="site-name"
|
||||
className="field mb-4"
|
||||
maxLength={32}
|
||||
value={siteName}
|
||||
onChange={(e) => setSiteName(e.target.value)}
|
||||
placeholder="姜十三论坛"
|
||||
/>
|
||||
|
||||
<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">用于页眉品牌、页脚版权与 SEO 元信息</p>
|
||||
<label className="field-label" htmlFor="site-desc">
|
||||
站点简介
|
||||
</label>
|
||||
<textarea
|
||||
id="site-desc"
|
||||
className="field min-h-[96px] mb-4"
|
||||
maxLength={160}
|
||||
value={siteDescription}
|
||||
onChange={(e) => setSiteDescription(e.target.value)}
|
||||
placeholder="一句话介绍社区定位…"
|
||||
/>
|
||||
|
||||
<label className="field-label" htmlFor="site-name">
|
||||
站点名称
|
||||
</label>
|
||||
<input
|
||||
id="site-name"
|
||||
className="field mb-4"
|
||||
maxLength={32}
|
||||
value={siteName}
|
||||
onChange={(e) => setSiteName(e.target.value)}
|
||||
placeholder="姜十三论坛"
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary btn-sm"
|
||||
disabled={!dirty || saving}
|
||||
onClick={() => void saveIdentity()}
|
||||
>
|
||||
{saving ? <Loader2 size={14} className="animate-spin" /> : <Save size={14} />}
|
||||
保存基本信息
|
||||
</button>
|
||||
</div>
|
||||
</AdminSection>
|
||||
|
||||
<AdminSettingRow
|
||||
title={
|
||||
<>
|
||||
<UserPlus size={15} /> 开放注册
|
||||
</>
|
||||
}
|
||||
description="关闭后,游客将无法提交注册;已有账号仍可登录。适用于内测或维护期。"
|
||||
control={
|
||||
<AdminSwitch
|
||||
checked={allowRegister}
|
||||
disabled={togglingReg}
|
||||
onChange={() => void toggleRegister()}
|
||||
label="开放注册"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<label className="field-label" htmlFor="site-desc">
|
||||
站点简介
|
||||
</label>
|
||||
<textarea
|
||||
id="site-desc"
|
||||
className="field min-h-[96px] mb-4"
|
||||
maxLength={160}
|
||||
value={siteDescription}
|
||||
onChange={(e) => setSiteDescription(e.target.value)}
|
||||
placeholder="一句话介绍社区定位…"
|
||||
<AdminSettingRow
|
||||
title={
|
||||
<>
|
||||
<MessageSquare size={15} /> 开放评论
|
||||
</>
|
||||
}
|
||||
description="关闭后全站禁止发表新评论与回复,已有评论仍可浏览。适用于 ICP/IPC 备案审核等合规需要;前台仅提示「暂停评论」,不会展示备案事由。"
|
||||
control={
|
||||
<AdminSwitch
|
||||
checked={allowComments}
|
||||
disabled={togglingComments}
|
||||
onChange={() => void toggleComments()}
|
||||
label="开放评论"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary btn-sm"
|
||||
disabled={!dirty || saving}
|
||||
onClick={() => void saveIdentity()}
|
||||
>
|
||||
{saving ? <Loader2 size={14} className="animate-spin" /> : <Save size={14} />}
|
||||
保存基本信息
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="j13-module p-5 sm:p-6 mb-5 flex items-start justify-between gap-4 flex-wrap">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 className="text-[15px] font-semibold flex items-center gap-1.5" style={{ color: "var(--ink)" }}>
|
||||
<UserPlus size={15} /> 开放注册
|
||||
</h2>
|
||||
<p className="meta text-[12.5px] mt-1 leading-relaxed">
|
||||
关闭后,游客将无法提交注册;已有账号仍可登录。适用于内测或维护期。
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={allowRegister}
|
||||
disabled={togglingReg}
|
||||
onChange={() => void toggleRegister()}
|
||||
label="开放注册"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="j13-module p-5 sm:p-6 mb-5 flex items-start justify-between gap-4 flex-wrap">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 className="text-[15px] font-semibold flex items-center gap-1.5" style={{ color: "var(--ink)" }}>
|
||||
<MessageSquare size={15} /> 开放评论
|
||||
</h2>
|
||||
<p className="meta text-[12.5px] mt-1 leading-relaxed">
|
||||
关闭后全站禁止发表新评论与回复,已有评论仍可浏览。适用于 ICP/IPC
|
||||
备案审核等合规需要;前台仅提示「暂停评论」,不会展示备案事由。
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={allowComments}
|
||||
disabled={togglingComments}
|
||||
onChange={() => void toggleComments()}
|
||||
label="开放评论"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="j13-module p-5 sm:p-6 mb-5">
|
||||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 className="text-[15px] font-semibold flex items-center gap-1.5" style={{ color: "var(--ink)" }}>
|
||||
<AdminSettingRow
|
||||
title={
|
||||
<>
|
||||
<Code2 size={15} /> 代码块自动折叠
|
||||
</h2>
|
||||
<p className="meta text-[12.5px] mt-1 leading-relaxed">
|
||||
开启后,帖子与评论中超过阈值行数的 Markdown 代码块默认折叠,读者可点「展开全部」。关闭则始终完整展示。
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={codeAutoFold}
|
||||
disabled={togglingFold}
|
||||
onChange={() => void toggleCodeFold()}
|
||||
label="代码块自动折叠"
|
||||
/>
|
||||
</div>
|
||||
{codeAutoFold ? (
|
||||
<div className="mt-4 pt-4 border-t border-[var(--line)]">
|
||||
<label className="field-label" htmlFor="fold-lines">
|
||||
折叠行数阈值
|
||||
</label>
|
||||
<p className="meta text-[12.5px] mb-2.5 leading-relaxed">
|
||||
代码块行数超过该值时折叠;折叠后约露出同样行数。设为 0 表示无预览。范围 0–100,默认 5。
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-2.5">
|
||||
<input
|
||||
id="fold-lines"
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
className="field w-24"
|
||||
value={foldLines}
|
||||
onChange={(e) => setFoldLines(Number(e.target.value))}
|
||||
aria-label="折叠行数阈值"
|
||||
/>
|
||||
<span className="meta text-[12.5px]">行</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-line btn-sm"
|
||||
disabled={!foldLinesDirty || savingFoldLines}
|
||||
onClick={() => void saveFoldLines()}
|
||||
>
|
||||
{savingFoldLines ? (
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
) : (
|
||||
<Save size={14} />
|
||||
)}
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
</>
|
||||
}
|
||||
description="开启后,帖子与评论中超过阈值行数的 Markdown 代码块默认折叠,读者可点「展开全部」。关闭则始终完整展示。"
|
||||
control={
|
||||
<AdminSwitch
|
||||
checked={codeAutoFold}
|
||||
disabled={togglingFold}
|
||||
onChange={() => void toggleCodeFold()}
|
||||
label="代码块自动折叠"
|
||||
/>
|
||||
}
|
||||
>
|
||||
{codeAutoFold ? (
|
||||
<>
|
||||
<label className="field-label" htmlFor="fold-lines">
|
||||
折叠行数阈值
|
||||
</label>
|
||||
<p className="meta text-[12.5px] mb-2.5 leading-relaxed">
|
||||
代码块行数超过该值时折叠;折叠后约露出同样行数。设为 0 表示无预览。范围 0–100,默认 5。
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-2.5">
|
||||
<input
|
||||
id="fold-lines"
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
className="field w-24"
|
||||
value={foldLines}
|
||||
onChange={(e) => setFoldLines(Number(e.target.value))}
|
||||
aria-label="折叠行数阈值"
|
||||
/>
|
||||
<span className="meta text-[12.5px]">行</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-line btn-sm"
|
||||
disabled={!foldLinesDirty || savingFoldLines}
|
||||
onClick={() => void saveFoldLines()}
|
||||
>
|
||||
{savingFoldLines ? (
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
) : (
|
||||
<Save size={14} />
|
||||
)}
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</AdminSettingRow>
|
||||
|
||||
<section className="j13-module p-5 sm:p-6 mb-5">
|
||||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 className="text-[15px] font-semibold flex items-center gap-1.5" style={{ color: "var(--ink)" }}>
|
||||
<AdminSettingRow
|
||||
title={
|
||||
<>
|
||||
<Sparkles size={15} /> 界面动画
|
||||
</h2>
|
||||
<p className="meta text-[12.5px] mt-1 leading-relaxed">
|
||||
总开关关闭后全站即时切换(等同减少动态效果)。系统若已开启「减少动态效果」,始终优先生效。开启总开关后可细调子项。
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={uiAnimations}
|
||||
disabled={togglingMotion}
|
||||
onChange={() => void toggleMotionField("ui_animations", !uiAnimations)}
|
||||
label="界面动画总开关"
|
||||
/>
|
||||
</div>
|
||||
{uiAnimations ? (
|
||||
<div className="mt-4 pt-4 border-t border-[var(--line)] space-y-4">
|
||||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-[13.5px] font-medium" style={{ color: "var(--ink)" }}>
|
||||
代码折叠动画
|
||||
</p>
|
||||
<p className="meta text-[12.5px] mt-0.5 leading-relaxed">
|
||||
代码块展开/收起的高度过渡与跟滚。
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
description="总开关关闭后全站即时切换(等同减少动态效果)。系统若已开启「减少动态效果」,始终优先生效。开启总开关后可细调子项。"
|
||||
control={
|
||||
<AdminSwitch
|
||||
checked={uiAnimations}
|
||||
disabled={togglingMotion}
|
||||
onChange={() => void toggleMotionField("ui_animations", !uiAnimations)}
|
||||
label="界面动画总开关"
|
||||
/>
|
||||
}
|
||||
>
|
||||
{uiAnimations ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-[13.5px] font-medium" style={{ color: "var(--ink)" }}>
|
||||
代码折叠动画
|
||||
</p>
|
||||
<p className="meta text-[12.5px] mt-0.5 leading-relaxed">
|
||||
代码块展开/收起的高度过渡与跟滚。
|
||||
</p>
|
||||
</div>
|
||||
<AdminSwitch
|
||||
checked={animCodeFold}
|
||||
disabled={togglingMotion}
|
||||
onChange={() => void toggleMotionField("anim_code_fold", !animCodeFold)}
|
||||
label="代码折叠动画"
|
||||
/>
|
||||
</div>
|
||||
<Switch
|
||||
checked={animCodeFold}
|
||||
disabled={togglingMotion}
|
||||
onChange={() => void toggleMotionField("anim_code_fold", !animCodeFold)}
|
||||
label="代码折叠动画"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-[13.5px] font-medium" style={{ color: "var(--ink)" }}>
|
||||
平滑滚动
|
||||
</p>
|
||||
<p className="meta text-[12.5px] mt-0.5 leading-relaxed">
|
||||
目录跳转、回顶、新内容提示等 scroll 行为。
|
||||
</p>
|
||||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-[13.5px] font-medium" style={{ color: "var(--ink)" }}>
|
||||
平滑滚动
|
||||
</p>
|
||||
<p className="meta text-[12.5px] mt-0.5 leading-relaxed">
|
||||
目录跳转、回顶、新内容提示等 scroll 行为。
|
||||
</p>
|
||||
</div>
|
||||
<AdminSwitch
|
||||
checked={animSmoothScroll}
|
||||
disabled={togglingMotion}
|
||||
onChange={() =>
|
||||
void toggleMotionField("anim_smooth_scroll", !animSmoothScroll)
|
||||
}
|
||||
label="平滑滚动"
|
||||
/>
|
||||
</div>
|
||||
<Switch
|
||||
checked={animSmoothScroll}
|
||||
disabled={togglingMotion}
|
||||
onChange={() =>
|
||||
void toggleMotionField("anim_smooth_scroll", !animSmoothScroll)
|
||||
}
|
||||
label="平滑滚动"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-[13.5px] font-medium" style={{ color: "var(--ink)" }}>
|
||||
装饰入场
|
||||
</p>
|
||||
<p className="meta text-[12.5px] mt-0.5 leading-relaxed">
|
||||
列表入场、Toast、点赞弹出、在线脉冲等装饰性动画。
|
||||
</p>
|
||||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-[13.5px] font-medium" style={{ color: "var(--ink)" }}>
|
||||
装饰入场
|
||||
</p>
|
||||
<p className="meta text-[12.5px] mt-0.5 leading-relaxed">
|
||||
列表入场、Toast、点赞弹出、在线脉冲等装饰性动画。
|
||||
</p>
|
||||
</div>
|
||||
<AdminSwitch
|
||||
checked={animChrome}
|
||||
disabled={togglingMotion}
|
||||
onChange={() => void toggleMotionField("anim_chrome", !animChrome)}
|
||||
label="装饰入场"
|
||||
/>
|
||||
</div>
|
||||
<Switch
|
||||
checked={animChrome}
|
||||
disabled={togglingMotion}
|
||||
onChange={() => void toggleMotionField("anim_chrome", !animChrome)}
|
||||
label="装饰入场"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
) : null}
|
||||
</AdminSettingRow>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user