import { createContext, useCallback, useContext, useMemo, useState } from 'react' import type { ReactNode } from 'react' export function Button({ children, onClick, variant = 'primary', size = 'md', disabled, type = 'button', title, }: { children: ReactNode onClick?: () => void variant?: 'primary' | 'secondary' | 'danger' | 'ghost' size?: 'sm' | 'md' disabled?: boolean type?: 'button' | 'submit' title?: string }) { const base = 'inline-flex items-center justify-center gap-1 rounded-md font-medium transition disabled:cursor-not-allowed disabled:opacity-50' const sizes = size === 'sm' ? 'px-2 py-1 text-xs' : 'px-3 py-1.5 text-sm' const variants: Record = { primary: 'bg-slate-900 text-white hover:bg-slate-700 dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-white', secondary: 'border border-slate-300 bg-white text-slate-700 hover:bg-slate-100 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700', danger: 'bg-red-600 text-white hover:bg-red-500', ghost: 'text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800', } return ( ) } export function Card({ title, action, children, className, }: { title?: ReactNode action?: ReactNode children: ReactNode className?: string }) { return (
{(title || action) && (

{title}

{action}
)} {children}
) } type Tone = 'neutral' | 'success' | 'warning' | 'danger' | 'info' export function Badge({ tone = 'neutral', children }: { tone?: Tone; children: ReactNode }) { const tones: Record = { neutral: 'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300', success: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900 dark:text-emerald-300', warning: 'bg-amber-100 text-amber-700 dark:bg-amber-900 dark:text-amber-300', danger: 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300', info: 'bg-sky-100 text-sky-700 dark:bg-sky-900 dark:text-sky-300', } return ( {children} ) } export function statusTone(state: string): Tone { switch (state) { case 'success': case 'succeeded': case 'done': case 'completed': case 'active': return 'success' case 'failed': case 'failure': case 'cancelled': case 'canceled': case 'removed': return 'danger' case 'running': case 'processing': case 'delivering': case 'scheduled': case 'cancel_requested': return 'info' case 'pending': case 'queued': return 'warning' default: return 'neutral' } } export function Field({ label, hint, children, }: { label: string hint?: string children: ReactNode }) { return ( ) } const inputClass = 'w-full rounded-md border border-slate-300 bg-white px-2.5 py-1.5 text-sm text-slate-900 outline-none focus:border-slate-500 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-100' export function TextInput({ value, onChange, placeholder, type = 'text', disabled, }: { value: string onChange: (value: string) => void placeholder?: string type?: string disabled?: boolean }) { return ( onChange(event.target.value)} /> ) } export function Select({ value, onChange, options, }: { value: string onChange: (value: string) => void options: { value: string; label: string }[] }) { return ( ) } export function Checkbox({ checked, onChange, label, }: { checked: boolean onChange: (value: boolean) => void label: string }) { return ( ) } export interface Column { key: string title: ReactNode render: (row: T) => ReactNode className?: string } export function DataTable({ columns, rows, rowKey, onRowClick, empty = '暂无数据', }: { columns: Column[] rows: T[] rowKey: (row: T) => string onRowClick?: (row: T) => void empty?: string }) { return (
{columns.map((column) => ( ))} {rows.length === 0 && ( )} {rows.map((row) => ( onRowClick(row) : undefined} className={`border-b border-slate-100 last:border-0 dark:border-slate-800 ${ onRowClick ? 'cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800/60' : '' }`} > {columns.map((column) => ( ))} ))}
{column.title}
{empty}
{column.render(row)}
) } export function Pagination({ total, limit, offset, onChange, }: { total: number limit: number offset: number onChange: (offset: number) => void }) { const page = Math.floor(offset / limit) + 1 const pages = Math.max(1, Math.ceil(total / limit)) return (
共 {total} 条 · 第 {page}/{pages} 页
) } export function Modal({ open, title, onClose, children, footer, }: { open: boolean title: ReactNode onClose: () => void children: ReactNode footer?: ReactNode }) { if (!open) { return null } return (

{title}

{children}
{footer &&
{footer}
}
) } export function ConfirmDialog({ open, title, description, confirmLabel = '确认执行', requireText, busy, onConfirm, onClose, }: { open: boolean title: string description: ReactNode confirmLabel?: string requireText?: string busy?: boolean onConfirm: () => void onClose: () => void }) { const [text, setText] = useState('') const canConfirm = !requireText || text === requireText return ( } >
{description}
{requireText && ( )}
) } export function ErrorBanner({ message }: { message: string | null }) { if (!message) { return null } return (
{message}
) } export function Loading({ label = '加载中…' }: { label?: string }) { return
{label}
} export function EmptyState({ label }: { label: string }) { return
{label}
} interface ToastItem { id: number message: string tone: 'success' | 'danger' } const ToastContext = createContext<(message: string, tone?: 'success' | 'danger') => void>(() => {}) export function ToastProvider({ children }: { children: ReactNode }) { const [items, setItems] = useState([]) const push = useCallback((message: string, tone: 'success' | 'danger' = 'success') => { const id = Date.now() + Math.random() setItems((current) => [...current, { id, message, tone }]) window.setTimeout(() => { setItems((current) => current.filter((item) => item.id !== id)) }, 4000) }, []) const value = useMemo(() => push, [push]) return ( {children}
{items.map((item) => (
{item.message}
))}
) } export function useToast() { return useContext(ToastContext) }