diff --git a/src/app/(root)/allocationStatus/page.tsx b/src/app/(root)/allocationStatus/page.tsx index 795437e..f2e9d47 100644 --- a/src/app/(root)/allocationStatus/page.tsx +++ b/src/app/(root)/allocationStatus/page.tsx @@ -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 -type SortKey = 'city' | 'count' | 'assigned' | 'overage' - export default function AllocationStatus({ detailed = false }: { detailed?: boolean }) { const [data, setData] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) - // 3.添加状态管理 - const [sortKey, setSortKey] = useState('count') - const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc') const form = useForm({ 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 - } - return sortDirection === 'asc' - ? - : - } - const onSubmit = (data: FilterSchema) => { fetchData() } @@ -172,49 +110,40 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool + { + const overage = val.overage as number + return ( + 0 ? 'text-red-600 font-medium' : ''}> + {overage} + + ) + }, + }, + ]} + /> - - - - 城市 - handleSort('count')}> -
- 可用IP量 - {renderSortIcon('count')} -
-
- handleSort('assigned')}> -
- 分配IP量 - {renderSortIcon('assigned')} -
-
- handleSort('overage')}> -
- 超额量 - {renderSortIcon('overage')} -
-
-
-
- - {sortedData.map((item, index) => { - const overage = calculateOverage(Number(item.assigned), Number(item.count)) - return ( - - {item.city} - {item.count} - {item.assigned} - - 0 ? 'text-red-600 font-medium' : ''}> - {overage} - - - - ) - })} - -
) } diff --git a/src/app/(root)/cityNodeStats/page.tsx b/src/app/(root)/cityNodeStats/page.tsx index baed55d..8f8f5cb 100644 --- a/src/app/(root)/cityNodeStats/page.tsx +++ b/src/app/(root)/cityNodeStats/page.tsx @@ -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([]) @@ -47,28 +48,31 @@ export default function CityNodeStats() { - - - - 城市 - 节点数量 - Hash - 标签 - 轮换顺位 - - - - {data.map((item, index) => ( - - {item.city} - {item.count} - {item.hash} - {item.label} - {item.offset} - - ))} - -
+ ) } diff --git a/src/app/(root)/edge/page.tsx b/src/app/(root)/edge/page.tsx index eb5f3e9..f01d95a 100644 --- a/src/app/(root)/edge/page.tsx +++ b/src/app/(root)/edge/page.tsx @@ -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() { - - - - MAC地址 - 城市 - 公网IP - 运营商 - 多IP节点 - 独享IP - 设备类型 - 在线时长 - - - - {data.map((item, index) => ( - - {item.macaddr} - {item.city} - {item.public} - - { + const isp = val.isp as string + return ( + - {item.isp} + }[isp])}> + {isp} - - - - {formatMultiIP(item.single)} + ) + }, + }, + { + label: '多IP节点', + render: (val) => { + const single = val.single as number + return ( + + {single === 1 ? '是' : single === 0 ? '否' : '未知'} - - - - {formatExclusiveIP(item.sole)} + ) + }, + }, + { + label: '独享IP', + render: (val) => { + const sole = val.sole as number + return ( + + {sole === 1 ? '是' : '否'} - - - - {formatArchType(item.arch)} + ) + }, + }, + { + label: '设备类型', + render: (val) => { + const arch = val.arch as number + return ( + + {arch === 0 ? '一代' : arch === 1 ? '二代' : arch === 2 ? 'AMD64' : arch === 3 ? 'x86' : `未知 (${arch})`} - - {formatOnlineTime(item.online)} - - ))} - -
+ ) + }, + }, + { + 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)}天` + }, + }, + ]} + /> + {/* 分页 */} ([]) @@ -144,24 +145,6 @@ function GatewayConfigContent() { fetchData(filters, 1) } - const getStatusBadge = (value: number, trueText: string = '是', falseText: string = '否') => { - // 0是正常1是更新,正常(绿)+ 更新(红) - return ( - - {value === 0 ? trueText : falseText} - - ) - } - - const getOnlineStatus = (isonline: number) => { - // 0是空闲1是在用,在用(红)+ 空闲(绿) - return ( -
-
- {getStatusBadge(isonline, '空闲', '在用')} -
- ) - } // 当前选中的mac const [selectedMac, setSelectedMac] = useState('') const handleMacClick = useCallback(async (macaddr: string) => { @@ -295,36 +278,60 @@ function GatewayConfigContent() { - - - - 端口 - 线路 - 城市 - 节点MAC - 节点IP - 配置更新 - 在用状态 - - - - {data.map((item, index) => ( - - {item.inner_ip} - {item.user} - {item.city} - {item.edge} - {item.public} - - {getStatusBadge(item.ischange, '正常', '更新')} - - - {getOnlineStatus(item.isonline)} - - - ))} - -
+ { + const ischange = val.ischange as number + return ( + + {ischange === 0 ? '正常' : '更新'} + + ) + }, + }, + { + label: '在用状态', + render: (val) => { + const isonline = val.isonline as number + return ( +
+ + {isonline === 0 ? '空闲' : '在用'} + +
+ ) + }, + }, + ]} + /> + {/* 分页组件 */} { - 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 (
@@ -200,40 +190,41 @@ export default function Gatewayinfo() {
- - - - -
- MAC地址 - + ( +
+ {String(val.macaddr)} +
- - 内网IP - 配置版本 - 状态 - - - - {filteredData.map((item, index) => ( - - -
- {item.macaddr} - -
-
- {item.inner_ip} - {item.setid} - - - {getStatusText(item.enable)} + ), + }, + { + label: '内网IP', + props: 'inner_ip', + }, + { + label: '配置版本', + props: 'setid', + }, + { + label: '状态', + render: (val) => { + const enable = val.enable as number + return ( + + {enable === 1 ? '启用' : '禁用'} - -
- ))} -
-
+ ) + }, + }, + ]} + />
diff --git a/src/components/data-table.tsx b/src/components/data-table.tsx index 3254bc6..00daca3 100644 --- a/src/components/data-table.tsx +++ b/src/components/data-table.tsx @@ -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('') + 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 + } + return sortDirection === 'asc' + ? + : + } + return ( {props.columns.map((item, index) => ( - {item.label} + item.sortable && handleSort(item.props || item.label)} className={item.sortable ? 'cursor-pointer hover:bg-gray-50' : ''}> +
+ {item.label} + {item.sortable && item.props && renderSortIcon(item.props)} +
+
))}
- {props.data.map((row, index) => ( + {sortedData.map((row, index) => ( {props.columns.map((colume, index) => (