Files
jiang13-bbs/frontend/components/admin/AdminSettingsLayout.tsx
freefire 40bbad2c82 公开页补齐自引用 Canonical 与按类型的结构化数据,图片补上说明文字。
后台设置和内容编辑改成可导航布局;部署用访客 origin 生成 Canonical,避免落到容器地址。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-20 00:11:24 +08:00

161 lines
4.3 KiB
TypeScript

import { Loader2 } from "lucide-react";
import type { ReactNode } from "react";
/** 站点设置工作区:限宽、主内容区内左对齐,不写入后台公共主容器 */
export function AdminSettingsPage({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return <div className={`admin-settings-page${className ? ` ${className}` : ""}`}>{children}</div>;
}
/** 分类导航 + 主表单列 */
export function AdminSettingsWork({
nav,
children,
}: {
nav: ReactNode;
children: ReactNode;
}) {
return (
<div className="admin-settings-work">
<div className="admin-settings-nav-col">{nav}</div>
<div className="admin-settings-main">{children}</div>
</div>
);
}
/** 一组相关设置:标题、说明、字段、底部操作,减少逐项套卡片 */
export function AdminSettingsGroup({
title,
description,
children,
footer,
id,
className,
}: {
title: string;
description?: ReactNode;
children: ReactNode;
footer?: ReactNode;
id?: string;
className?: string;
}) {
return (
<section className={`admin-settings-group${className ? ` ${className}` : ""}`} id={id}>
<header className="admin-settings-group-head">
<h2 className="admin-settings-group-title">{title}</h2>
{description != null && description !== "" ? (
<p className="admin-settings-group-desc">{description}</p>
) : null}
</header>
<div className="admin-settings-group-body">{children}</div>
{footer != null ? <div className="admin-settings-group-footer">{footer}</div> : null}
</section>
);
}
/** 标签列 + 控件列;窄表单自动改为上下堆叠 */
export function AdminSettingsRow({
label,
htmlFor,
hint,
hintBelow,
error,
errorId,
children,
wide,
className,
}: {
label: ReactNode;
htmlFor?: string;
/** 短说明,紧邻标签 */
hint?: ReactNode;
/** 长说明,放在控件下方 */
hintBelow?: ReactNode;
error?: ReactNode;
errorId?: string;
children: ReactNode;
/** 简介等较长控件 */
wide?: boolean;
className?: string;
}) {
const Tag = htmlFor ? "label" : "div";
return (
<div className={`admin-settings-row${wide ? " is-wide" : ""}${className ? ` ${className}` : ""}`}>
<div className="admin-settings-row-label">
<Tag className="admin-settings-row-name" {...(htmlFor ? { htmlFor } : {})}>
{label}
</Tag>
{hint != null && hint !== "" ? <p className="admin-settings-row-hint">{hint}</p> : null}
</div>
<div className="admin-settings-row-control">
{children}
{hintBelow != null && hintBelow !== "" ? (
<p className="admin-settings-row-hint is-below">{hintBelow}</p>
) : null}
{error != null && error !== "" ? (
<p className="admin-settings-row-error" id={errorId} role="alert">
{error}
</p>
) : null}
</div>
</div>
);
}
/** 保存单元底部操作:按钮与状态贴齐控件列左边缘 */
export function AdminSettingsActions({
dirty,
saving,
saveLabel,
onSave,
onDiscard,
saveDisabled,
error,
children,
}: {
dirty: boolean;
saving?: boolean;
saveLabel: string;
onSave: () => void;
onDiscard?: () => void;
saveDisabled?: boolean;
error?: string;
children?: ReactNode;
}) {
const status = saving ? "正在保存…" : error ? error : dirty ? "有未保存的修改" : "已保存";
return (
<div className="admin-settings-actions" aria-busy={saving || undefined}>
<div className="admin-settings-actions-inner">
<div className="admin-settings-actions-btns">
{children}
{onDiscard ? (
<button type="button" className="btn btn-line btn-sm" disabled={!dirty || saving} onClick={onDiscard}>
撤销修改
</button>
) : null}
<button
type="button"
className="btn btn-primary btn-sm"
disabled={saveDisabled ?? (!dirty || saving)}
onClick={onSave}
>
{saving ? <Loader2 size={14} className="animate-spin" aria-hidden /> : null}
{saving ? "保存中…" : saveLabel}
</button>
</div>
<p
className={`admin-settings-actions-status${error ? " is-error" : ""}`}
aria-live="polite"
>
{status}
</p>
</div>
</div>
);
}