页面更新替换data-table组件
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user