diff --git a/src/actions/stats.ts b/src/actions/stats.ts index 072679e..63926e2 100644 --- a/src/actions/stats.ts +++ b/src/actions/stats.ts @@ -105,15 +105,22 @@ export type GatewayConfig = { } // 网关配置 -export async function getGatewayConfig(page?: number, mac?: string): Promise>> { +export async function getGatewayConfig(page?: number, filters?: { + mac?: string + public?: string + city?: string + user?: string + inner_ip?: string +}): Promise>> { try { - if (!page && !mac) { + if (!page && !filters?.mac) { throw new Error('页码和MAC地址不能同时为空') } - page = mac ? 1 : Math.max(1, page || 1) + page = filters?.mac ? 1 : Math.max(1, page || 1) + const [total, result] = await Promise.all([ - drizzle.$count(gateway, mac ? eq(gateway.macaddr, mac) : undefined), + drizzle.$count(gateway, filters?.mac ? eq(gateway.macaddr, filters?.mac) : undefined), drizzle .select({ city: cityhash.city, @@ -127,12 +134,11 @@ export async function getGatewayConfig(page?: number, mac?: string): Promise + +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) - const [timeFilter, setTimeFilter] = useState('24') // 默认24小时 - const [customHours, setCustomHours] = useState('') + // 3.添加状态管理 + const [sortKey, setSortKey] = useState('count') + const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc') + + const form = useForm({ + resolver: zodResolver(filterSchema), + defaultValues: { + timeFilter: '24', + }, + }) + const timeFilter = form.watch('timeFilter') - // 获取时间参数(小时数) const getTimeHours = useCallback(() => { - if (timeFilter === 'custom' && customHours) { - const hours = parseInt(customHours) - return isNaN(hours) ? 24 : Math.max(1, hours) // 默认24小时,最少1小时 + 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 } - return parseInt(timeFilter) || 24 // 默认24小时 - }, [timeFilter, customHours]) - - // 计算超额量 - const calculateOverage = (assigned: number, count: number) => { - const overage = assigned - count - return Math.max(0, overage) - } + 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 () => { try { @@ -37,16 +89,13 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool const hours = getTimeHours() const result = await getAllocationStatus(hours) - // 数据验证 - const validatedData = (result.data).map(item => ({ + const validatedData = result.data.map(item => ({ city: item.city || '未知', count: item.count, assigned: item.assigned, })) - const sortedData = validatedData.sort((a, b) => b.count - a.count) - - setData(sortedData) + setData(validatedData) } catch (error) { console.error('Failed to fetch allocation status:', error) @@ -61,57 +110,117 @@ 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() + } + if (loading) return if (error) return - const problematicCities = data.filter(item => item.assigned > item.count) - return ( -
+

节点分配状态

- {/* 时间筛选器 */}
- - +
+ + ( +
+ 时间筛选: + +
+ )} + /> - + + +
-
+
- 城市 - 可用IP量 - 分配IP量 - 超额量 + handleSort('city')} + > +
+ 城市 + {renderSortIcon('city')} +
+
+ handleSort('count')} + > +
+ 可用IP量 + {renderSortIcon('count')} +
+
+ handleSort('assigned')} + > +
+ 分配IP量 + {renderSortIcon('assigned')} +
+
+ handleSort('overage')} + > +
+ 超额量 + {renderSortIcon('overage')} +
+
- {data.map((item, index) => { + {sortedData.map((item, index) => { const overage = calculateOverage(Number(item.assigned), Number(item.count)) return ( - + {item.city} {item.count} {item.assigned} diff --git a/src/app/dashboard/components/cityNodeStats.tsx b/src/app/dashboard/components/cityNodeStats.tsx index 0a386fc..c9900ed 100644 --- a/src/app/dashboard/components/cityNodeStats.tsx +++ b/src/app/dashboard/components/cityNodeStats.tsx @@ -15,6 +15,9 @@ export default function CityNodeStats() { const fetchData = async () => { try { const result = await getCityNodeCount() + if (!result.success) { + throw new Error(result.error || '查询城市节点失败') + } setData(result.data) } catch (error) { @@ -27,7 +30,7 @@ export default function CityNodeStats() { if (loading) { return ( -
+

城市节点数量分布

加载中...
@@ -35,7 +38,7 @@ export default function CityNodeStats() { } return ( -
+

城市节点数量分布

@@ -66,7 +69,7 @@ export default function CityNodeStats() { {item.hash} - {item.label} + {item.label || '无标签'} {item.offset} diff --git a/src/app/dashboard/components/edge.tsx b/src/app/dashboard/components/edge.tsx index 0aac9fc..abffc04 100644 --- a/src/app/dashboard/components/edge.tsx +++ b/src/app/dashboard/components/edge.tsx @@ -3,19 +3,24 @@ import { useEffect, useState } from 'react' import { Pagination } from '@/components/ui/pagination' import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table' -import { getEdgeNodes } from '@/actions/stats' +import { getEdgeNodes, type Edge } from '@/actions/stats' +import { zodResolver } from '@hookform/resolvers/zod' +import { useForm } from 'react-hook-form' +import { z } from 'zod' +import { Button } from '@/components/ui/button' +import { Form, FormField, FormItem, FormLabel } from '@/components/ui/form' +import { Input } from '@/components/ui/input' +import { cn } from '@/lib/utils' -interface Edge { - id: number - macaddr: string - city: string - public: string - isp: string - single: number | boolean - sole: number | boolean - arch: number - online: number -} +// 定义表单验证规则 +const filterSchema = z.object({ + macaddr: z.string().optional(), + public: z.string().optional(), + city: z.string().optional(), + isp: z.string().optional(), +}) + +type FilterFormValues = z.infer export default function Edge() { const [data, setData] = useState([]) @@ -23,38 +28,36 @@ export default function Edge() { const [error, setError] = useState(null) // 分页状态 - const [currentPage, setCurrentPage] = useState(1) - const [itemsPerPage, setItemsPerPage] = useState(100) // 默认100条 - const [totalItems, setTotalItems] = useState(0) + const [page, setPage] = useState(1) + const [total, setTotal] = useState(0) + + // 初始化表单 + const form = useForm({ + resolver: zodResolver(filterSchema), + defaultValues: { + macaddr: '', + public: '', + city: '', + isp: '', + }, + }) useEffect(() => { - fetchData() - }, [currentPage, itemsPerPage]) // 监听页码和每页数量的变化 + fetchData({}, page) + }, [page]) - const fetchData = async () => { + const fetchData = async (val: { + macaddr?: string + public?: string + city?: string + isp?: string + }, page: number = 1) => { try { setError(null) setLoading(true) + const result = await getEdgeNodes(val, page) - // 计算偏移量 - const offset = (currentPage - 1) * itemsPerPage - - // if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`) - - const result = await getEdgeNodes(offset, itemsPerPage) - type ResultEdge = { - id: number - macaddr: string - city: string - public: string - isp: string - single: number | boolean - sole: number | boolean - arch: number - online: number - } - - const validatedData = (result.data as ResultEdge[]).map(item => ({ + const validatedData = (result.data).map(item => ({ id: item.id, macaddr: item.macaddr || '', city: item.city || '', @@ -67,7 +70,7 @@ export default function Edge() { })) setData(validatedData) - setTotalItems(result.totalCount || 0) + setTotal(result.totalCount || 0) } catch (error) { console.error('Failed to fetch edge nodes:', error) @@ -78,6 +81,18 @@ export default function Edge() { } } + const onSubmit = async (values: FilterFormValues) => { + setPage(1) + const filters = { + macaddr: values.macaddr || undefined, + public: values.public || undefined, + city: values.city || undefined, + isp: values.isp || undefined, + } + + fetchData(filters, 1) + } + // 多IP节点格式化 const formatMultiIP = (value: number | boolean): string => { if (typeof value === 'number') { @@ -149,28 +164,43 @@ export default function Edge() { // 处理页码变化 const handlePageChange = (page: number) => { - setCurrentPage(page) + setPage(page) + const formValues = form.getValues() + const filters = { + macaddr: formValues.macaddr || '', + public: formValues.public || '', + city: formValues.city || '', + isp: formValues.isp || '', + } + fetchData(filters, page) } // 处理每页显示数量变化 const handleSizeChange = (size: number) => { - setItemsPerPage(size) - setCurrentPage(1) // 重置到第一页 + setPage(1) + const formValues = form.getValues() + const filters = { + macaddr: formValues.macaddr || '', + public: formValues.public || '', + city: formValues.city || '', + isp: formValues.isp || '', + } + fetchData(filters, 1) } if (loading) return ( -
+

节点列表

加载节点数据中...
) if (error) return ( -
+

节点列表

{error}
+ +
+ + +
+ {data.length === 0 ? (
📋
@@ -188,7 +271,7 @@ export default function Edge() { ) : ( <>
-
+
@@ -209,30 +292,29 @@ export default function Edge() { {item.city} {item.public} - + {item.isp} - + {formatMultiIP(item.single)} - + {formatExclusiveIP(item.sole)} - + {formatArchType(item.arch)} @@ -247,9 +329,10 @@ export default function Edge() { {/* 分页 */} ([]) const [loading, setLoading] = useState(false) - const [macAddress, setMacAddress] = useState('') const searchParams = useSearchParams() // 分页状态 const [page, setPage] = useState(1) const [total, setTotal] = useState(0) + // 定义表单验证规则 + const filterSchema = z.object({ + macaddr: z.string().optional(), + public: z.string().optional(), + city: z.string().optional(), + inner_ip: z.string().optional(), + user: z.string().optional(), + }) + + type FilterFormValues = z.infer + // 初始化表单 + const form = useForm({ + resolver: zodResolver(filterSchema), + defaultValues: { + macaddr: '', + public: '', + city: '', + inner_ip: '', + user: '', + }, + }) + const { watch, handleSubmit: formHandleSubmit, setValue } = form + const macaddrValue = watch('macaddr') // 监听URL的mac参数变化 useEffect(() => { const urlMac = searchParams.get('mac') if (urlMac) { - setMacAddress(urlMac) - setPage(1) // 重置到第一页 - fetchData(urlMac, 1) + setValue('macaddr', urlMac) + setPage(1) + fetchData({ mac: urlMac }, 1) } else { - setMacAddress('') - setPage(1) // 重置到第一页 - fetchData('', 1) + setValue('macaddr', '') + setPage(1) + fetchData({}, 1) } - }, [searchParams]) + }, [searchParams, setValue]) - const fetchData = async (mac: string, page: number = 1) => { + const fetchData = async (filters: { + mac?: string + public?: string + city?: string + user?: string + inner_ip?: string + }, page: number = 1) => { setLoading(true) try { - // 计算偏移量 - const result = await getGatewayConfig(page, mac) + const result = await getGatewayConfig(page, filters) + if (!result.success) { throw new Error(result.error || '查询网关配置失败') } @@ -60,22 +94,44 @@ function GatewayConfigContent() { } } - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault() - setPage(1) // 重置到第一页 - fetchData(macAddress, 1) + const onSubmit = (data: FilterFormValues) => { + setPage(1) + const filters = { + mac: data.macaddr || '', + public: data.public || '', + city: data.city || '', + user: data.user || '', + inner_ip: data.inner_ip || '', + } + fetchData(filters, 1) } // 处理页码变化 const handlePageChange = (page: number) => { setPage(page) - fetchData(macAddress, page) + const formValues = form.getValues() + const filters = { + mac: formValues.macaddr || undefined, + public: formValues.public || undefined, + city: formValues.city || undefined, + user: formValues.user || undefined, + inner_ip: formValues.inner_ip || undefined, + } + fetchData(filters, page) } // 处理每页显示数量变化 const handleSizeChange = (size: number) => { setPage(1) - fetchData(macAddress, 1) + const formValues = form.getValues() + const filters = { + mac: formValues.macaddr || undefined, + public: formValues.public || undefined, + city: formValues.city || undefined, + user: formValues.user || undefined, + inner_ip: formValues.inner_ip || undefined, + } + fetchData(filters, 1) } const getStatusBadge = (value: number, trueText: string = '是', falseText: string = '否') => { @@ -98,32 +154,65 @@ function GatewayConfigContent() { } return ( -
-
-
-

网关配置状态

-

查询和管理网关设备的配置信息

-
-
+
{/* 查询表单 */} -
-
- setMacAddress(e.target.value)} - placeholder="输入MAC地址查询" - className="px-4 py-2 h-10 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> - -
- +
+
+ +
+ ( + + MAC地址 + + + )} + /> + ( + + IP地址 + + + )} + /> + ( + + 线路 + + + )} + /> + ( + + 端口号 + + + )} + /> + ( + + 城市 + + + )} + /> + +
+ + +
{loading ? (
@@ -149,15 +238,15 @@ function GatewayConfigContent() { {data.map((item, index) => ( - {item.inner_ip} - {item.user} - {item.city} - {item.edge} - {item.public} - + {item.inner_ip} + {item.user} + {item.city} + {item.edge} + {item.public} + {getStatusBadge(item.ischange, '正常', '更新')} - + {getOnlineStatus(item.isonline)} @@ -186,17 +275,22 @@ function GatewayConfigContent() {
{/* 分页组件 */} - {!macAddress && ( - - )} +
+
+ {!macaddrValue && ( + + )} +
+
+
) : (
@@ -211,7 +305,7 @@ function GatewayConfigContent() { export default function GatewayConfig() { return ( +

加载搜索参数...

diff --git a/src/app/dashboard/components/gatewayinfo.tsx b/src/app/dashboard/components/gatewayinfo.tsx index 1884192..f1bfac7 100644 --- a/src/app/dashboard/components/gatewayinfo.tsx +++ b/src/app/dashboard/components/gatewayinfo.tsx @@ -9,6 +9,7 @@ import { z } from 'zod' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { getGatewayInfo, type GatewayInfo } from '@/actions/stats' +import { Copy, Check } from 'lucide-react' const filterSchema = z.object({ status: z.string(), @@ -16,6 +17,66 @@ const filterSchema = z.object({ type FilterSchema = z.infer +const SmartCopyButton = ({ + data, + mode = 'single', +}: { + data: string | GatewayInfo[] + mode?: 'single' | 'batch' +}) => { + const [isCopied, setIsCopied] = useState(false) + + const handleCopy = async () => { + try { + let textToCopy: string + + if (mode === 'single' && typeof data === 'string') { + textToCopy = data + } + else if (mode === 'batch' && Array.isArray(data)) { + if (data.length === 0) return + textToCopy = data.map(item => item.macaddr).join('\n') + } + else { + return + } + + await navigator.clipboard.writeText(textToCopy) + setIsCopied(true) + setTimeout(() => setIsCopied(false), 2000) + } + catch (err) { + console.error('复制失败:', err) + } + } + + const isBatch = mode === 'batch' + const disabled = isBatch && Array.isArray(data) && data.length === 0 + + return ( + + ) +} + export default function Gatewayinfo() { const [data, setData] = useState([]) const [filteredData, setFilteredData] = useState([]) @@ -85,7 +146,7 @@ export default function Gatewayinfo() { if (loading) { return ( -
+

网关基本信息

加载网关信息中...
@@ -94,7 +155,7 @@ export default function Gatewayinfo() { if (error) { return ( -
+

网关基本信息

{error}
@@ -102,7 +163,7 @@ export default function Gatewayinfo() { } return ( -
+
网关基本信息 @@ -142,7 +203,12 @@ export default function Gatewayinfo() {
- MAC地址 + +
+ MAC地址 + +
+
内网IP 配置版本 状态 @@ -155,14 +221,17 @@ export default function Gatewayinfo() { className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'} > - +
+ + +
{item.inner_ip} {item.setid} diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 6e547ad..b801fe2 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -88,7 +88,7 @@ function DashboardContent() { -
+
-
+
{activeTab === 'gatewayInfo' && } {activeTab === 'gateway' && } {activeTab === 'city' && } diff --git a/src/components/ui/errorCard.tsx b/src/components/ui/errorCard.tsx index b5058a5..87a0975 100644 --- a/src/components/ui/errorCard.tsx +++ b/src/components/ui/errorCard.tsx @@ -8,7 +8,7 @@ export default function ErrorCard({ onRetry: () => void }) { return ( -
+

{title}

加载失败: {error}