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