polish(admin): 抽出后台控件包并统一各页布局与交互

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-16 19:03:21 +08:00
parent cfc6c1403b
commit f73d2a89e8
23 changed files with 1720 additions and 1412 deletions

View File

@@ -0,0 +1,59 @@
"use client";
import type { ReactNode } from "react";
export type AdminSegmentOption<T extends string = string> = {
key: T;
label: string;
icon?: ReactNode;
count?: number | string;
disabled?: boolean;
};
/** 分段筛选:可带 count / icon;选中态走 --nav-active-* */
export default function AdminSegmented<T extends string = string>({
options,
value,
onChange,
ariaLabel,
size = "md",
role = "tablist",
}: {
options: AdminSegmentOption<T>[];
value: T;
onChange: (key: T) => void;
ariaLabel: string;
size?: "md" | "sm";
role?: "tablist" | "group";
}) {
const useTabs = role === "tablist";
return (
<div
className="flex items-center gap-1.5 flex-wrap"
role={role}
aria-label={ariaLabel}
>
{options.map((opt) => {
const active = value === opt.key;
return (
<button
key={opt.key}
type="button"
role={useTabs ? "tab" : undefined}
aria-selected={useTabs ? active : undefined}
aria-pressed={!useTabs ? active : undefined}
disabled={opt.disabled}
onClick={() => {
if (!active) onChange(opt.key);
}}
className={`admin-seg ${size === "sm" ? "admin-seg-sm" : ""}`.trim()}
>
{opt.icon}
{opt.label}
{opt.count != null && <span className="admin-seg-count">{opt.count}</span>}
</button>
);
})}
</div>
);
}