"use client"; import { CheckCircle2, Info, Loader2, Search, ShieldAlert, X, XCircle } from "lucide-react"; import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ButtonHTMLAttributes, type InputHTMLAttributes, type ReactNode, type SelectHTMLAttributes, } from "react"; export function cn(...values: (string | false | null | undefined)[]) { return values.filter(Boolean).join(" "); } /* ------------------------------------------------------------------ 卡片 */ export function Card({ title, description, actions, children, className, bodyClassName, }: { title?: ReactNode; description?: ReactNode; actions?: ReactNode; children: ReactNode; className?: string; bodyClassName?: string; }) { return (
{(title || actions) && (
{title &&

{title}

} {description &&

{description}

}
{actions &&
{actions}
}
)}
{children}
); } /* -------------------------------------------------------------- 统计卡片 */ export function StatCard({ label, value, hint, hintTone = "muted", icon, }: { label: string; value: ReactNode; hint?: ReactNode; hintTone?: "muted" | "danger" | "success"; icon?: ReactNode; }) { const hintColor = hintTone === "danger" ? "text-red-500" : hintTone === "success" ? "text-emerald-600" : "text-slate-400"; return (
{icon &&
{icon}
}

{label}

{value}

{hint &&

{hint}

}
); } /* ------------------------------------------------------------------ 标签 */ export type Tone = "neutral" | "info" | "success" | "warning" | "danger"; const TONE_CLASS: Record = { neutral: "bg-slate-100 text-slate-600", info: "bg-indigo-50 text-indigo-600", success: "bg-emerald-50 text-emerald-600", warning: "bg-amber-50 text-amber-600", danger: "bg-red-50 text-red-600", }; export function Badge({ children, tone = "neutral", className, }: { children: ReactNode; tone?: Tone; className?: string; }) { return ( {children} ); } /** 业务状态 → 标签配色,避免各处散落 if-else。 */ export function statusTone(value?: string | null): Tone { switch (value) { case "已签约": case "sent": case "已完成": case "已排除": case "已确认": return "success"; case "已结束": case "已作废": case "失败": case "discarded": return "neutral"; case "待处理": case "处理中": case "未处理": case "draft": case "初审": return "info"; case "高": case "特急": case "紧急": case "冻结": return "danger"; case "中": return "warning"; default: return "neutral"; } } /* ------------------------------------------------------------------ 按钮 */ type ButtonProps = ButtonHTMLAttributes & { variant?: "primary" | "secondary" | "danger" | "ghost"; size?: "sm" | "md"; loading?: boolean; }; const BUTTON_VARIANT: Record, string> = { primary: "bg-indigo-600 text-white hover:bg-indigo-700 disabled:bg-indigo-300", secondary: "border border-slate-200 bg-white text-slate-700 hover:bg-slate-50 disabled:text-slate-300", danger: "border border-red-200 bg-white text-red-600 hover:bg-red-50 disabled:text-red-300", ghost: "text-slate-500 hover:bg-slate-100 disabled:text-slate-300", }; export function Button({ variant = "secondary", size = "md", loading, disabled, className, children, ...rest }: ButtonProps) { return ( ); } /* ------------------------------------------------------------ 表单控件 */ export function SearchInput({ className, ...rest }: InputHTMLAttributes) { return (
); } export function Select({ className, children, ...rest }: SelectHTMLAttributes) { return ( ); } export function TextInput({ className, ...rest }: InputHTMLAttributes) { return ( ); } /* ------------------------------------------------------------------ 表格 */ export function TableWrap({ children }: { children: ReactNode }) { return (
{children}
); } export function Th({ children, className }: { children?: ReactNode; className?: string }) { return ( {children} ); } export function Td({ children, className, title, }: { children?: ReactNode; className?: string; title?: string; }) { return ( {children} ); } /* ------------------------------------------------------------------ 分页 */ export function Pagination({ page, totalPages, total, onChange, }: { page: number; totalPages: number; total: number; onChange: (page: number) => void; }) { return (
共 {total} 条 · 第 {page} / {Math.max(totalPages, 1)} 页
); } /* ------------------------------------------------------ 空 / 加载 / 无权限 */ export function EmptyState({ text = "暂无数据" }: { text?: string }) { return (
{text}
); } export function LoadingBlock({ text = "加载中..." }: { text?: string }) { return (
{text}
); } /** 模块级无权限提示:不整页拦截,员工仍可使用其它功能区。 */ export function NoPermission({ module, hint }: { module: string; hint?: string }) { return (

无权限访问「{module}」

{hint ?? "当前账号角色不具备该模块的操作权限。如需开通请联系系统管理员。"}

); } export function ErrorState({ message, onRetry }: { message: string; onRetry?: () => void }) { return (
{message} {onRetry && ( )}
); } /* ------------------------------------------------------------------ 弹窗 */ export function Modal({ open, title, onClose, children, footer, }: { open: boolean; title: string; onClose: () => void; children: ReactNode; footer?: ReactNode; }) { useEffect(() => { if (!open) return; const onKey = (event: KeyboardEvent) => event.key === "Escape" && onClose(); window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [open, onClose]); if (!open) return null; return (

{title}

{children}
{footer &&
{footer}
}
); } /* ------------------------------------------------------------------ Toast */ interface ToastItem { id: number; tone: "success" | "error" | "info"; text: string; } const ToastContext = createContext<(text: string, tone?: ToastItem["tone"]) => void>(() => {}); export function useToast() { return useContext(ToastContext); } export function ToastProvider({ children }: { children: ReactNode }) { const [items, setItems] = useState([]); const push = useCallback((text: string, tone: ToastItem["tone"] = "success") => { const id = Date.now() + Math.random(); setItems((prev) => [...prev, { id, tone, text }]); setTimeout(() => setItems((prev) => prev.filter((item) => item.id !== id)), 3200); }, []); const value = useMemo(() => push, [push]); return ( {children}
{items.map((item) => (
{item.tone === "success" && } {item.tone === "error" && } {item.tone === "info" && } {item.text}
))}
); } /* ---------------------------------------------------------------- 工具函数 */ export function formatMoney(value?: string | number | null) { if (value === null || value === undefined || value === "") return "--"; const num = typeof value === "number" ? value : Number(value); if (Number.isNaN(num)) return "--"; return `¥${num.toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; } export function formatPercent(value?: number | null, digits = 1) { if (value === null || value === undefined) return "--"; return `${(value * 100).toFixed(digits)}%`; } export function formatTime(value?: string | null) { if (!value) return "--"; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return date.toLocaleString("zh-CN", { hour12: false }); } /** 判断是否已逾期(due_at 早于当前时间且未完成)。 */ export function isOverdue(dueAt?: string | null) { if (!dueAt) return false; const time = new Date(dueAt).getTime(); return !Number.isNaN(time) && time < Date.now(); }