Files
TallyNote/web-next/src/pages/common.tsx
T
2026-09-04 16:51:43 +08:00

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>;
}