"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 (
);
}
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 (
);
}
/* ------------------------------------------------------------------ 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();
}