页面更新替换data-table组件

This commit is contained in:
wmp
2025-10-16 14:18:52 +08:00
parent 5d3f1daadf
commit 8e0c9284a0
6 changed files with 288 additions and 343 deletions

View File

@@ -3,7 +3,6 @@
import { useEffect, useState, useCallback } from 'react' import { useEffect, useState, useCallback } from 'react'
import LoadingCard from '@/components/ui/loadingCard' import LoadingCard from '@/components/ui/loadingCard'
import ErrorCard from '@/components/ui/errorCard' import ErrorCard from '@/components/ui/errorCard'
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
import { getAllocationStatus, type AllocationStatus } from '@/actions/stats' import { getAllocationStatus, type AllocationStatus } from '@/actions/stats'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Form, FormField } from '@/components/ui/form' import { Form, FormField } from '@/components/ui/form'
@@ -11,23 +10,18 @@ import { z } from 'zod'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { ArrowUpDownIcon, ArrowUpIcon, ArrowDownIcon } from 'lucide-react'
import { Page } from '@/components/page' import { Page } from '@/components/page'
import { DataTable } from '@/components/data-table'
const filterSchema = z.object({ const filterSchema = z.object({
timeFilter: z.string(), timeFilter: z.string(),
}) })
type FilterSchema = z.infer<typeof filterSchema> type FilterSchema = z.infer<typeof filterSchema>
type SortKey = 'city' | 'count' | 'assigned' | 'overage'
export default function AllocationStatus({ detailed = false }: { detailed?: boolean }) { export default function AllocationStatus({ detailed = false }: { detailed?: boolean }) {
const [data, setData] = useState<AllocationStatus[]>([]) const [data, setData] = useState<AllocationStatus[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
// 3.添加状态管理
const [sortKey, setSortKey] = useState<SortKey>('count')
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc')
const form = useForm<FilterSchema>({ const form = useForm<FilterSchema>({
resolver: zodResolver(filterSchema), resolver: zodResolver(filterSchema),
@@ -40,47 +34,12 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
const getTimeHours = useCallback(() => { const getTimeHours = useCallback(() => {
return parseInt(timeFilter) || 24 return parseInt(timeFilter) || 24
}, [timeFilter]) }, [timeFilter])
// 1.准备计算超额量的工具函数
const calculateOverage = (assigned: number, count: number) => {
return Math.max(0, assigned - count)
}
// 2.写一个排序数据的函数 const newData = data.map(item => ({
// 5.计算排序后的数据 ...item,
const sortedData = [...data].sort((a, b) => { overage: Math.max(0, Number(item.assigned) - Number(item.count)),
let aValue: string | number }))
let bValue: string | number console.log(newData, 'newData')
switch (sortKey) {
case 'city':
aValue = a.city || '未知'
bValue = b.city || '未知'
break
case 'count':
aValue = Number(a.count)
bValue = Number(b.count)
break
case 'assigned':
aValue = Number(a.assigned)
bValue = Number(b.assigned)
break
case 'overage':
aValue = calculateOverage(Number(a.assigned), Number(a.count))
bValue = calculateOverage(Number(b.assigned), Number(b.count))
break
}
if (typeof aValue === 'string') {
return sortDirection === 'asc'
? aValue.localeCompare(bValue as string)
: (bValue as string).localeCompare(aValue)
}
else {
return sortDirection === 'asc'
? aValue - (bValue as number)
: (bValue as number) - aValue
}
})
const fetchData = useCallback(async () => { const fetchData = useCallback(async () => {
try { try {
@@ -111,27 +70,6 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
fetchData() fetchData()
}, [fetchData]) }, [fetchData])
// 4.封装一个函数用于处理排序问题
const handleSort = (key: SortKey) => {
if (sortKey === key) {
setSortDirection(direction => direction === 'asc' ? 'desc' : 'asc')
}
else {
setSortKey(key)
setSortDirection('desc')
}
}
// 6. 渲染排序图标然后集成到ui里
const renderSortIcon = (key: SortKey) => {
if (sortKey !== key) {
return <ArrowUpDownIcon className="h-4 w-4 text-gray-400" />
}
return sortDirection === 'asc'
? <ArrowUpIcon className="h-4 w-4 text-blue-600" />
: <ArrowDownIcon className="h-4 w-4 text-blue-600" />
}
const onSubmit = (data: FilterSchema) => { const onSubmit = (data: FilterSchema) => {
fetchData() fetchData()
} }
@@ -172,49 +110,40 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
</form> </form>
</Form> </Form>
</div> </div>
<DataTable
data={newData}
columns={[
{
label: '城市',
props: 'city',
sortable: true,
},
{
label: '可用IP量',
props: 'count',
sortable: true,
},
{
label: '分配IP量',
props: 'assigned',
sortable: true,
},
{
label: '超额量',
props: 'overage',
sortable: true,
render: (val) => {
const overage = val.overage as number
return (
<span className={overage > 0 ? 'text-red-600 font-medium' : ''}>
{overage}
</span>
)
},
},
]}
/>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead onClick={() => handleSort('count')}>
<div className="flex items-center gap-2">
<span>IP量</span>
{renderSortIcon('count')}
</div>
</TableHead>
<TableHead onClick={() => handleSort('assigned')}>
<div className="flex items-center gap-2">
<span>IP量</span>
{renderSortIcon('assigned')}
</div>
</TableHead>
<TableHead onClick={() => handleSort('overage')}>
<div className="flex items-center gap-2">
<span></span>
{renderSortIcon('overage')}
</div>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sortedData.map((item, index) => {
const overage = calculateOverage(Number(item.assigned), Number(item.count))
return (
<TableRow key={index}>
<TableCell>{item.city}</TableCell>
<TableCell>{item.count}</TableCell>
<TableCell>{item.assigned}</TableCell>
<TableCell>
<span className={overage > 0 ? 'text-red-600 font-medium' : ''}>
{overage}
</span>
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</Page> </Page>
) )
} }

View File

@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table' import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
import { getCityNodeCount, type CityNode } from '@/actions/stats' import { getCityNodeCount, type CityNode } from '@/actions/stats'
import { Page } from '@/components/page' import { Page } from '@/components/page'
import { DataTable } from '@/components/data-table'
export default function CityNodeStats() { export default function CityNodeStats() {
const [data, setData] = useState<CityNode[]>([]) const [data, setData] = useState<CityNode[]>([])
@@ -47,28 +48,31 @@ export default function CityNodeStats() {
</span> </span>
</div> </div>
<Table> <DataTable
<TableHeader> data={data}
<TableRow> columns={[
<TableHead></TableHead> {
<TableHead></TableHead> label: '城市',
<TableHead>Hash</TableHead> props: 'city',
<TableHead></TableHead> },
<TableHead></TableHead> {
</TableRow> label: '节点数量',
</TableHeader> props: 'count',
<TableBody> },
{data.map((item, index) => ( {
<TableRow key={index}> label: 'Hash',
<TableCell>{item.city}</TableCell> props: 'hash',
<TableCell>{item.count}</TableCell> },
<TableCell>{item.hash}</TableCell> {
<TableCell>{item.label}</TableCell> label: '标签',
<TableCell>{item.offset}</TableCell> props: 'label',
</TableRow> },
))} {
</TableBody> label: '轮换顺位',
</Table> props: 'offset',
},
]}
/>
</Page> </Page>
) )
} }

View File

@@ -12,6 +12,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel } from '@/components/
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Page } from '@/components/page' import { Page } from '@/components/page'
import { DataTable } from '@/components/data-table'
// 定义表单验证规则 // 定义表单验证规则
const filterSchema = z.object({ const filterSchema = z.object({
@@ -53,14 +54,12 @@ export default function Edge() {
if (!result.success) { if (!result.success) {
throw new Error(result.error) throw new Error(result.error)
} }
const data = result.data const data = result.data
console.log(data) console.log(data)
setData(data.items) setData(data.items)
setTotal(data.total) setTotal(data.total)
setPage(data.page) setPage(data.page)
setSize(data.size) setSize(data.size)
setError(null) setError(null)
} }
catch (error) { catch (error) {
@@ -88,75 +87,6 @@ export default function Edge() {
fetchData(1, size) fetchData(1, size)
} }
// 多IP节点格式化
const formatMultiIP = (value: number | boolean): string => {
if (typeof value === 'number') {
switch (value) {
case 1: return '是'
case 0: return '否'
case -1: return '未知'
default: return `未知 (${value})`
}
}
return value ? '是' : '否'
}
// 独享IP节点格式化
const formatExclusiveIP = (value: number | boolean): string => {
if (typeof value === 'number') {
return value === 1 ? '是' : '否'
}
return value ? '是' : '否'
}
// 多IP节点颜色
const getMultiIPColor = (value: number | boolean): string => {
if (typeof value === 'number') {
switch (value) {
case 1: return 'bg-red-100 text-red-800'
case 0: return 'bg-green-100 text-green-800'
case -1: return 'bg-gray-100 text-gray-800'
default: return 'bg-gray-100 text-gray-800'
}
}
return value ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800'
}
// 独享IP节点颜色
const getExclusiveIPColor = (value: number | boolean): string => {
if (typeof value === 'number') {
return value === 1 ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
}
return value ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
}
const formatArchType = (arch: number): string => {
switch (arch) {
case 0: return '一代'
case 1: return '二代'
case 2: return 'AMD64'
case 3: return 'x86'
default: return `未知 (${arch})`
}
}
const getArchColor = (arch: number): string => {
switch (arch) {
case 0: return 'bg-blue-100 text-blue-800'
case 1: return 'bg-green-100 text-green-800'
case 2: return 'bg-purple-100 text-purple-800'
case 3: return 'bg-orange-100 text-orange-800'
default: return 'bg-gray-100 text-gray-800'
}
}
const formatOnlineTime = (seconds: number): string => {
if (seconds < 60) return `${seconds}`
if (seconds < 3600) return `${Math.floor(seconds / 60)}分钟`
if (seconds < 86400) return `${Math.floor(seconds / 3600)}小时`
return `${Math.floor(seconds / 86400)}`
}
useEffect(() => { useEffect(() => {
fetchData(page, size) fetchData(page, size)
}, []) }, [])
@@ -238,58 +168,89 @@ export default function Edge() {
</Button> </Button>
</form> </form>
</Form> </Form>
<Table> <DataTable
<TableHeader> data={data}
<TableRow> columns={[
<TableHead>MAC地址</TableHead> {
<TableHead></TableHead> label: 'MAC地址',
<TableHead>IP</TableHead> props: 'macaddr',
<TableHead></TableHead> },
<TableHead>IP节点</TableHead> {
<TableHead>IP</TableHead> label: '城市',
<TableHead></TableHead> props: 'city',
<TableHead>线</TableHead> },
</TableRow> {
</TableHeader> label: '公网IP',
<TableBody> props: 'public',
{data.map((item, index) => ( },
<TableRow key={item.id} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}> {
<TableCell className="px-4 py-3 text-sm font-mono text-blue-600">{item.macaddr}</TableCell> label: '运营商',
<TableCell className="px-4 py-3 text-sm text-gray-700">{item.city}</TableCell> render: (val) => {
<TableCell className="px-4 py-3 text-sm font-mono text-green-600">{item.public}</TableCell> const isp = val.isp as string
<TableCell className="px-4 py-3 text-sm text-gray-700"> return (
<span className={cn( <span className={cn('px-2 py-1 rounded-full text-xs', 'bg-gray-100 text-gray-800',
'px-2 py-1 rounded-full text-xs', { : 'bg-blue-100 text-blue-800',
'bg-gray-100 text-gray-800',
{
: 'bg-blue-100 text-blue-800',
: 'bg-purple-100 text-purple-800', : 'bg-purple-100 text-purple-800',
: 'bg-red-100 text-red-800', : 'bg-red-100 text-red-800',
}[item.isp], }[isp])}>
)}> {isp}
{item.isp}
</span> </span>
</TableCell> )
<TableCell className="px-4 py-3 text-sm"> },
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getMultiIPColor(item.single)}`}> },
{formatMultiIP(item.single)} {
label: '多IP节点',
render: (val) => {
const single = val.single as number
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
single === 1 ? 'bg-red-100 text-red-800' : single === 0 ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`}>
{single === 1 ? '是' : single === 0 ? '否' : '未知'}
</span> </span>
</TableCell> )
<TableCell className="px-4 py-3 text-sm"> },
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getExclusiveIPColor(item.sole)}`}> },
{formatExclusiveIP(item.sole)} {
label: '独享IP',
render: (val) => {
const sole = val.sole as number
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
sole === 1 ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`}>
{sole === 1 ? '是' : '否'}
</span> </span>
</TableCell> )
<TableCell className="px-4 py-3 text-sm"> },
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getArchColor(item.arch)}`}> },
{formatArchType(item.arch)} {
label: '设备类型',
render: (val) => {
const arch = val.arch as number
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
arch === 0 ? 'bg-blue-100 text-blue-800'
: arch === 1 ? 'bg-green-100 text-green-800'
: arch === 2 ? 'bg-purple-100 text-purple-800'
: arch === 3 ? 'bg-orange-100 text-orange-800'
: 'bg-gray-100 text-gray-800'}`}>
{arch === 0 ? '一代' : arch === 1 ? '二代' : arch === 2 ? 'AMD64' : arch === 3 ? 'x86' : `未知 (${arch})`}
</span> </span>
</TableCell> )
<TableCell className="px-4 py-3 text-sm text-gray-700">{formatOnlineTime(item.online)}</TableCell> },
</TableRow> },
))} {
</TableBody> label: '在线时长',
</Table> render: (val) => {
const seconds = val.online as number
return seconds < 60 ? `${seconds}`
: seconds < 3600 ? `${Math.floor(seconds / 60)}分钟`
: seconds < 86400 ? `${Math.floor(seconds / 3600)}小时`
: `${Math.floor(seconds / 86400)}`
},
},
]}
/>
{/* 分页 */} {/* 分页 */}
<Pagination <Pagination
page={page} page={page}

View File

@@ -13,6 +13,7 @@ import { Input } from '@/components/ui/input'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { SearchIcon } from 'lucide-react' import { SearchIcon } from 'lucide-react'
import { Page } from '@/components/page' import { Page } from '@/components/page'
import { DataTable } from '@/components/data-table'
function GatewayConfigContent() { function GatewayConfigContent() {
const [data, setData] = useState<GatewayConfig[]>([]) const [data, setData] = useState<GatewayConfig[]>([])
@@ -144,24 +145,6 @@ function GatewayConfigContent() {
fetchData(filters, 1) fetchData(filters, 1)
} }
const getStatusBadge = (value: number, trueText: string = '是', falseText: string = '否') => {
// 0是正常1是更新正常绿+ 更新(红)
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${value === 0 ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
{value === 0 ? trueText : falseText}
</span>
)
}
const getOnlineStatus = (isonline: number) => {
// 0是空闲1是在用在用+ 空闲(绿)
return (
<div className="flex items-center">
<div className={`${isonline === 0 ? 'bg-green-500' : 'bg-red-500'}`} />
{getStatusBadge(isonline, '空闲', '在用')}
</div>
)
}
// 当前选中的mac // 当前选中的mac
const [selectedMac, setSelectedMac] = useState<string>('') const [selectedMac, setSelectedMac] = useState<string>('')
const handleMacClick = useCallback(async (macaddr: string) => { const handleMacClick = useCallback(async (macaddr: string) => {
@@ -295,36 +278,60 @@ function GatewayConfigContent() {
</Button> </Button>
</form> </form>
</Form> </Form>
<Table> <DataTable
<TableHeader> data={data}
<TableRow> columns={[
<TableHead></TableHead> {
<TableHead>线</TableHead> label: '端口',
<TableHead></TableHead> props: 'inner_ip',
<TableHead>MAC</TableHead> },
<TableHead>IP</TableHead> {
<TableHead></TableHead> label: '线路',
<TableHead></TableHead> props: 'user',
</TableRow> },
</TableHeader> {
<TableBody> label: '城市',
{data.map((item, index) => ( props: 'city',
<TableRow key={index}> },
<TableCell>{item.inner_ip}</TableCell> {
<TableCell>{item.user}</TableCell> label: '节点MAC',
<TableCell>{item.city}</TableCell> props: 'edge',
<TableCell>{item.edge}</TableCell> },
<TableCell>{item.public}</TableCell> {
<TableCell> label: '节点IP',
{getStatusBadge(item.ischange, '正常', '更新')} props: 'public',
</TableCell> },
<TableCell> {
{getOnlineStatus(item.isonline)} label: '配置更新',
</TableCell> render: (val) => {
</TableRow> const ischange = val.ischange as number
))} return (
</TableBody> <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
</Table> ischange === 0 ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}>
{ischange === 0 ? '正常' : '更新'}
</span>
)
},
},
{
label: '在用状态',
render: (val) => {
const isonline = val.isonline as number
return (
<div className="flex items-center gap-1.5">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
isonline === 0 ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}>
{isonline === 0 ? '空闲' : '在用'}
</span>
</div>
)
},
},
]}
/>
{/* 分页组件 */} {/* 分页组件 */}
<Pagination <Pagination
total={total} total={total}

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table' import { DataTable } from '@/components/data-table'
import { Form, FormField } from '@/components/ui/form' import { Form, FormField } from '@/components/ui/form'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { z } from 'zod' import { z } from 'zod'
@@ -134,16 +134,6 @@ export default function Gatewayinfo() {
} }
} }
const getStatusText = (enable: number) => {
return enable === 1 ? '启用' : '禁用'
}
const getStatusClass = (enable: number) => {
return enable === 1
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}
if (loading) { if (loading) {
return ( return (
<div className="bg-white w-full shadow p-6 overflow-hidden"> <div className="bg-white w-full shadow p-6 overflow-hidden">
@@ -200,40 +190,41 @@ export default function Gatewayinfo() {
<div className="flex-auto overflow-hidden gap-6 flex"> <div className="flex-auto overflow-hidden gap-6 flex">
<div className="flex-3 flex flex-col"> <div className="flex-3 flex flex-col">
<Table> <DataTable
<TableHeader> data={filteredData}
<TableRow> columns={[
<TableHead> {
<div className="flex items-center"> label: 'MAC地址',
<span>MAC地址</span> render: val => (
<SmartCopyButton data={filteredData} mode="batch" /> <div className="flex items-center gap-2">
{String(val.macaddr)}
<SmartCopyButton data={String(val.macaddr)} />
</div> </div>
</TableHead> ),
<TableHead>IP</TableHead> },
<TableHead></TableHead> {
<TableHead></TableHead> label: '内网IP',
</TableRow> props: 'inner_ip',
</TableHeader> },
<TableBody> {
{filteredData.map((item, index) => ( label: '配置版本',
<TableRow key={index}> props: 'setid',
<TableCell> },
<div className="flex items-center gap-2"> {
{item.macaddr} label: '状态',
<SmartCopyButton data={item.macaddr} /> render: (val) => {
</div> const enable = val.enable as number
</TableCell> return (
<TableCell>{item.inner_ip}</TableCell> <span className={`px-2 py-1 rounded-full text-xs font-medium ${enable === 1
<TableCell>{item.setid}</TableCell> ? 'bg-green-100 text-green-800'
<TableCell> : 'bg-red-100 text-red-800'}`}>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(item.enable)}`}> {enable === 1 ? '启用' : '禁用'}
{getStatusText(item.enable)}
</span> </span>
</TableCell> )
</TableRow> },
))} },
</TableBody> ]}
</Table> />
</div> </div>
<div className="flex-1 flex-col gap-4 mb-6 flex"> <div className="flex-1 flex-col gap-4 mb-6 flex">
<div className="bg-blue-50 p-4 rounded-lg"> <div className="bg-blue-50 p-4 rounded-lg">

View File

@@ -1,6 +1,7 @@
import * as React from 'react' import * as React from 'react'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table'
import { ReactNode } from 'react' import { ReactNode, useState } from 'react'
import { ArrowUpDownIcon, ArrowUpIcon, ArrowDownIcon, Columns } from 'lucide-react'
type Data = { type Data = {
[key: string]: unknown [key: string]: unknown
@@ -10,20 +11,72 @@ type Column = {
label: string label: string
props?: string props?: string
render?: (val: Data) => ReactNode render?: (val: Data) => ReactNode
sortable?: boolean
} }
export function DataTable(props: { data: Data[], columns: Column[] }) { export function DataTable(props: { data: Data[], columns: Column[] }) {
const [sortKey, setSortKey] = useState<string>('')
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc')
const sortedData = [...props.data].sort((a, b) => {
let aValue = a[sortKey]
let bValue = b[sortKey]
if (aValue === undefined || aValue === null) aValue = ''
if (bValue === undefined || bValue === null) bValue = ''
const aStr = String(aValue)
const bStr = String(bValue)
const aNum = Number(aValue)
const bNum = Number(bValue)
if (!isNaN(aNum) && !isNaN(bNum)) {
return sortDirection === 'asc' ? aNum - bNum : bNum - aNum
}
else {
return sortDirection === 'asc'
? aStr.localeCompare(bStr)
: bStr.localeCompare(aStr)
}
})
const handleSort = (key: string) => {
if (!key) return
if (sortKey === key) {
setSortDirection(direction => direction === 'asc' ? 'desc' : 'asc')
}
else {
setSortKey(key)
setSortDirection('desc')
}
}
const renderSortIcon = (key: string) => {
if (sortKey !== key) {
return <ArrowUpDownIcon className="h-4 w-4 text-gray-400" />
}
return sortDirection === 'asc'
? <ArrowUpIcon className="h-4 w-4 text-blue-600" />
: <ArrowDownIcon className="h-4 w-4 text-blue-600" />
}
return ( return (
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
{props.columns.map((item, index) => ( {props.columns.map((item, index) => (
<TableHead key={index}>{item.label}</TableHead> <TableHead key={index} onClick={() => item.sortable && handleSort(item.props || item.label)} className={item.sortable ? 'cursor-pointer hover:bg-gray-50' : ''}>
<div className="flex items-center gap-2">
<span> {item.label}</span>
{item.sortable && item.props && renderSortIcon(item.props)}
</div>
</TableHead>
))} ))}
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{props.data.map((row, index) => ( {sortedData.map((row, index) => (
<TableRow key={index}> <TableRow key={index}>
{props.columns.map((colume, index) => ( {props.columns.map((colume, index) => (
<TableCell key={index}> <TableCell key={index}>