重构网关配置总览 & 完善数据表格组件
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
name: jihu-monitor
|
||||
|
||||
services:
|
||||
|
||||
mariadb:
|
||||
|
||||
@@ -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<Res<Page<GatewayConfig>>> {
|
||||
}): Promise<Res<Page<GatewayConfig>>> => {
|
||||
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
|
||||
|
||||
@@ -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<GatewayConfig[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
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)
|
||||
}
|
||||
}
|
||||
export default function GatewayConfigs() {
|
||||
const [gateways, setGateways] = useState<Map<string, GatewayInfo>>(new Map())
|
||||
const [data, setData] = useState<Map<string, Map<string, GatewayConfig | undefined>>>(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<string>()
|
||||
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<string, GatewayInfo>())
|
||||
setGateways(findGateways)
|
||||
|
||||
setInitialMacAddresses(sortedMacAddresses)
|
||||
setMacAddresses(sortedMacAddresses)
|
||||
// 获取网关配置
|
||||
const data = new Map<string, Map<string, GatewayConfig | undefined>>()
|
||||
for (const slot of slots) {
|
||||
data.set(slot, new Map<string, GatewayConfig>())
|
||||
}
|
||||
|
||||
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<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()
|
||||
})
|
||||
}))
|
||||
|
||||
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 (
|
||||
<Page>
|
||||
{/* 查询表单 */}
|
||||
<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 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="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>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<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>
|
||||
</div>
|
||||
<Page className="gap-3">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex items-center">
|
||||
<div className="w-3 h-3 bg-green-500 rounded-full mr-2"></div>
|
||||
<span className="text-sm font-medium">在线 {Array.from(gateways.values()).filter(item => item.enable).length}</span>
|
||||
</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 className="w-3 h-3 bg-red-500 rounded-full mr-2"></div>
|
||||
<span className="text-sm font-medium">离线 {Array.from(gateways.values()).filter(item => !item.enable).length}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="border-r sticky left-0 bg-white z-10">端口</TableHead>
|
||||
{gatewayPairs.map((pair, index) => (
|
||||
<TableHead key={index} className="border-r">
|
||||
<TableHead className="border-r sticky left-0 bg-gray-50">端口</TableHead>
|
||||
{gateways.values().map((pair, index) => (
|
||||
<TableHead key={index} className="border-r h-auto">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="font-medium">{pair.macaddr}</div>
|
||||
<div className="text-xs text-gray-500 mt-1">{pair.inner_ip}</div>
|
||||
<div className="font-medium">{pair.inner_ip}</div>
|
||||
<div className="text-xs text-gray-500 mt-1">{pair.macaddr}</div>
|
||||
</div>
|
||||
</TableHead>
|
||||
))}
|
||||
)).toArray()}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{matrixData.map((row, rowIndex) => (
|
||||
{data.entries().map(([slot, configs], rowIndex) => (
|
||||
<TableRow key={rowIndex}>
|
||||
<TableCell className="border-r sticky left-0 bg-white z-10">{row.inner_ip}</TableCell>
|
||||
{gatewayPairs.map((pair, colIndex) => {
|
||||
const configs = row.devices[pair.macaddr] || []
|
||||
<TableCell className="border-r sticky left-0 bg-background">{slot}</TableCell>
|
||||
{configs.entries().map(([_, config], colIndex) => {
|
||||
if (!config) {
|
||||
return (
|
||||
<TableCell key={colIndex} className="not-last:border-r">
|
||||
-
|
||||
</TableCell>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<TableCell key={`${rowIndex}-${colIndex}`} className="border-r">
|
||||
{configs.length === 0 ? (
|
||||
<div className="text-center">-</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{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 (
|
||||
<div key={itemIndex} className="flex flex-col gap-1">
|
||||
<div className="text-sm font-medium">{item.public || 'N/A'}</div>
|
||||
<div className="text-xs font-medium">{item.city || 'N/A'}</div>
|
||||
<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}`}>
|
||||
{statusConfig.ischange.label}
|
||||
</span>
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${statusConfig.isonline.bg} ${statusConfig.isonline.text}`}>
|
||||
{statusConfig.isonline.label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<TableCell key={`${colIndex}`} className="not-last:border-r">
|
||||
<div key={colIndex} className="flex flex-col gap-1">
|
||||
<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">
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${statusConfig.ischange.bg} ${statusConfig.ischange.text}`}>
|
||||
{statusConfig.ischange.label}
|
||||
</span>
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${statusConfig.isonline.bg} ${statusConfig.isonline.text}`}>
|
||||
{statusConfig.isonline.label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
)
|
||||
})}
|
||||
}).toArray()}
|
||||
</TableRow>
|
||||
))}
|
||||
)).toArray()}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
export default function GatewayConfigs() {
|
||||
return (
|
||||
<Suspense fallback={(
|
||||
<div className="bg-white shadow p-6">
|
||||
<div className="text-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">加载搜索参数...</p>
|
||||
</div>
|
||||
</div>
|
||||
)}>
|
||||
<GatewayConfigContent />
|
||||
</Suspense>
|
||||
)
|
||||
function shrinkCity(city: string) {
|
||||
switch (city) {
|
||||
case '黔东南苗族侗族自治州':
|
||||
return '黔东南'
|
||||
case '延边朝鲜族自治州':
|
||||
return '延边'
|
||||
default:
|
||||
return city
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ export default function DashboardLayout({
|
||||
{/* 主要内容区域 */}
|
||||
<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="/gatewayConfig" active={isActive('/gatewayConfig')}><DoorClosedLockedIcon size={20} />网关配置</NavbarItem>
|
||||
<NavbarItem href="/gatewayMonitor" active={isActive('/gatewayMonitor')}><DoorOpenIcon size={20} />配置总览</NavbarItem>
|
||||
@@ -99,7 +99,7 @@ function NavbarItem(props: {
|
||||
href={props.href}
|
||||
className={cn(
|
||||
'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
|
||||
? 'text-primary bg-primary/10'
|
||||
: 'hover:bg-muted',
|
||||
|
||||
@@ -18,6 +18,7 @@ type Column = {
|
||||
export function DataTable<T extends Data>(props: {
|
||||
data: T[]
|
||||
columns: Column[]
|
||||
pinFirst?: boolean
|
||||
}) {
|
||||
const table = useReactTable({
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
@@ -41,12 +42,13 @@ export function DataTable<T extends Data>(props: {
|
||||
<TableRow key={group.id}>
|
||||
|
||||
{/* 表头 */}
|
||||
{group.headers.map(header => (
|
||||
{group.headers.map((header, index) => (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
className={cn(
|
||||
header.column.columnDef.enableSorting && 'hover:bg-gray-200 transition-colors duration-150 ease-in-out cursor-pointer',
|
||||
header.column.getIsSorted() && 'text-primary',
|
||||
props.pinFirst && index === 0 && 'sticky left-0 bg-gray-50 border-r',
|
||||
)}
|
||||
onClick={header.column.getToggleSortingHandler()}>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
@@ -74,8 +76,12 @@ export function DataTable<T extends Data>(props: {
|
||||
<TableRow key={row.id}>
|
||||
|
||||
{/* 表格 */}
|
||||
{row.getVisibleCells().map(cell => (
|
||||
<TableCell key={cell.id}>
|
||||
{row.getVisibleCells().map((cell, index) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className={cn(
|
||||
props.pinFirst && index === 0 && 'sticky left-0 bg-white border-r',
|
||||
)}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
|
||||
@@ -12,7 +12,7 @@ function Table({ className, ...props }: React.ComponentProps<'table'>) {
|
||||
>
|
||||
<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}
|
||||
/>
|
||||
</div>
|
||||
@@ -23,7 +23,7 @@ function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
|
||||
return (
|
||||
<thead
|
||||
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}
|
||||
/>
|
||||
)
|
||||
@@ -33,7 +33,7 @@ function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
|
||||
return (
|
||||
<tbody
|
||||
data-slot="table-body"
|
||||
className={cn('[&_tr:last-child]:border-0', className)}
|
||||
className={cn('[&>tr:last-child>td]:border-b-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -57,7 +57,7 @@ function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
@@ -71,7 +71,7 @@ function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
|
||||
data-slot="table-head"
|
||||
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-sm text-gray-500',
|
||||
'text-sm text-gray-500 border-b',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -84,7 +84,7 @@ function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user