54 lines
2.6 KiB
TypeScript
54 lines
2.6 KiB
TypeScript
import { BeamLoading, BeamBar } from "../components/BeamLoading";
|
|
import type { ReactNode } from "react";
|
|
import { AlertCircle, Loader2 } from "lucide-react";
|
|
import { Alert, Button, Card, Loading, Space, Tag } from "tdesign-react";
|
|
|
|
export function Page({ eyebrow, title, subtitle, actions, children, className = "" }: {
|
|
eyebrow?: string;
|
|
title: string;
|
|
subtitle?: string;
|
|
actions?: ReactNode;
|
|
children: ReactNode;
|
|
className?: string;
|
|
}) {
|
|
return <section className={`tn-page ${className}`}>
|
|
<div className="tn-page-head">
|
|
<div>{eyebrow && <div className="tn-eyebrow">{eyebrow}</div>}<h1 className="tn-page-title">{title}</h1>{subtitle && <p className="tn-page-subtitle">{subtitle}</p>}</div>
|
|
{actions && <div className="tn-page-actions">{actions}</div>}
|
|
</div>
|
|
{children}
|
|
</section>;
|
|
}
|
|
|
|
export function AsyncState({ loading, error, empty, onRetry, children }: {
|
|
loading: boolean;
|
|
error?: string;
|
|
empty?: ReactNode;
|
|
onRetry?: () => void;
|
|
children: ReactNode;
|
|
}) {
|
|
if (loading && empty) return <div className="tn-empty" role="status" aria-live="polite"><BeamLoading text="数据加载中…" /></div>;
|
|
if (error && empty) return <div className="tn-empty" role="alert"><AlertCircle size={28} /><p>{error}</p>{onRetry && <Button variant="outline" onClick={onRetry}>重试</Button>}</div>;
|
|
return <>
|
|
{loading && <div className="tn-inline-loading" role="status"><BeamBar className="tn-beam-bar-sm" /> 正在同步…</div>}
|
|
{error && <ErrorBanner message={error} onRetry={onRetry} />}
|
|
{empty || children}
|
|
</>;
|
|
}
|
|
|
|
export function ErrorBanner({ message, onRetry }: { message: string; onRetry?: () => void }) {
|
|
return <div role="alert"><Alert theme="error" className="tn-alert" icon={<AlertCircle size={16} />} message={<Space size="small"><span>{message}</span>{onRetry && <Button variant="text" onClick={onRetry}>重试</Button>}</Space>} /></div>;
|
|
}
|
|
|
|
export function BusyIcon() { return <Loader2 size={16} className="tn-spin" aria-hidden="true" />; }
|
|
|
|
export function StatusTag({ status }: { status: "unreimbursed" | "reimbursed" | "active" | "disabled" }) {
|
|
const labels = { unreimbursed: "未报销", reimbursed: "已报销", active: "有效", disabled: "已停用" } as const;
|
|
const themes = { unreimbursed: "warning", reimbursed: "success", active: "success", disabled: "default" } as const;
|
|
return <Tag theme={themes[status]} variant="light-outline">{labels[status]}</Tag>;
|
|
}
|
|
|
|
export function Surface({ children, className = "" }: { children: ReactNode; className?: string }) {
|
|
return <Card bordered className={`tn-flat-section ${className}`}>{children}</Card>;
|
|
}
|