页面更新替换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 LoadingCard from '@/components/ui/loadingCard'
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Form, FormField } from '@/components/ui/form'
@@ -11,23 +10,18 @@ import { z } from 'zod'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { Button } from '@/components/ui/button'
import { ArrowUpDownIcon, ArrowUpIcon, ArrowDownIcon } from 'lucide-react'
import { Page } from '@/components/page'
import { DataTable } from '@/components/data-table'
const filterSchema = z.object({
timeFilter: z.string(),
})
type FilterSchema = z.infer<typeof filterSchema>
type SortKey = 'city' | 'count' | 'assigned' | 'overage'
export default function AllocationStatus({ detailed = false }: { detailed?: boolean }) {
const [data, setData] = useState<AllocationStatus[]>([])
const [loading, setLoading] = useState(true)
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>({
resolver: zodResolver(filterSchema),
@@ -40,47 +34,12 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
const getTimeHours = useCallback(() => {
return parseInt(timeFilter) || 24
}, [timeFilter])
// 1.准备计算超额量的工具函数
const calculateOverage = (assigned: number, count: number) => {
return Math.max(0, assigned - count)
}
// 2.写一个排序数据的函数
// 5.计算排序后的数据
const sortedData = [...data].sort((a, b) => {
let aValue: string | number
let bValue: string | number
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 newData = data.map(item => ({
...item,
overage: Math.max(0, Number(item.assigned) - Number(item.count)),
}))
console.log(newData, 'newData')
const fetchData = useCallback(async () => {
try {
@@ -111,27 +70,6 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
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) => {
fetchData()
}
@@ -172,49 +110,40 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
</form>
</Form>
</div>
<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))
<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 (
<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>
)
}

View File

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

View File

@@ -12,6 +12,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel } from '@/components/
import { Input } from '@/components/ui/input'
import { cn } from '@/lib/utils'
import { Page } from '@/components/page'
import { DataTable } from '@/components/data-table'
// 定义表单验证规则
const filterSchema = z.object({
@@ -53,14 +54,12 @@ export default function Edge() {
if (!result.success) {
throw new Error(result.error)
}
const data = result.data
console.log(data)
setData(data.items)
setTotal(data.total)
setPage(data.page)
setSize(data.size)
setError(null)
}
catch (error) {
@@ -88,75 +87,6 @@ export default function Edge() {
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(() => {
fetchData(page, size)
}, [])
@@ -238,58 +168,89 @@ export default function Edge() {
</Button>
</form>
</Form>
<Table>
<TableHeader>
<TableRow>
<TableHead>MAC地址</TableHead>
<TableHead></TableHead>
<TableHead>IP</TableHead>
<TableHead></TableHead>
<TableHead>IP节点</TableHead>
<TableHead>IP</TableHead>
<TableHead></TableHead>
<TableHead>线</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{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>
<TableCell className="px-4 py-3 text-sm text-gray-700">{item.city}</TableCell>
<TableCell className="px-4 py-3 text-sm font-mono text-green-600">{item.public}</TableCell>
<TableCell className="px-4 py-3 text-sm text-gray-700">
<span className={cn(
'px-2 py-1 rounded-full text-xs',
'bg-gray-100 text-gray-800',
<DataTable
data={data}
columns={[
{
: 'bg-blue-100 text-blue-800',
label: 'MAC地址',
props: 'macaddr',
},
{
label: '城市',
props: 'city',
},
{
label: '公网IP',
props: 'public',
},
{
label: '运营商',
render: (val) => {
const isp = val.isp as string
return (
<span className={cn('px-2 py-1 rounded-full text-xs', 'bg-gray-100 text-gray-800',
{ : 'bg-blue-100 text-blue-800',
: 'bg-purple-100 text-purple-800',
: 'bg-red-100 text-red-800',
}[item.isp],
)}>
{item.isp}
}[isp])}>
{isp}
</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>
</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>
</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>
</TableCell>
<TableCell className="px-4 py-3 text-sm text-gray-700">{formatOnlineTime(item.online)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
},
},
{
label: '在线时长',
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
page={page}

View File

@@ -13,6 +13,7 @@ import { Input } from '@/components/ui/input'
import { cn } from '@/lib/utils'
import { SearchIcon } from 'lucide-react'
import { Page } from '@/components/page'
import { DataTable } from '@/components/data-table'
function GatewayConfigContent() {
const [data, setData] = useState<GatewayConfig[]>([])
@@ -144,24 +145,6 @@ function GatewayConfigContent() {
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
const [selectedMac, setSelectedMac] = useState<string>('')
const handleMacClick = useCallback(async (macaddr: string) => {
@@ -295,36 +278,60 @@ function GatewayConfigContent() {
</Button>
</form>
</Form>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead>线</TableHead>
<TableHead></TableHead>
<TableHead>MAC</TableHead>
<TableHead>IP</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.map((item, index) => (
<TableRow key={index}>
<TableCell>{item.inner_ip}</TableCell>
<TableCell>{item.user}</TableCell>
<TableCell>{item.city}</TableCell>
<TableCell>{item.edge}</TableCell>
<TableCell>{item.public}</TableCell>
<TableCell>
{getStatusBadge(item.ischange, '正常', '更新')}
</TableCell>
<TableCell>
{getOnlineStatus(item.isonline)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<DataTable
data={data}
columns={[
{
label: '端口',
props: 'inner_ip',
},
{
label: '线路',
props: 'user',
},
{
label: '城市',
props: 'city',
},
{
label: '节点MAC',
props: 'edge',
},
{
label: '节点IP',
props: 'public',
},
{
label: '配置更新',
render: (val) => {
const ischange = val.ischange as number
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
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
total={total}

View File

@@ -1,7 +1,7 @@
'use client'
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
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) {
return (
<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-3 flex flex-col">
<Table>
<TableHeader>
<TableRow>
<TableHead>
<div className="flex items-center">
<span>MAC地址</span>
<SmartCopyButton data={filteredData} mode="batch" />
</div>
</TableHead>
<TableHead>IP</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredData.map((item, index) => (
<TableRow key={index}>
<TableCell>
<DataTable
data={filteredData}
columns={[
{
label: 'MAC地址',
render: val => (
<div className="flex items-center gap-2">
{item.macaddr}
<SmartCopyButton data={item.macaddr} />
{String(val.macaddr)}
<SmartCopyButton data={String(val.macaddr)} />
</div>
</TableCell>
<TableCell>{item.inner_ip}</TableCell>
<TableCell>{item.setid}</TableCell>
<TableCell>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(item.enable)}`}>
{getStatusText(item.enable)}
),
},
{
label: '内网IP',
props: 'inner_ip',
},
{
label: '配置版本',
props: 'setid',
},
{
label: '状态',
render: (val) => {
const enable = val.enable as number
return (
<span className={`px-2 py-1 rounded-full text-xs font-medium ${enable === 1
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'}`}>
{enable === 1 ? '启用' : '禁用'}
</span>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
},
},
]}
/>
</div>
<div className="flex-1 flex-col gap-4 mb-6 flex">
<div className="bg-blue-50 p-4 rounded-lg">

View File

@@ -1,6 +1,7 @@
import * as React from 'react'
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 = {
[key: string]: unknown
@@ -10,20 +11,72 @@ type Column = {
label: string
props?: string
render?: (val: Data) => ReactNode
sortable?: boolean
}
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 (
<Table>
<TableHeader>
<TableRow>
{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>
</TableHeader>
<TableBody>
{props.data.map((row, index) => (
{sortedData.map((row, index) => (
<TableRow key={index}>
{props.columns.map((colume, index) => (
<TableCell key={index}>