import { App, Button, Dropdown, Table } from "antd";
import type { ButtonProps, MenuProps, TableProps } from "antd";
import { saveAs } from "file-saver";
import { CheckSquare2, ChevronDown, Download, SearchX, X } from "lucide-react";
import { useState } from "react";
import type { ReactNode } from "react";
import { ListToolbar } from "@/components/layout/workspace-page";
import { cn } from "@/lib/utils";
export const configuredSecretText = "已配置 · 留空不改";
export type AdminStatusTone = "neutral" | "success" | "warning" | "error" | "info";
export function AdminStatusBadge({ label, tone = "neutral", title }: { label: string; tone?: AdminStatusTone; title?: string }) {
return {label};
}
export function AdminStatTile({ label, value, detail, trend }: { label: string; value: string | number; detail?: string; trend?: { value: string; tone?: AdminStatusTone } }) {
return (
{label}
{value}
{trend || detail ? (
{trend ?
: null}
{trend && detail ?
· : null}
{detail ?
{detail} : null}
) : null}
);
}
export function AdminDataTable({
toolbar,
toolbarActive,
toolbarFilters,
toolbarActiveFilters,
onReset,
trailing,
batchActions,
footer,
table,
empty,
skeletonColumns = 6,
skeletonRows = 8,
className,
}: {
toolbar?: ReactNode;
toolbarActive?: boolean;
toolbarFilters?: ReactNode;
toolbarActiveFilters?: ReactNode;
onReset?: () => void;
trailing?: ReactNode;
batchActions?: ReactNode;
footer?: ReactNode;
table: TableProps;
empty?: ReactNode;
skeletonColumns?: number;
skeletonRows?: number;
className?: string;
}) {
const dataSource = table.dataSource as readonly RecordType[] | undefined;
const showSkeleton = Boolean(table.loading) && !dataSource?.length;
return (
{toolbar ?
{toolbar} : null}
{batchActions}
{footer ?
{footer}
: null}
);
}
function isStatusConfig(value: ReactNode | { label: string; color?: string }): value is { label: string; color?: string } {
if (!value || typeof value !== "object") return false;
return typeof (value as { label?: unknown }).label === "string";
}
export function AdminExportButton({
exportFile,
fileName,
label = "导出",
successMessage,
errorMessage = "导出失败",
size,
...buttonProps
}: Omit & {
exportFile: () => Blob | Promise;
fileName: string | (() => string);
label?: string;
successMessage?: string;
errorMessage?: string;
}) {
const { message } = App.useApp();
const [exporting, setExporting] = useState(false);
const runExport = async () => {
setExporting(true);
try {
const blob = await exportFile();
saveAs(blob, typeof fileName === "function" ? fileName() : fileName);
if (successMessage) message.success(successMessage);
} catch (error) {
message.error(error instanceof Error ? error.message : errorMessage);
} finally {
setExporting(false);
}
};
return } loading={exporting} onClick={() => void runExport()}>{label};
}
export function AdminTableEmpty({
filtered = false,
title,
description,
action,
}: {
filtered?: boolean;
title?: string;
description?: string;
action?: ReactNode;
}) {
return (
{title || (filtered ? "没有符合筛选条件的数据" : "暂无数据")}
{description ?
{description}
: null}
{action ?
{action}
: null}
);
}
export function AdminFilterChip({ label, onRemove }: { label: ReactNode; onRemove: () => void }) {
return (
);
}
export function AdminTableSkeleton({ rows = 8, columns = 6 }: { rows?: number; columns?: number }) {
return (
{Array.from({ length: columns }).map((_, index) => )}
{Array.from({ length: Math.max(8, rows) }).map((_, rowIndex) => (
{Array.from({ length: columns }).map((_, columnIndex) => (
))}
))}
);
}
export function AdminBatchBar({ count, onClear, children }: { count: number; onClear: () => void; children: ReactNode }) {
if (count <= 0) return null;
return (
已选择 {count} 项
{children}} onClick={onClear}>取消选择
);
}
export type AdminRowAction = {
key: string;
label: ReactNode;
icon?: ReactNode;
danger?: boolean;
disabled?: boolean;
onClick: () => void | Promise;
confirm?: {
title: string;
description: string;
okText: string;
};
};
export function AdminRowActions({
primary,
actions,
visibleActionCount,
}: {
primary?: { label: ReactNode; icon?: ReactNode; onClick: () => void | Promise; disabled?: boolean };
actions: AdminRowAction[];
visibleActionCount?: number;
}) {
const { modal } = App.useApp();
// 少量操作直接露出,只有真正过多时才收进菜单,避免“更多”成为默认操作列。
const resolvedVisibleActionCount = visibleActionCount ?? (actions.length <= 3 ? actions.length : 2);
const visibleActions = actions.slice(0, Math.max(0, resolvedVisibleActionCount));
const menuActions = actions.slice(Math.max(0, resolvedVisibleActionCount));
const items: MenuProps["items"] = menuActions.map((action) => ({
key: action.key,
label: action.label,
icon: action.icon,
danger: action.danger,
disabled: action.disabled,
}));
const runAction = (action: AdminRowAction) => {
if (!action.confirm) {
void action.onClick();
return;
}
modal.confirm({
title: action.confirm.title,
content: action.confirm.description,
okText: action.confirm.okText,
cancelText: "取消",
okButtonProps: { danger: action.danger },
onOk: action.onClick,
});
};
const renderActionButton = (action: AdminRowAction) => (
);
return (
{primary ? (
) : null}
{visibleActions.map(renderActionButton)}
{menuActions.length ? (
{
const action = actions.find((item) => item.key === key);
if (action) runAction(action);
},
}}
>
) : null}
);
}
export function SettingsSectionCard({
icon,
title,
description,
status,
children,
footer,
className,
contentClassName,
layout = "split",
}: {
icon?: ReactNode;
title: string;
description?: string;
status?: { label: string; color?: string } | ReactNode;
children: ReactNode;
footer?: ReactNode;
className?: string;
contentClassName?: string;
layout?: "split" | "stacked";
}) {
const isStacked = layout === "stacked";
return (
{icon ?
{icon} : null}
{title}
{description ?
{description}
: null}
{status ?
{isStatusConfig(status) ?
: status}
: null}
{children}
{footer ?
{footer}
: null}
);
}