feat: Sprint 1 — 设计Token系统 + 新导航5分组 + AppShell/PageHeader/FilterBar/DataTable组件 + 花名册导出扩充19字段 + 社保/公积金不缴纳选项 + 个税申报表导出 + 入离职/绩效统计看板
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* 数据表格组件 — 统一表头、行样式、排序状态和空状态
|
||||
* 支持列定义、行点击、选中行、固定列、对齐和空状态
|
||||
*/
|
||||
import { ReactNode } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { ChevronUp, ChevronDown, ChevronsUpDown } from 'lucide-react'
|
||||
|
||||
export interface Column<T> {
|
||||
key: string
|
||||
header: string
|
||||
render?: (row: T) => ReactNode
|
||||
sortable?: boolean
|
||||
align?: 'left' | 'center' | 'right'
|
||||
width?: string
|
||||
fixed?: 'left' | 'right'
|
||||
}
|
||||
|
||||
interface DataTableProps<T> {
|
||||
columns: Column<T>[]
|
||||
data: T[]
|
||||
rowKey: (row: T) => string
|
||||
onRowClick?: (row: T) => void
|
||||
sortBy?: string
|
||||
sortOrder?: 'asc' | 'desc'
|
||||
onSort?: (key: string) => void
|
||||
emptyState?: ReactNode
|
||||
dense?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据表格 — 支持排序、行点击、空状态和密度切换
|
||||
*/
|
||||
export function DataTable<T>({
|
||||
columns,
|
||||
data,
|
||||
rowKey,
|
||||
onRowClick,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
onSort,
|
||||
emptyState,
|
||||
dense,
|
||||
className,
|
||||
}: DataTableProps<T>) {
|
||||
const alignClass = (align?: string) =>
|
||||
align === 'right' ? 'text-right' : align === 'center' ? 'text-center' : 'text-left'
|
||||
|
||||
return (
|
||||
<div className={clsx('overflow-x-auto border border-border-default rounded-card bg-surface-card', className)}>
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border-default bg-surface-muted">
|
||||
{columns.map((col) => (
|
||||
<th
|
||||
key={col.key}
|
||||
className={clsx(
|
||||
'px-3 font-medium text-ink-600 whitespace-nowrap select-none',
|
||||
dense ? 'py-1.5' : 'py-2.5',
|
||||
alignClass(col.align),
|
||||
col.fixed === 'left' && 'sticky left-0 z-10 bg-surface-muted',
|
||||
col.fixed === 'right' && 'sticky right-0 z-10 bg-surface-muted',
|
||||
onSort && col.sortable && 'cursor-pointer hover:text-ink-900',
|
||||
)}
|
||||
style={col.width ? { width: col.width } : undefined}
|
||||
onClick={col.sortable && onSort ? () => onSort(col.key) : undefined}
|
||||
>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{col.header}
|
||||
{col.sortable && onSort && (
|
||||
<span className="inline-flex">
|
||||
{sortBy === col.key ? (
|
||||
sortOrder === 'asc' ? (
|
||||
<ChevronUp className="w-3.5 h-3.5" />
|
||||
) : (
|
||||
<ChevronDown className="w-3.5 h-3.5" />
|
||||
)
|
||||
) : (
|
||||
<ChevronsUpDown className="w-3.5 h-3.5 text-ink-400" />
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={columns.length} className="text-center py-12 text-ink-400">
|
||||
{emptyState || '暂无数据'}
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
data.map((row) => (
|
||||
<tr
|
||||
key={rowKey(row)}
|
||||
onClick={onRowClick ? () => onRowClick(row) : undefined}
|
||||
className={clsx(
|
||||
'border-b border-border-subtle transition-colors',
|
||||
onRowClick && 'cursor-pointer hover:bg-surface-muted',
|
||||
)}
|
||||
>
|
||||
{columns.map((col) => (
|
||||
<td
|
||||
key={col.key}
|
||||
className={clsx(
|
||||
'px-3 text-ink-900 whitespace-nowrap',
|
||||
dense ? 'py-1.5' : 'py-2.5',
|
||||
alignClass(col.align),
|
||||
col.fixed === 'left' && 'sticky left-0 z-10 bg-surface-card',
|
||||
col.fixed === 'right' && 'sticky right-0 z-10 bg-surface-card',
|
||||
)}
|
||||
>
|
||||
{col.render ? col.render(row) : (row as any)[col.key]}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 筛选栏组件 — 统一搜索、筛选条件展示和工具按钮
|
||||
* 支持搜索框、可移除的筛选条件标签、右侧工具区
|
||||
*/
|
||||
import { ReactNode } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { Search, X } from 'lucide-react'
|
||||
|
||||
interface FilterBarProps {
|
||||
searchValue?: string
|
||||
searchPlaceholder?: string
|
||||
onSearchChange?: (value: string) => void
|
||||
filters?: ReactNode
|
||||
activeChips?: Array<{ label: string; onRemove: () => void }>
|
||||
tools?: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 筛选栏 — 搜索 + 筛选条件 + 工具按钮
|
||||
*/
|
||||
export function FilterBar({
|
||||
searchValue,
|
||||
searchPlaceholder = '搜索…',
|
||||
onSearchChange,
|
||||
filters,
|
||||
activeChips,
|
||||
tools,
|
||||
className,
|
||||
}: FilterBarProps) {
|
||||
return (
|
||||
<div className={clsx('flex flex-col gap-2 mb-3', className)}>
|
||||
{/* 第一行:搜索 + 筛选器 + 工具 */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{onSearchChange !== undefined && (
|
||||
<div className="relative flex-1 min-w-[180px] max-w-xs">
|
||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-ink-400" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchValue || ''}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
placeholder={searchPlaceholder}
|
||||
className="w-full pl-8 pr-3 py-1.5 rounded-md border border-border-default bg-surface-card text-sm text-ink-900 focus:outline-none focus:ring-2 focus:ring-brand-600/30 focus:border-brand-600 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{filters && <div className="flex items-center gap-2 flex-wrap">{filters}</div>}
|
||||
{tools && <div className="flex items-center gap-2 ml-auto shrink-0">{tools}</div>}
|
||||
</div>
|
||||
|
||||
{/* 第二行:激活的筛选条件标签 */}
|
||||
{activeChips && activeChips.length > 0 && (
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
{activeChips.map((chip, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-surface-muted text-ink-600"
|
||||
>
|
||||
{chip.label}
|
||||
<button
|
||||
onClick={chip.onRemove}
|
||||
className="hover:text-danger transition-colors"
|
||||
aria-label="移除筛选条件"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user