93 lines
7.2 KiB
TypeScript
93 lines
7.2 KiB
TypeScript
'use client'
|
|
import { InputHTMLAttributes, SelectHTMLAttributes, ReactNode } from 'react'
|
|
|
|
export { default as Modal } from './Modal'
|
|
export { default as Button } from './Button'
|
|
|
|
interface InputProps extends InputHTMLAttributes<HTMLInputElement> { label?: string; error?: string }
|
|
export function Input({ label, error, className = '', ...props }: InputProps) {
|
|
return (<div className="space-y-1">{label && <label className="block text-sm font-medium text-slate-700 dark:text-slate-300">{label}</label>}<input className={`w-full px-3 py-2 rounded-lg border bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 border-slate-300 dark:border-slate-600 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 transition-colors text-sm ${className}`} {...props} />{error && <p className="text-sm text-red-500">{error}</p>}</div>)
|
|
}
|
|
|
|
interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> { label?: string; options: Array<{ value: string; label: string }> }
|
|
export function Select({ label, options, className = '', ...props }: SelectProps) {
|
|
return (<div className="space-y-1">{label && <label className="block text-sm font-medium text-slate-700 dark:text-slate-300">{label}</label>}<select className={`w-full px-3 py-2 rounded-lg border bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 border-slate-300 dark:border-slate-600 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 transition-colors text-sm ${className}`} {...props}>{options.map((o) => (<option key={o.value} value={o.value}>{o.label}</option>))}</select></div>)
|
|
}
|
|
|
|
interface TableHeadersProps { headers: string[]; children: ReactNode }
|
|
export function Table({ headers, children }: TableHeadersProps) {
|
|
return (<div className="overflow-x-auto rounded-xl border border-slate-200 dark:border-slate-700"><table className="w-full text-sm"><thead className="bg-slate-50 dark:bg-slate-800"><tr>{headers.map((h) => (<th key={h} className="px-4 py-3 text-left font-medium text-slate-600 dark:text-slate-300">{h}</th>))}</tr></thead><tbody className="divide-y divide-slate-200 dark:divide-slate-700">{children}</tbody></table></div>)
|
|
}
|
|
|
|
import { useState } from 'react'
|
|
|
|
interface PaginationProps { page: number; totalPages: number; onPageChange: (p: number) => void }
|
|
export function Pagination({ page, totalPages, onPageChange }: PaginationProps) {
|
|
const [jumpValue, setJumpValue] = useState('')
|
|
|
|
if (totalPages <= 1) return null
|
|
const pages: (number | '...')[] = []
|
|
for (let i = 1; i <= totalPages; i++) {
|
|
if (i === 1 || i === totalPages || (i >= page - 1 && i <= page + 1)) pages.push(i)
|
|
else if (pages[pages.length - 1] !== '...') pages.push('...')
|
|
}
|
|
|
|
function handleJump() {
|
|
const targetPage = parseInt(jumpValue)
|
|
if (isNaN(targetPage) || targetPage < 1 || targetPage > totalPages) return
|
|
onPageChange(targetPage)
|
|
setJumpValue('')
|
|
}
|
|
|
|
return (<div className="flex items-center justify-center gap-2">
|
|
<button onClick={() => onPageChange(1)} disabled={page <= 1} title="首页" className="p-1.5 rounded-lg text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-800 disabled:opacity-40 disabled:cursor-not-allowed"><|</button>
|
|
<button onClick={() => onPageChange(page - 1)} disabled={page <= 1} title="上一页" className="p-1.5 rounded-lg text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-800 disabled:opacity-40 disabled:cursor-not-allowed"><</button>
|
|
{pages.map((p, i) => p === '...'
|
|
? <span key={`d-${i}`} className="px-1.5 text-slate-400 text-sm">…</span>
|
|
: <button key={p} onClick={() => onPageChange(p)} className={`min-w-[36px] px-2 py-1.5 rounded-lg text-sm font-medium transition-colors ${p === page ? 'bg-blue-600 text-white' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800'}`}>{p}</button>
|
|
)}
|
|
<button onClick={() => onPageChange(page + 1)} disabled={page >= totalPages} title="下一页" className="p-1.5 rounded-lg text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-800 disabled:opacity-40 disabled:cursor-not-allowed">></button>
|
|
<button onClick={() => onPageChange(totalPages)} disabled={page >= totalPages} title="末页" className="p-1.5 rounded-lg text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-800 disabled:opacity-40 disabled:cursor-not-allowed">|></button>
|
|
|
|
{/* 跳转输入框 */}
|
|
<div className="flex items-center gap-1.5 ml-2 border-l border-slate-200 dark:border-slate-700 pl-3">
|
|
<span className="text-xs text-slate-500 dark:text-slate-400">跳转到</span>
|
|
<input
|
|
type="number"
|
|
min="1"
|
|
max={totalPages}
|
|
value={jumpValue}
|
|
onChange={e => setJumpValue(e.target.value)}
|
|
onKeyDown={e => { if (e.key === 'Enter') handleJump() }}
|
|
className="w-14 px-2 py-1.5 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-sm text-slate-900 dark:text-white text-center focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
placeholder={`1-${totalPages}`}
|
|
/>
|
|
<button
|
|
onClick={handleJump}
|
|
disabled={!jumpValue || parseInt(jumpValue) < 1 || parseInt(jumpValue) > totalPages}
|
|
className="px-2 py-1.5 rounded-lg text-xs font-medium bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
|
>
|
|
跳转
|
|
</button>
|
|
<span className="text-xs text-slate-500 dark:text-slate-400">/ 共{totalPages}页</span>
|
|
</div>
|
|
</div>)
|
|
}
|
|
|
|
interface BadgeProps { variant?: 'default' | 'success' | 'warning' | 'danger' | 'info'; children: ReactNode }
|
|
export function Badge({ variant = 'default', children }: BadgeProps) {
|
|
const v = { default: 'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300', success: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400', warning: 'bg-amber-100 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400', danger: 'bg-red-100 text-red-700 dark:bg-red-500/10 dark:text-red-400', info: 'bg-blue-100 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400' }
|
|
return (<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${v[variant]}`}>{children}</span>)
|
|
}
|
|
|
|
interface CardProps { children: ReactNode; className?: string }
|
|
export function Card({ children, className = '' }: CardProps) {
|
|
return (<div className={`bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-700 shadow-sm ${className}`}>{children}</div>)
|
|
}
|
|
|
|
interface ToastProps { message: string; type?: 'success' | 'error' | 'info'; onClose: () => void }
|
|
export function Toast({ message, type = 'info', onClose }: ToastProps) {
|
|
const c = { success: 'bg-emerald-50 text-emerald-800 border-emerald-200 dark:bg-emerald-500/10 dark:text-emerald-400 dark:border-emerald-500/20', error: 'bg-red-50 text-red-800 border-red-200 dark:bg-red-500/10 dark:text-red-400 dark:border-red-500/20', info: 'bg-blue-50 text-blue-800 border-blue-200 dark:bg-blue-500/10 dark:text-blue-400 dark:border-blue-500/20' }
|
|
return (<div className={`fixed bottom-6 right-6 z-50 px-4 py-3 rounded-lg border shadow-lg text-sm font-medium ${c[type]}`}><div className="flex items-center gap-3"><span>{message}</span><button onClick={onClose} className="text-current opacity-60 hover:opacity-100">×</button></div></div>)
|
|
}
|