diff --git a/docker-compose.yaml b/docker-compose.yaml index 00f329f..b8aa4a2 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,3 +1,5 @@ +name: jihu-monitor + services: mariadb: diff --git a/src/actions/stats.ts b/src/actions/stats.ts index 06f778f..ece86a3 100644 --- a/src/actions/stats.ts +++ b/src/actions/stats.ts @@ -2,6 +2,7 @@ import { Page, Res } from '@/lib/api' import drizzle, { and, change, cityhash, count, desc, edge, eq, gateway, is, sql, token } from '@/lib/drizzle' +import { cache } from 'react' export type AllocationStatus = { city: string @@ -105,13 +106,13 @@ export type GatewayConfig = { } // 网关配置 -export async function getGatewayConfig(page?: number, filters?: { +export const getGatewayConfig = cache(async (page?: number, filters?: { mac?: string public?: string city?: string user?: string inner_ip?: string -}): Promise>> { +}): Promise>> => { try { if (!page && !filters?.mac) { throw new Error('页码和MAC地址不能同时为空') @@ -171,7 +172,7 @@ export async function getGatewayConfig(page?: number, filters?: { error: '查询网关配置失败', } } -} +}) export type CityNode = { city: string diff --git a/src/app/(root)/gatewayMonitor/page.tsx b/src/app/(root)/gatewayMonitor/page.tsx index 5c5779f..84c2cbf 100644 --- a/src/app/(root)/gatewayMonitor/page.tsx +++ b/src/app/(root)/gatewayMonitor/page.tsx @@ -3,497 +3,157 @@ import { useEffect, useState, Suspense } from 'react' import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table' import { getGatewayInfo, getGatewayConfig, type GatewayConfig, type GatewayInfo } from '@/actions/stats' import { toast } from 'sonner' -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 { Page } from '@/components/page' -function GatewayConfigContent() { - const [data, setData] = useState([]) - const [loading, setLoading] = useState(false) - const [infoData, setInfoData] = useState([]) - const [totalCount, setTotalCount] = useState(0) - const [matrixData, setMatrixData] = useState<{ inner_ip: string, devices: { [macaddr: string]: GatewayConfig[] } }[]>([]) - const [macAddresses, setMacAddresses] = useState([]) - const [currentTotal, setCurrentTotal] = useState(0) - // 缓存处理先保存初始MAC地址列表 - const [initialMacAddresses, setInitialMacAddresses] = useState([]) - // 保存每个MAC地址对应的完整配置数据 { mac1: [config1, config2], mac2: [config3] } - const [initialConfigData, setInitialConfigData] = useState<{ [mac: string]: GatewayConfig[] }>({}) - // 初始数据是否已加载完成 - const [isInitialDataLoaded, setIsInitialDataLoaded] = useState(false) - // MAC 地址 - 内网 IP - const [gatewayPairs, setGatewayPairs] = useState<{ macaddr: string, inner_ip: string }[]>([]) - - // 表单验证规则 - 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 { handleSubmit: formHandleSubmit } = form - - // 处理城市名称缩写 - const processCityNames = (items: GatewayConfig[]) => { - const shrink = ['黔东南', '延边'] - return items.map((item) => { - const processedItem = { ...item } - shrink.forEach((s) => { - if (processedItem.city?.startsWith(s)) { - processedItem.city = s - } - }) - return processedItem - }) - } - - // 获取IP最后一段用于排序 - const getLastOctet = (ip: string | undefined): number => { - if (!ip) return 0 - const parts = ip.split('.') - if (parts.length !== 4) return 0 - const last = parseInt(parts[3], 10) - return isNaN(last) ? 0 : last - } - - // 按内网IP排序配置 - const sortByInnerIp = (a: GatewayConfig, b: GatewayConfig): number => { - const lastOctetA = getLastOctet(a.inner_ip) - const lastOctetB = getLastOctet(b.inner_ip) - return lastOctetA - lastOctetB - } - - // 为单个MAC获取配置数据 - const fetchConfigForMac = async (mac: string, filters: { - mac?: string | undefined - public?: string | undefined - city?: string | undefined - user?: string | undefined - inner_ip?: string | undefined - }) => { - try { - const queryParams = { ...filters, mac } - const result = await getGatewayConfig(1, queryParams) - if (result.success) { - const processedData = processCityNames(result.data.items) - return [...processedData].sort(sortByInnerIp) - } - return [] - } - catch (error) { - console.error(`获取MAC ${mac} 配置失败:`, error) - return [] - } - } - - // 本地筛选数据 - const filterLocalData = (filters: { - mac?: string - public?: string - city?: string - user?: string - inner_ip?: string - }) => { - const { mac, public: publicIp, city, user, inner_ip } = filters - let filteredPairs = [...gatewayPairs] - - // 按MAC筛选 - if (mac) { - filteredPairs = filteredPairs.filter(pair => - pair.macaddr.toLowerCase().includes(mac.toLowerCase()), - ) - } - - // 按内网IP筛选 - if (inner_ip) { - filteredPairs = filteredPairs.filter(pair => - pair.inner_ip.includes(inner_ip), - ) - } - - // 筛选配置数据 - const filteredConfigs: { [mac: string]: GatewayConfig[] } = {} - filteredPairs.forEach((pair) => { - const configs = initialConfigData[pair.macaddr] || [] - const filtered = configs.filter((config) => { - // 按各个字段筛选 - const matchPublic = !publicIp || (config.public && config.public.includes(publicIp)) - const matchCity = !city || (config.city && config.city.includes(city)) - const matchUser = !user || (config.user && config.user.includes(user)) - const matchInnerIp = !inner_ip || (config.inner_ip && config.inner_ip.includes(inner_ip)) - return matchPublic && matchCity && matchUser && matchInnerIp - }) - if (filtered.length > 0) { - filteredConfigs[pair.macaddr] = filtered - } - }) - - // 筛选后按内网IP重新排序,确保表头与列顺序一致 - const sortedFilteredPairs = filteredPairs - .filter(pair => filteredConfigs[pair.macaddr]?.length > 0) - .sort((a, b) => { - const lastOctetA = getLastOctet(a.inner_ip) - const lastOctetB = getLastOctet(b.inner_ip) - return lastOctetA - lastOctetB - }) - - return { filteredPairs: sortedFilteredPairs, filteredConfigs } - } - - // 构建矩阵数据 - const buildMatrixData = async ( - gatewayPairsList: { macaddr: string, inner_ip: string }[], - configData: { [mac: string]: GatewayConfig[] } = {}, - useLocalData: boolean = false, - ) => { - setLoading(true) - try { - let macConfigMap: { [mac: string]: GatewayConfig[] } = {} - const macList = gatewayPairsList.map(pair => pair.macaddr) - - // 加载配置数据 - if (useLocalData) { - // 使用本地数据 - macConfigMap = configData - } - else { - // 调用接口获取数据 - const configPromises = macList.map(mac => fetchConfigForMac(mac, {})) - const configResults = await Promise.all(configPromises) - - // 创建MAC地址到配置数据的映射 - macList.forEach((mac, index) => { - macConfigMap[mac] = configResults[index] || [] - }) - } - - // 获取所有端口并排序 - const allPortLines = Array.from( - new Set( - Object.values(macConfigMap) - .flat() - .map(item => item.inner_ip) - .filter(Boolean), - ), - ).sort((a, b) => { - const portA = getLastOctet(a.split('|')[0]) - const portB = getLastOctet(b.split('|')[0]) - return portA - portB - }) - - // 构建矩阵 - const matrix = allPortLines.map((portLine) => { - const [inner_ip] = portLine.split('|') - const row = { inner_ip: inner_ip || '', devices: {} as { [macaddr: string]: GatewayConfig[] } } - - // 遍历排序后的网关对,双重校验配置归属 - gatewayPairsList.forEach((pair) => { - const configsForPortLine = macConfigMap[pair.macaddr] - .filter(item => item.inner_ip === inner_ip) // 匹配当前行端口 - .sort(sortByInnerIp) - row.devices[pair.macaddr] = configsForPortLine - }) - - return row - }) - - // 更新状态 - setMatrixData(matrix) - setGatewayPairs(gatewayPairsList) - setCurrentTotal(matrix.length) - setTotalCount(Object.values(macConfigMap).reduce((sum, configs) => sum + configs.length, 0)) - } - catch (error) { - toast.error('构建矩阵数据失败') - console.error('矩阵构建错误:', error) - } - finally { - setLoading(false) - } - } +export default function GatewayConfigs() { + const [gateways, setGateways] = useState>(new Map()) + const [data, setData] = useState>>(new Map()) // 初始化数据 - useEffect(() => { - const initData = async () => { - setLoading(true) - try { - // 获取网关基本信息 - const infoResult = await getGatewayInfo() - if (!infoResult.success) throw new Error(infoResult.error || '查询网关信息失败') - setInfoData(infoResult.data) + const initData = async () => { + const now = Date.now() + try { + // 固定端口信息 + const slots = new Set() + for (let i = 2; i <= 251; i++) { + slots.add(`172.30.168.${i}`) + } - // 构建排序后的网关对(MAC+内网IP) - const sortedGatewayPairs = infoResult.data - .filter(item => item.macaddr && item.inner_ip) - .sort((a, b) => { - const lastOctetA = getLastOctet(a.inner_ip) - const lastOctetB = getLastOctet(b.inner_ip) - return lastOctetA - lastOctetB - }) - .map(item => ({ macaddr: item.macaddr!, inner_ip: item.inner_ip! })) + // 获取网关信息 + const resp = await getGatewayInfo() + if (!resp.success) { + throw new Error(`查询网关信息失败:${resp.error}`) + } - // 加载初始配置 - setGatewayPairs(sortedGatewayPairs)// 表头数据 - const sortedMacAddresses = sortedGatewayPairs.map(pair => pair.macaddr) + const gateways = resp.data + const findGateways = gateways.reduce((map, gateway) => { + map.set(gateway.macaddr, gateway) + return map + }, new Map()) + setGateways(findGateways) - setInitialMacAddresses(sortedMacAddresses) - setMacAddresses(sortedMacAddresses) + // 获取网关配置 + const data = new Map>() + for (const slot of slots) { + data.set(slot, new Map()) + } - const configPromises = sortedMacAddresses.map(mac => fetchConfigForMac(mac, {})) - const configResults = await Promise.all(configPromises) - const initialConfig: { [mac: string]: GatewayConfig[] } = {} - sortedMacAddresses.forEach((mac, index) => { - initialConfig[mac] = configResults[index] || [] + await Promise.all(gateways.map((gateway, index) => { + return new Promise(async (resolve) => { + const resp = await getGatewayConfig(1, { mac: gateway.macaddr }) + if (!resp.success) { + throw new Error(`查询网关 ${gateway.inner_ip} 配置失败:${resp.error}`) + } + + const configs = resp.data.items + const findConfig = configs.reduce((map, config) => { + map.set(config.inner_ip, config) + return map + }, new Map()) + + for (const slot of slots) { + data.get(slot)!.set(gateway.macaddr, findConfig.get(slot)) + } + + resolve() }) + })) - setInitialConfigData(initialConfig) - setIsInitialDataLoaded(true) - - // 构建初始矩阵 - await buildMatrixData(sortedGatewayPairs, initialConfig, true) - } - catch (error) { - toast.error((error as Error).message || '获取数据失败') - } - finally { - setLoading(false) - } + setData(data) } + catch (error) { + toast.error(`初始化页面数据失败:${(error as Error).message}`) + } + console.log('初始化数据耗时', Date.now() - now, 'ms') + } + + useEffect(() => { initData() }, []) - // 查询数据表单查询 - const fetchData = async (filters: { - mac?: string - public?: string - city?: string - user?: string - inner_ip?: string - }, page: number = 1) => { - setLoading(true) - try { - // 如果有初始数据,就本地筛选 - if (isInitialDataLoaded) { - const { filteredPairs, filteredConfigs } = filterLocalData(filters) - setData(Object.values(filteredConfigs).flat()) - setTotalCount(Object.values(filteredConfigs).flat().length) - await buildMatrixData(filteredPairs, filteredConfigs, true) - } - else { - // 如果没有初始数据,就接口查询 - const result = await getGatewayConfig(page, filters) - if (!result.success) throw new Error(result.error || '查询网关配置失败') - const processedData = processCityNames(result.data.items) - const sortedData = [...processedData].sort(sortByInnerIp) - setData(sortedData) - setTotalCount(result.data.total) - - const filteredMacs = Array.from(new Set(sortedData.map(item => item.edge).filter(Boolean))).sort() as string[] - const temporaryGatewayPairs = filteredMacs - .map(mac => ({ macaddr: mac, inner_ip: '' })) - .sort((a, b) => a.macaddr.localeCompare(b.macaddr)) - - await buildMatrixData(temporaryGatewayPairs, {}, false) - } - } - catch (error) { - toast.error((error as Error).message || '获取网关配置失败') - } - finally { - setLoading(false) - } - } - - // 提交查询 - const onSubmit = async (formData: FilterFormValues) => { - const filters = { - mac: formData.macaddr || '', - public: formData.public || '', - city: formData.city || '', - user: formData.user || '', - inner_ip: formData.inner_ip || '', - } - await fetchData(filters, 1) - } - return ( - - {/* 查询表单 */} -
- - ( - - MAC地址 - - - )} - /> - ( - - IP地址 - - - )} - /> - ( - - 线路 - - - )} - /> - ( - - 端口号 - - - )} - /> - ( - - 城市 - - - )} - /> - - - - - -
-
-
-
- 蓝色:在用 -
-
-
- 绿色:空闲 -
-
-
- 黄色:更新 -
-
-
-
-
- 在线 {infoData.filter(item => item.enable === 1).length} -
-
-
- 离线 {infoData.filter(item => item.enable === 0).length} -
+ +
+
+
+ 在线 {Array.from(gateways.values()).filter(item => item.enable).length}
-
- 当前{currentTotal} 条 | - 共 {totalCount} 条记录 -
+
+ 离线 {Array.from(gateways.values()).filter(item => !item.enable).length}
- 端口 - {gatewayPairs.map((pair, index) => ( - + 端口 + {gateways.values().map((pair, index) => ( +
-
{pair.macaddr}
-
{pair.inner_ip}
+
{pair.inner_ip}
+
{pair.macaddr}
- ))} + )).toArray()}
- {matrixData.map((row, rowIndex) => ( + {data.entries().map(([slot, configs], rowIndex) => ( - {row.inner_ip} - {gatewayPairs.map((pair, colIndex) => { - const configs = row.devices[pair.macaddr] || [] + {slot} + {configs.entries().map(([_, config], colIndex) => { + if (!config) { + return ( + + - + + ) + } + + const statusConfig = { + ischange: config.ischange === 0 + ? { bg: 'bg-green-100', text: 'text-green-800', label: '正常' } + : { bg: 'bg-yellow-100', text: 'text-yellow-800', label: '更新' }, + isonline: config.isonline === 0 + ? { bg: 'bg-green-100', text: 'text-green-800', label: '空闲' } + : { bg: 'bg-blue-100', text: 'text-blue-800', label: '在用' }, + } return ( - - {configs.length === 0 ? ( -
-
- ) : ( -
- {configs.map((item, itemIndex) => { - const statusConfig = { - ischange: item.ischange === 0 - ? { bg: 'bg-green-100', text: 'text-green-800', label: '正常' } - : { bg: 'bg-yellow-100', text: 'text-yellow-800', label: '更新' }, - isonline: item.isonline === 0 - ? { bg: 'bg-green-100', text: 'text-green-800', label: '空闲' } - : { bg: 'bg-blue-100', text: 'text-blue-800', label: '在用' }, - } - return ( -
-
{item.public || 'N/A'}
-
{item.city || 'N/A'}
-
- - {statusConfig.ischange.label} - - - {statusConfig.isonline.label} - -
-
- ) - })} + +
+
{config.public}
+
+ {config.user} + {shrinkCity(config.city || '?')}
- )} +
+ + {statusConfig.ischange.label} + + + {statusConfig.isonline.label} + +
+
) - })} + }).toArray()} - ))} + )).toArray()}
) } -export default function GatewayConfigs() { - return ( - -
-
-

加载搜索参数...

-
-
- )}> - - - ) +function shrinkCity(city: string) { + switch (city) { + case '黔东南苗族侗族自治州': + return '黔东南' + case '延边朝鲜族自治州': + return '延边' + default: + return city + } } diff --git a/src/app/(root)/layout.tsx b/src/app/(root)/layout.tsx index a461e0e..a52354b 100644 --- a/src/app/(root)/layout.tsx +++ b/src/app/(root)/layout.tsx @@ -70,7 +70,7 @@ export default function DashboardLayout({ {/* 主要内容区域 */}
{/* 侧边栏 */} -