公开页补齐自引用 Canonical 与按类型的结构化数据,图片补上说明文字。
后台设置和内容编辑改成可导航布局;部署用访客 origin 生成 Canonical,避免落到容器地址。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
160
frontend/components/admin/AdminSettingsLayout.tsx
Normal file
160
frontend/components/admin/AdminSettingsLayout.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user