重构网关配置总览 & 完善数据表格组件
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
name: jihu-monitor
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
mariadb:
|
mariadb:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { Page, Res } from '@/lib/api'
|
import { Page, Res } from '@/lib/api'
|
||||||
import drizzle, { and, change, cityhash, count, desc, edge, eq, gateway, is, sql, token } from '@/lib/drizzle'
|
import drizzle, { and, change, cityhash, count, desc, edge, eq, gateway, is, sql, token } from '@/lib/drizzle'
|
||||||
|
import { cache } from 'react'
|
||||||
|
|
||||||
export type AllocationStatus = {
|
export type AllocationStatus = {
|
||||||
city: string
|
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
|
mac?: string
|
||||||
public?: string
|
public?: string
|
||||||
city?: string
|
city?: string
|
||||||
user?: string
|
user?: string
|
||||||
inner_ip?: string
|
inner_ip?: string
|
||||||
}): Promise<Res<Page<GatewayConfig>>> {
|
}): Promise<Res<Page<GatewayConfig>>> => {
|
||||||
try {
|
try {
|
||||||
if (!page && !filters?.mac) {
|
if (!page && !filters?.mac) {
|
||||||
throw new Error('页码和MAC地址不能同时为空')
|
throw new Error('页码和MAC地址不能同时为空')
|
||||||
@@ -171,7 +172,7 @@ export async function getGatewayConfig(page?: number, filters?: {
|
|||||||
error: '查询网关配置失败',
|
error: '查询网关配置失败',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
export type CityNode = {
|
export type CityNode = {
|
||||||
city: string
|
city: string
|
||||||
|
|||||||
@@ -3,462 +3,130 @@ import { useEffect, useState, Suspense } 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 { getGatewayInfo, getGatewayConfig, type GatewayConfig, type GatewayInfo } from '@/actions/stats'
|
import { getGatewayInfo, getGatewayConfig, type GatewayConfig, type GatewayInfo } from '@/actions/stats'
|
||||||
import { toast } from 'sonner'
|
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'
|
import { Page } from '@/components/page'
|
||||||
|
|
||||||
function GatewayConfigContent() {
|
export default function GatewayConfigs() {
|
||||||
const [data, setData] = useState<GatewayConfig[]>([])
|
const [gateways, setGateways] = useState<Map<string, GatewayInfo>>(new Map())
|
||||||
const [loading, setLoading] = useState(false)
|
const [data, setData] = useState<Map<string, Map<string, GatewayConfig | undefined>>>(new Map())
|
||||||
const [infoData, setInfoData] = useState<GatewayInfo[]>([])
|
|
||||||
const [totalCount, setTotalCount] = useState(0)
|
|
||||||
const [matrixData, setMatrixData] = useState<{ inner_ip: string, devices: { [macaddr: string]: GatewayConfig[] } }[]>([])
|
|
||||||
const [macAddresses, setMacAddresses] = useState<string[]>([])
|
|
||||||
const [currentTotal, setCurrentTotal] = useState(0)
|
|
||||||
// 缓存处理先保存初始MAC地址列表
|
|
||||||
const [initialMacAddresses, setInitialMacAddresses] = useState<string[]>([])
|
|
||||||
// 保存每个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<typeof filterSchema>
|
|
||||||
const form = useForm<FilterFormValues>({
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化数据
|
// 初始化数据
|
||||||
useEffect(() => {
|
|
||||||
const initData = async () => {
|
const initData = async () => {
|
||||||
setLoading(true)
|
const now = Date.now()
|
||||||
try {
|
try {
|
||||||
// 获取网关基本信息
|
// 固定端口信息
|
||||||
const infoResult = await getGatewayInfo()
|
const slots = new Set<string>()
|
||||||
if (!infoResult.success) throw new Error(infoResult.error || '查询网关信息失败')
|
for (let i = 2; i <= 251; i++) {
|
||||||
setInfoData(infoResult.data)
|
slots.add(`172.30.168.${i}`)
|
||||||
|
}
|
||||||
|
|
||||||
// 构建排序后的网关对(MAC+内网IP)
|
// 获取网关信息
|
||||||
const sortedGatewayPairs = infoResult.data
|
const resp = await getGatewayInfo()
|
||||||
.filter(item => item.macaddr && item.inner_ip)
|
if (!resp.success) {
|
||||||
.sort((a, b) => {
|
throw new Error(`查询网关信息失败:${resp.error}`)
|
||||||
const lastOctetA = getLastOctet(a.inner_ip)
|
}
|
||||||
const lastOctetB = getLastOctet(b.inner_ip)
|
|
||||||
return lastOctetA - lastOctetB
|
const gateways = resp.data
|
||||||
|
const findGateways = gateways.reduce((map, gateway) => {
|
||||||
|
map.set(gateway.macaddr, gateway)
|
||||||
|
return map
|
||||||
|
}, new Map<string, GatewayInfo>())
|
||||||
|
setGateways(findGateways)
|
||||||
|
|
||||||
|
// 获取网关配置
|
||||||
|
const data = new Map<string, Map<string, GatewayConfig | undefined>>()
|
||||||
|
for (const slot of slots) {
|
||||||
|
data.set(slot, new Map<string, GatewayConfig>())
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(gateways.map((gateway, index) => {
|
||||||
|
return new Promise<void>(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<string, GatewayConfig>())
|
||||||
|
|
||||||
|
for (const slot of slots) {
|
||||||
|
data.get(slot)!.set(gateway.macaddr, findConfig.get(slot))
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve()
|
||||||
})
|
})
|
||||||
.map(item => ({ macaddr: item.macaddr!, inner_ip: item.inner_ip! }))
|
}))
|
||||||
|
|
||||||
// 加载初始配置
|
setData(data)
|
||||||
setGatewayPairs(sortedGatewayPairs)// 表头数据
|
|
||||||
const sortedMacAddresses = sortedGatewayPairs.map(pair => pair.macaddr)
|
|
||||||
|
|
||||||
setInitialMacAddresses(sortedMacAddresses)
|
|
||||||
setMacAddresses(sortedMacAddresses)
|
|
||||||
|
|
||||||
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] || []
|
|
||||||
})
|
|
||||||
|
|
||||||
setInitialConfigData(initialConfig)
|
|
||||||
setIsInitialDataLoaded(true)
|
|
||||||
|
|
||||||
// 构建初始矩阵
|
|
||||||
await buildMatrixData(sortedGatewayPairs, initialConfig, true)
|
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
toast.error((error as Error).message || '获取数据失败')
|
toast.error(`初始化页面数据失败:${(error as Error).message}`)
|
||||||
}
|
|
||||||
finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
}
|
||||||
|
console.log('初始化数据耗时', Date.now() - now, 'ms')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
initData()
|
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 (
|
return (
|
||||||
<Page>
|
<Page className="gap-3">
|
||||||
{/* 查询表单 */}
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={formHandleSubmit(onSubmit)} className="flex gap-3">
|
|
||||||
<FormField
|
|
||||||
name="macaddr"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>MAC地址</FormLabel>
|
|
||||||
<Input placeholder="输入MAC地址" {...field} />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
name="public"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>IP地址</FormLabel>
|
|
||||||
<Input placeholder="输入IP地址" {...field} />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
name="user"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>线路</FormLabel>
|
|
||||||
<Input placeholder="输入线路" {...field} />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
name="inner_ip"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>端口号</FormLabel>
|
|
||||||
<Input placeholder="输入端口" {...field} />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
name="city"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>城市</FormLabel>
|
|
||||||
<Input placeholder="输入城市" {...field} />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Button type="submit" className="ml-2 mt-6 px-4 py-2 bg-blue-600 hover:bg-blue-700">查询</Button>
|
|
||||||
<Button type="button" variant="outline" className="ml-2 mt-6 px-4 py-2" onClick={() => form.reset()}>重置</Button>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
|
|
||||||
<div className="flex gap-6 p-2 items-center justify-between text-sm">
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-3 h-3 bg-blue-100 border border-blue-300 rounded"></div>
|
|
||||||
<span className="text-xs font-medium">蓝色:在用</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-3 h-3 bg-green-100 border border-green-300 rounded"></div>
|
|
||||||
<span className="text-xs font-medium">绿色:空闲</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-3 h-3 bg-yellow-100 border border-yellow-300 rounded"></div>
|
|
||||||
<span className="text-xs font-medium">黄色:更新</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-4">
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="w-3 h-3 bg-green-500 rounded-full mr-2"></div>
|
<div className="w-3 h-3 bg-green-500 rounded-full mr-2"></div>
|
||||||
<span className="text-sm font-medium">在线 {infoData.filter(item => item.enable === 1).length}</span>
|
<span className="text-sm font-medium">在线 {Array.from(gateways.values()).filter(item => item.enable).length}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="w-3 h-3 bg-red-500 rounded-full mr-2"></div>
|
<div className="w-3 h-3 bg-red-500 rounded-full mr-2"></div>
|
||||||
<span className="text-sm font-medium">离线 {infoData.filter(item => item.enable === 0).length}</span>
|
<span className="text-sm font-medium">离线 {Array.from(gateways.values()).filter(item => !item.enable).length}</span>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="text-sm text-gray-600">
|
|
||||||
当前<span className="font-semibold text-blue-600">{currentTotal}</span> 条 |
|
|
||||||
共 <span className="font-semibold text-blue-600">{totalCount}</span> 条记录
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="border-r sticky left-0 bg-white z-10">端口</TableHead>
|
<TableHead className="border-r sticky left-0 bg-gray-50">端口</TableHead>
|
||||||
{gatewayPairs.map((pair, index) => (
|
{gateways.values().map((pair, index) => (
|
||||||
<TableHead key={index} className="border-r">
|
<TableHead key={index} className="border-r h-auto">
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<div className="font-medium">{pair.macaddr}</div>
|
<div className="font-medium">{pair.inner_ip}</div>
|
||||||
<div className="text-xs text-gray-500 mt-1">{pair.inner_ip}</div>
|
<div className="text-xs text-gray-500 mt-1">{pair.macaddr}</div>
|
||||||
</div>
|
</div>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
))}
|
)).toArray()}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{matrixData.map((row, rowIndex) => (
|
{data.entries().map(([slot, configs], rowIndex) => (
|
||||||
<TableRow key={rowIndex}>
|
<TableRow key={rowIndex}>
|
||||||
<TableCell className="border-r sticky left-0 bg-white z-10">{row.inner_ip}</TableCell>
|
<TableCell className="border-r sticky left-0 bg-background">{slot}</TableCell>
|
||||||
{gatewayPairs.map((pair, colIndex) => {
|
{configs.entries().map(([_, config], colIndex) => {
|
||||||
const configs = row.devices[pair.macaddr] || []
|
if (!config) {
|
||||||
return (
|
return (
|
||||||
<TableCell key={`${rowIndex}-${colIndex}`} className="border-r">
|
<TableCell key={colIndex} className="not-last:border-r">
|
||||||
{configs.length === 0 ? (
|
-
|
||||||
<div className="text-center">-</div>
|
</TableCell>
|
||||||
) : (
|
)
|
||||||
<div className="space-y-2">
|
}
|
||||||
{configs.map((item, itemIndex) => {
|
|
||||||
const statusConfig = {
|
const statusConfig = {
|
||||||
ischange: item.ischange === 0
|
ischange: config.ischange === 0
|
||||||
? { bg: 'bg-green-100', text: 'text-green-800', label: '正常' }
|
? { bg: 'bg-green-100', text: 'text-green-800', label: '正常' }
|
||||||
: { bg: 'bg-yellow-100', text: 'text-yellow-800', label: '更新' },
|
: { bg: 'bg-yellow-100', text: 'text-yellow-800', label: '更新' },
|
||||||
isonline: item.isonline === 0
|
isonline: config.isonline === 0
|
||||||
? { bg: 'bg-green-100', text: 'text-green-800', label: '空闲' }
|
? { bg: 'bg-green-100', text: 'text-green-800', label: '空闲' }
|
||||||
: { bg: 'bg-blue-100', text: 'text-blue-800', label: '在用' },
|
: { bg: 'bg-blue-100', text: 'text-blue-800', label: '在用' },
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div key={itemIndex} className="flex flex-col gap-1">
|
<TableCell key={`${colIndex}`} className="not-last:border-r">
|
||||||
<div className="text-sm font-medium">{item.public || 'N/A'}</div>
|
<div key={colIndex} className="flex flex-col gap-1">
|
||||||
<div className="text-xs font-medium">{item.city || 'N/A'}</div>
|
<div className="text-sm font-medium">{config.public}</div>
|
||||||
|
<div className="text-xs font-medium flex justify-between">
|
||||||
|
<span>{config.user}</span>
|
||||||
|
<span>{shrinkCity(config.city || '?')}</span>
|
||||||
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${statusConfig.ischange.bg} ${statusConfig.ischange.text}`}>
|
<span className={`px-2 py-0.5 rounded text-xs font-medium ${statusConfig.ischange.bg} ${statusConfig.ischange.text}`}>
|
||||||
{statusConfig.ischange.label}
|
{statusConfig.ischange.label}
|
||||||
@@ -468,32 +136,24 @@ function GatewayConfigContent() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)
|
)
|
||||||
})}
|
}).toArray()}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
)).toArray()}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function GatewayConfigs() {
|
function shrinkCity(city: string) {
|
||||||
return (
|
switch (city) {
|
||||||
<Suspense fallback={(
|
case '黔东南苗族侗族自治州':
|
||||||
<div className="bg-white shadow p-6">
|
return '黔东南'
|
||||||
<div className="text-center py-12">
|
case '延边朝鲜族自治州':
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto"></div>
|
return '延边'
|
||||||
<p className="mt-4 text-gray-600">加载搜索参数...</p>
|
default:
|
||||||
</div>
|
return city
|
||||||
</div>
|
}
|
||||||
)}>
|
|
||||||
<GatewayConfigContent />
|
|
||||||
</Suspense>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export default function DashboardLayout({
|
|||||||
{/* 主要内容区域 */}
|
{/* 主要内容区域 */}
|
||||||
<div className="flex-auto overflow-hidden flex">
|
<div className="flex-auto overflow-hidden flex">
|
||||||
{/* 侧边栏 */}
|
{/* 侧边栏 */}
|
||||||
<nav className="flex-none basis-64 p-3 space-y-2 border-r flex flex-col">
|
<nav className="flex-none basis-64 border-r flex flex-col p-3 gap-2">
|
||||||
<NavbarItem href="/gatewayinfo" active={isActive('/gatewayinfo')}><DoorClosedIcon size={20} />网关信息</NavbarItem>
|
<NavbarItem href="/gatewayinfo" active={isActive('/gatewayinfo')}><DoorClosedIcon size={20} />网关信息</NavbarItem>
|
||||||
<NavbarItem href="/gatewayConfig" active={isActive('/gatewayConfig')}><DoorClosedLockedIcon size={20} />网关配置</NavbarItem>
|
<NavbarItem href="/gatewayConfig" active={isActive('/gatewayConfig')}><DoorClosedLockedIcon size={20} />网关配置</NavbarItem>
|
||||||
<NavbarItem href="/gatewayMonitor" active={isActive('/gatewayMonitor')}><DoorOpenIcon size={20} />配置总览</NavbarItem>
|
<NavbarItem href="/gatewayMonitor" active={isActive('/gatewayMonitor')}><DoorOpenIcon size={20} />配置总览</NavbarItem>
|
||||||
@@ -99,7 +99,7 @@ function NavbarItem(props: {
|
|||||||
href={props.href}
|
href={props.href}
|
||||||
className={cn(
|
className={cn(
|
||||||
'transition-colors duration-150 ease-in-out',
|
'transition-colors duration-150 ease-in-out',
|
||||||
'p-2 gap-2 rounded-md text-sm flex items-center',
|
'h-10 rounded-md text-sm flex items-center p-2 gap-2',
|
||||||
props.active
|
props.active
|
||||||
? 'text-primary bg-primary/10'
|
? 'text-primary bg-primary/10'
|
||||||
: 'hover:bg-muted',
|
: 'hover:bg-muted',
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type Column = {
|
|||||||
export function DataTable<T extends Data>(props: {
|
export function DataTable<T extends Data>(props: {
|
||||||
data: T[]
|
data: T[]
|
||||||
columns: Column[]
|
columns: Column[]
|
||||||
|
pinFirst?: boolean
|
||||||
}) {
|
}) {
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
@@ -41,12 +42,13 @@ export function DataTable<T extends Data>(props: {
|
|||||||
<TableRow key={group.id}>
|
<TableRow key={group.id}>
|
||||||
|
|
||||||
{/* 表头 */}
|
{/* 表头 */}
|
||||||
{group.headers.map(header => (
|
{group.headers.map((header, index) => (
|
||||||
<TableHead
|
<TableHead
|
||||||
key={header.id}
|
key={header.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
header.column.columnDef.enableSorting && 'hover:bg-gray-200 transition-colors duration-150 ease-in-out cursor-pointer',
|
header.column.columnDef.enableSorting && 'hover:bg-gray-200 transition-colors duration-150 ease-in-out cursor-pointer',
|
||||||
header.column.getIsSorted() && 'text-primary',
|
header.column.getIsSorted() && 'text-primary',
|
||||||
|
props.pinFirst && index === 0 && 'sticky left-0 bg-gray-50 border-r',
|
||||||
)}
|
)}
|
||||||
onClick={header.column.getToggleSortingHandler()}>
|
onClick={header.column.getToggleSortingHandler()}>
|
||||||
<div className="flex flex-row items-center justify-between">
|
<div className="flex flex-row items-center justify-between">
|
||||||
@@ -74,8 +76,12 @@ export function DataTable<T extends Data>(props: {
|
|||||||
<TableRow key={row.id}>
|
<TableRow key={row.id}>
|
||||||
|
|
||||||
{/* 表格 */}
|
{/* 表格 */}
|
||||||
{row.getVisibleCells().map(cell => (
|
{row.getVisibleCells().map((cell, index) => (
|
||||||
<TableCell key={cell.id}>
|
<TableCell
|
||||||
|
key={cell.id}
|
||||||
|
className={cn(
|
||||||
|
props.pinFirst && index === 0 && 'sticky left-0 bg-white border-r',
|
||||||
|
)}>
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ function Table({ className, ...props }: React.ComponentProps<'table'>) {
|
|||||||
>
|
>
|
||||||
<table
|
<table
|
||||||
data-slot="table"
|
data-slot="table"
|
||||||
className={cn('w-full caption-bottom text-sm', className)}
|
className={cn('w-full caption-bottom text-sm border-separate border-spacing-0', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -23,7 +23,7 @@ function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
|
|||||||
return (
|
return (
|
||||||
<thead
|
<thead
|
||||||
data-slot="table-header"
|
data-slot="table-header"
|
||||||
className={cn('sticky top-0 bg-gray-50', className)}
|
className={cn('sticky top-0 bg-gray-50 z-10', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -33,7 +33,7 @@ function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
|
|||||||
return (
|
return (
|
||||||
<tbody
|
<tbody
|
||||||
data-slot="table-body"
|
data-slot="table-body"
|
||||||
className={cn('[&_tr:last-child]:border-0', className)}
|
className={cn('[&>tr:last-child>td]:border-b-0', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -57,7 +57,7 @@ function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
|
|||||||
<tr
|
<tr
|
||||||
data-slot="table-row"
|
data-slot="table-row"
|
||||||
className={cn(
|
className={cn(
|
||||||
'hover:data-[state=selected]:bg-muted border-b border-border/50 transition-colors',
|
'hover:data-[state=selected]:bg-muted border-border/50 transition-colors',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -71,7 +71,7 @@ function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
|
|||||||
data-slot="table-head"
|
data-slot="table-head"
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
'text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||||
'text-sm text-gray-500',
|
'text-sm text-gray-500 border-b',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -84,7 +84,7 @@ function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
|
|||||||
<td
|
<td
|
||||||
data-slot="table-cell"
|
data-slot="table-cell"
|
||||||
className={cn(
|
className={cn(
|
||||||
'p-2 h-10 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
'p-2 h-10 border-b align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
Reference in New Issue
Block a user