后台设置和内容编辑改成可导航布局;部署用访客 origin 生成 Canonical,避免落到容器地址。 Co-authored-by: Cursor <cursoragent@cursor.com>
161 lines
4.3 KiB
TypeScript
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>
|
|
);
|
|
}
|