Browse Source
- 新增网络栈配置管理页面(RAN配置、IMS配置、核心网络配置、网络栈配置) - 将网络栈配置从仪表管理中分离,创建独立的路由组和菜单项 - 添加网络栈配置相关权限到AuthContext - 更新路由配置和菜单配置 - 完善修改记录文档feature/x1-web-request
31 changed files with 8516 additions and 610 deletions
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -1,251 +0,0 @@ |
|||
import React from 'react'; |
|||
import { Button } from '@/components/ui/button'; |
|||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; |
|||
import { Config, ConfigType } from '@/services/configService'; |
|||
import { Badge } from '@/components/ui/badge'; |
|||
import { EyeOpenIcon, CheckIcon, PlayIcon } from '@radix-ui/react-icons'; |
|||
|
|||
interface ConfigsTableProps { |
|||
configs: Config[]; |
|||
loading: boolean; |
|||
onView: (config: Config) => void; |
|||
onValidate: (config: Config) => void; |
|||
onApply: (config: Config) => void; |
|||
page: number; |
|||
pageSize: number; |
|||
total: number; |
|||
onPageChange: (page: number) => void; |
|||
hideCard?: boolean; |
|||
density?: 'compact' | 'default' | 'comfortable'; |
|||
columns?: { key: string; title: string; visible: boolean }[]; |
|||
} |
|||
|
|||
// 配置类型徽章组件
|
|||
const ConfigTypeBadge: React.FC<{ type: ConfigType }> = ({ type }) => { |
|||
const typeConfig = { |
|||
system: { label: '系统配置', className: 'bg-blue-100 text-blue-800' }, |
|||
network: { label: '网络配置', className: 'bg-green-100 text-green-800' }, |
|||
security: { label: '安全配置', className: 'bg-red-100 text-red-800' }, |
|||
communication: { label: '通信配置', className: 'bg-purple-100 text-purple-800' }, |
|||
monitoring: { label: '监控配置', className: 'bg-orange-100 text-orange-800' }, |
|||
}; |
|||
|
|||
const config = typeConfig[type]; |
|||
return ( |
|||
<Badge className={config.className}> |
|||
{config.label} |
|||
</Badge> |
|||
); |
|||
}; |
|||
|
|||
// 状态徽章组件
|
|||
const StatusBadge: React.FC<{ isActive: boolean }> = ({ isActive }) => { |
|||
return ( |
|||
<Badge className={isActive ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}> |
|||
{isActive ? '启用' : '禁用'} |
|||
</Badge> |
|||
); |
|||
}; |
|||
|
|||
// 默认配置徽章组件
|
|||
const DefaultConfigBadge: React.FC<{ isDefault: boolean }> = ({ isDefault }) => { |
|||
return ( |
|||
<Badge className={isDefault ? 'bg-blue-100 text-blue-800' : 'bg-gray-100 text-gray-800'}> |
|||
{isDefault ? '默认' : '自定义'} |
|||
</Badge> |
|||
); |
|||
}; |
|||
|
|||
// 网络设置显示组件
|
|||
const NetworkSettingsDisplay: React.FC<{ settings: Config['networkSettings'] }> = ({ settings }) => { |
|||
return ( |
|||
<div className="text-xs space-y-1"> |
|||
<div className="font-mono">{settings.ipAddress}:{settings.port}</div> |
|||
<div className="text-gray-500">网关: {settings.gateway}</div> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
// 安全设置显示组件
|
|||
const SecuritySettingsDisplay: React.FC<{ settings: Config['securitySettings'] }> = ({ settings }) => { |
|||
return ( |
|||
<div className="text-xs space-y-1"> |
|||
<div className="flex items-center gap-1"> |
|||
<Badge className={settings.encryptionEnabled ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}> |
|||
{settings.encryptionEnabled ? '加密' : '未加密'} |
|||
</Badge> |
|||
<Badge className={settings.sslEnabled ? 'bg-blue-100 text-blue-800' : 'bg-gray-100 text-gray-800'}> |
|||
{settings.sslEnabled ? 'SSL' : '无SSL'} |
|||
</Badge> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
// 通信设置显示组件
|
|||
const CommunicationSettingsDisplay: React.FC<{ settings: Config['communicationSettings'] }> = ({ settings }) => { |
|||
return ( |
|||
<div className="text-xs space-y-1"> |
|||
<div>{settings.protocol}</div> |
|||
<div className="text-gray-500">超时: {settings.timeout}ms</div> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
// 监控设置显示组件
|
|||
const MonitoringSettingsDisplay: React.FC<{ settings: Config['monitoringSettings'] }> = ({ settings }) => { |
|||
return ( |
|||
<div className="text-xs space-y-1"> |
|||
<div>间隔: {settings.dataCollectionInterval}s</div> |
|||
<div className="text-gray-500">日志: {settings.logLevel}</div> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default function ConfigsTable({ |
|||
configs, |
|||
loading, |
|||
onView, |
|||
onValidate, |
|||
onApply, |
|||
page, |
|||
pageSize, |
|||
total, |
|||
onPageChange, |
|||
hideCard = false, |
|||
density = 'default', |
|||
columns = [] |
|||
}: ConfigsTableProps) { |
|||
const densityClasses = { |
|||
compact: 'py-1', |
|||
default: 'py-2', |
|||
comfortable: 'py-3', |
|||
}; |
|||
|
|||
const visibleColumns = columns.filter(col => col.visible); |
|||
|
|||
const renderCell = (config: Config, columnKey: string) => { |
|||
switch (columnKey) { |
|||
case 'configId': |
|||
return ( |
|||
<div className="font-medium"> |
|||
{config.configId} |
|||
</div> |
|||
); |
|||
case 'configName': |
|||
return ( |
|||
<div className="max-w-xs truncate" title={config.configName}> |
|||
{config.configName} |
|||
</div> |
|||
); |
|||
case 'configType': |
|||
return <ConfigTypeBadge type={config.configType} />; |
|||
case 'isActive': |
|||
return <StatusBadge isActive={config.isActive} />; |
|||
case 'isDefault': |
|||
return <DefaultConfigBadge isDefault={config.isDefault} />; |
|||
case 'version': |
|||
return <span>{config.version}</span>; |
|||
case 'networkSettings': |
|||
return <NetworkSettingsDisplay settings={config.networkSettings} />; |
|||
case 'securitySettings': |
|||
return <SecuritySettingsDisplay settings={config.securitySettings} />; |
|||
case 'communicationSettings': |
|||
return <CommunicationSettingsDisplay settings={config.communicationSettings} />; |
|||
case 'monitoringSettings': |
|||
return <MonitoringSettingsDisplay settings={config.monitoringSettings} />; |
|||
case 'appliedTo': |
|||
return <span>{config.appliedTo.length} 台设备</span>; |
|||
case 'createdBy': |
|||
return <span>{config.createdBy}</span>; |
|||
case 'actions': |
|||
return ( |
|||
<div className="flex items-center gap-2"> |
|||
<Button |
|||
variant="ghost" |
|||
size="sm" |
|||
onClick={() => onView(config)} |
|||
className="h-8 w-8 p-0" |
|||
> |
|||
<EyeOpenIcon className="h-4 w-4" /> |
|||
</Button> |
|||
<Button |
|||
variant="ghost" |
|||
size="sm" |
|||
onClick={() => onValidate(config)} |
|||
className="h-8 w-8 p-0" |
|||
> |
|||
<CheckIcon className="h-4 w-4" /> |
|||
</Button> |
|||
<Button |
|||
variant="ghost" |
|||
size="sm" |
|||
onClick={() => onApply(config)} |
|||
className="h-8 w-8 p-0" |
|||
disabled={!config.isActive} |
|||
> |
|||
<PlayIcon className="h-4 w-4" /> |
|||
</Button> |
|||
</div> |
|||
); |
|||
default: |
|||
return null; |
|||
} |
|||
}; |
|||
|
|||
if (loading) { |
|||
return ( |
|||
<div className="flex items-center justify-center py-8"> |
|||
<div className="text-center"> |
|||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div> |
|||
<p className="mt-2 text-sm text-muted-foreground">加载中...</p> |
|||
</div> |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
if (configs.length === 0) { |
|||
return ( |
|||
<div className="flex items-center justify-center py-8"> |
|||
<div className="text-center"> |
|||
<p className="text-sm text-muted-foreground">暂无配置数据</p> |
|||
</div> |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
const tableContent = ( |
|||
<Table> |
|||
<TableHeader> |
|||
<TableRow> |
|||
{visibleColumns.map((column) => ( |
|||
<TableHead key={column.key} className={densityClasses[density]}> |
|||
{column.title} |
|||
</TableHead> |
|||
))} |
|||
</TableRow> |
|||
</TableHeader> |
|||
<TableBody> |
|||
{configs.map((config) => ( |
|||
<TableRow key={config.id} className={densityClasses[density]}> |
|||
{visibleColumns.map((column) => ( |
|||
<TableCell key={column.key}> |
|||
{renderCell(config, column.key)} |
|||
</TableCell> |
|||
))} |
|||
</TableRow> |
|||
))} |
|||
</TableBody> |
|||
</Table> |
|||
); |
|||
|
|||
if (hideCard) { |
|||
return tableContent; |
|||
} |
|||
|
|||
return ( |
|||
<div className="rounded-md border"> |
|||
{tableContent} |
|||
</div> |
|||
); |
|||
} |
@ -1,217 +0,0 @@ |
|||
import React, { useEffect, useState } from 'react'; |
|||
import { configService, Config } from '@/services/configService'; |
|||
import ConfigsTable from './ConfigsTable'; |
|||
import { Input } from '@/components/ui/input'; |
|||
import PaginationBar from '@/components/ui/PaginationBar'; |
|||
import TableToolbar, { DensityType } from '@/components/ui/TableToolbar'; |
|||
import { ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons'; |
|||
|
|||
const defaultColumns = [ |
|||
{ key: 'configId', title: '配置ID', visible: true }, |
|||
{ key: 'configName', title: '配置名称', visible: true }, |
|||
{ key: 'configType', title: '配置类型', visible: true }, |
|||
{ key: 'isActive', title: '状态', visible: true }, |
|||
{ key: 'isDefault', title: '默认配置', visible: true }, |
|||
{ key: 'version', title: '版本', visible: true }, |
|||
{ key: 'networkSettings', title: '网络设置', visible: true }, |
|||
{ key: 'securitySettings', title: '安全设置', visible: true }, |
|||
{ key: 'communicationSettings', title: '通信设置', visible: true }, |
|||
{ key: 'monitoringSettings', title: '监控设置', visible: true }, |
|||
{ key: 'appliedTo', title: '应用设备数', visible: true }, |
|||
{ key: 'createdBy', title: '创建人', visible: true }, |
|||
{ key: 'actions', title: '操作', visible: true } |
|||
]; |
|||
|
|||
// 字段类型声明
|
|||
type SearchField = |
|||
| { key: string; label: string; type: 'input'; placeholder: string } |
|||
| { key: string; label: string; type: 'select'; options: { value: string; label: string }[] }; |
|||
|
|||
// 第一行字段(收起时只显示这3个)
|
|||
const firstRowFields: SearchField[] = [ |
|||
{ key: 'configId', label: '配置ID', type: 'input', placeholder: '请输入' }, |
|||
{ key: 'configType', label: '配置类型', type: 'select', options: [ |
|||
{ value: '', label: '请选择' }, |
|||
{ value: 'system', label: '系统配置' }, |
|||
{ value: 'network', label: '网络配置' }, |
|||
{ value: 'security', label: '安全配置' }, |
|||
{ value: 'communication', label: '通信配置' }, |
|||
{ value: 'monitoring', label: '监控配置' }, |
|||
] }, |
|||
{ key: 'isActive', label: '状态', type: 'select', options: [ |
|||
{ value: '', label: '请选择' }, |
|||
{ value: 'true', label: '启用' }, |
|||
{ value: 'false', label: '禁用' }, |
|||
] }, |
|||
]; |
|||
|
|||
// 高级字段(展开时才显示)
|
|||
const advancedFields: SearchField[] = [ |
|||
{ key: 'isDefault', label: '默认配置', type: 'select', options: [ |
|||
{ value: '', label: '请选择' }, |
|||
{ value: 'true', label: '是' }, |
|||
{ value: 'false', label: '否' }, |
|||
] }, |
|||
{ key: 'createdBy', label: '创建人', type: 'input', placeholder: '请输入' }, |
|||
]; |
|||
|
|||
export default function ConfigsView() { |
|||
const [configs, setConfigs] = useState<Config[]>([]); |
|||
const [loading, setLoading] = useState(false); |
|||
const [total, setTotal] = useState(0); |
|||
const [configId, setConfigId] = useState(''); |
|||
const [page, setPage] = useState(1); |
|||
const [pageSize, setPageSize] = useState(10); |
|||
const [density, setDensity] = useState<DensityType>('default'); |
|||
const [columns, setColumns] = useState(defaultColumns); |
|||
const [showAdvanced, setShowAdvanced] = useState(false); |
|||
|
|||
const fetchConfigs = async (params = {}) => { |
|||
setLoading(true); |
|||
const result = await configService.getAllConfigs({ configId, page, pageSize, ...params }); |
|||
if (result.isSuccess && result.data) { |
|||
setConfigs(result.data.configs || []); |
|||
setTotal(result.data.totalCount || 0); |
|||
} |
|||
setLoading(false); |
|||
}; |
|||
|
|||
useEffect(() => { |
|||
fetchConfigs(); |
|||
// eslint-disable-next-line
|
|||
}, [page, pageSize]); |
|||
|
|||
const handleView = (config: Config) => { |
|||
// 这里可以实现查看配置详情的逻辑
|
|||
console.log('查看配置:', config); |
|||
}; |
|||
|
|||
const handleValidate = (config: Config) => { |
|||
// 这里可以实现验证配置的逻辑
|
|||
console.log('验证配置:', config); |
|||
}; |
|||
|
|||
const handleApply = (config: Config) => { |
|||
// 这里可以实现应用配置的逻辑
|
|||
console.log('应用配置:', config); |
|||
}; |
|||
|
|||
// 查询按钮
|
|||
const handleQuery = () => { |
|||
setPage(1); |
|||
fetchConfigs({ page: 1 }); |
|||
}; |
|||
|
|||
// 重置按钮
|
|||
const handleReset = () => { |
|||
setConfigId(''); |
|||
setPage(1); |
|||
fetchConfigs({ configId: '', page: 1 }); |
|||
}; |
|||
|
|||
// 每页条数选择
|
|||
const handlePageSizeChange = (size: number) => { |
|||
setPageSize(size); |
|||
setPage(1); |
|||
}; |
|||
|
|||
const totalPages = Math.ceil(total / pageSize); |
|||
|
|||
return ( |
|||
<main className="flex-1 p-4 transition-all duration-300 ease-in-out sm:p-6"> |
|||
<div className="w-full space-y-4"> |
|||
{/* 丰富美化后的搜索栏 */} |
|||
<div className="flex flex-col bg-white p-4 rounded-md border mb-2"> |
|||
<form |
|||
className={`grid gap-x-8 gap-y-4 items-center ${showAdvanced ? 'md:grid-cols-3' : 'md:grid-cols-4'} grid-cols-1`} |
|||
onSubmit={e => { |
|||
e.preventDefault(); |
|||
handleQuery(); |
|||
}} |
|||
> |
|||
{(showAdvanced ? [...firstRowFields, ...advancedFields] : firstRowFields).map(field => ( |
|||
<div className="flex flex-row items-center min-w-[200px] flex-1" key={field.key}> |
|||
<label |
|||
className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right" |
|||
style={{ width: 80, minWidth: 80 }} |
|||
> |
|||
{field.label}: |
|||
</label> |
|||
{field.type === 'input' && ( |
|||
<Input |
|||
className="input flex-1" |
|||
placeholder={field.placeholder} |
|||
value={field.key === 'configId' ? configId : ''} |
|||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { |
|||
if (field.key === 'configId') setConfigId(e.target.value); |
|||
}} |
|||
/> |
|||
)} |
|||
{field.type === 'select' && ( |
|||
<select className="input h-10 rounded border border-border bg-background px-3 text-sm flex-1"> |
|||
{field.options.map(opt => ( |
|||
<option value={opt.value} key={opt.value}>{opt.label}</option> |
|||
))} |
|||
</select> |
|||
)} |
|||
</div> |
|||
))} |
|||
{/* 按钮组直接作为表单项之一,紧跟在最后一个表单项后面 */} |
|||
<div className="flex flex-row items-center min-w-[200px] flex-1 justify-end gap-2"> |
|||
<button type="button" className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" onClick={handleReset}>重置</button> |
|||
<button type="submit" className="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700">查询</button> |
|||
<button type="button" className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" onClick={() => setShowAdvanced(v => !v)}> |
|||
{showAdvanced ? ( |
|||
<> |
|||
收起 <ChevronUpIcon className="inline-block ml-1 w-4 h-4 align-middle" /> |
|||
</> |
|||
) : ( |
|||
<> |
|||
展开 <ChevronDownIcon className="inline-block ml-1 w-4 h-4 align-middle" /> |
|||
</> |
|||
)} |
|||
</button> |
|||
</div> |
|||
</form> |
|||
</div> |
|||
{/* 表格整体卡片区域,包括工具栏、表格、分页 */} |
|||
<div className="rounded-md border bg-background p-4"> |
|||
{/* 顶部工具栏 */} |
|||
<div className="flex items-center justify-end mb-2"> |
|||
<TableToolbar |
|||
onRefresh={() => fetchConfigs()} |
|||
onDensityChange={setDensity} |
|||
onColumnsChange={setColumns} |
|||
onColumnsReset={() => setColumns(defaultColumns)} |
|||
columns={columns} |
|||
density={density} |
|||
/> |
|||
</div> |
|||
{/* 表格区域 */} |
|||
<ConfigsTable |
|||
configs={configs} |
|||
loading={loading} |
|||
onView={handleView} |
|||
onValidate={handleValidate} |
|||
onApply={handleApply} |
|||
page={page} |
|||
pageSize={pageSize} |
|||
total={total} |
|||
onPageChange={setPage} |
|||
hideCard={true} |
|||
density={density} |
|||
columns={columns} |
|||
/> |
|||
{/* 分页 */} |
|||
<PaginationBar |
|||
page={page} |
|||
pageSize={pageSize} |
|||
total={total} |
|||
onPageChange={setPage} |
|||
onPageSizeChange={handlePageSizeChange} |
|||
/> |
|||
</div> |
|||
</div> |
|||
</main> |
|||
); |
|||
} |
@ -0,0 +1,90 @@ |
|||
import React from 'react'; |
|||
import { Button } from '@/components/ui/button'; |
|||
import { Input } from '@/components/ui/input'; |
|||
import { Label } from '@/components/ui/label'; |
|||
import { Textarea } from '@/components/ui/textarea'; |
|||
import { Checkbox } from '@/components/ui/checkbox'; |
|||
import { CreateCoreNetworkConfigRequest, UpdateCoreNetworkConfigRequest, CoreNetworkConfig } from '@/services/coreNetworkConfigService'; |
|||
|
|||
interface CoreNetworkConfigFormProps { |
|||
onSubmit: (data: CreateCoreNetworkConfigRequest | UpdateCoreNetworkConfigRequest) => void; |
|||
initialData?: Partial<CoreNetworkConfig>; |
|||
isEdit?: boolean; |
|||
isSubmitting?: boolean; |
|||
} |
|||
|
|||
export default function CoreNetworkConfigForm({ onSubmit, initialData, isEdit = false, isSubmitting = false }: CoreNetworkConfigFormProps) { |
|||
const [formData, setFormData] = React.useState<CreateCoreNetworkConfigRequest>({ |
|||
name: initialData?.name || '', |
|||
configContent: initialData?.configContent || '', |
|||
description: initialData?.description || '', |
|||
isDisabled: initialData?.isDisabled ?? false |
|||
}); |
|||
|
|||
const handleSubmit = (e: React.FormEvent) => { |
|||
e.preventDefault(); |
|||
if (isSubmitting) return; // 防止重复提交
|
|||
onSubmit(formData); |
|||
}; |
|||
|
|||
return ( |
|||
<form onSubmit={handleSubmit} className="space-y-4"> |
|||
<div className="space-y-2"> |
|||
<Label htmlFor="name">配置名称</Label> |
|||
<Input |
|||
id="name" |
|||
value={formData.name} |
|||
onChange={e => setFormData({ ...formData, name: e.target.value })} |
|||
placeholder="请输入配置名称" |
|||
required |
|||
disabled={isSubmitting} |
|||
/> |
|||
</div> |
|||
|
|||
<div className="space-y-2"> |
|||
<Label htmlFor="configContent">配置内容 (JSON格式)</Label> |
|||
<Textarea |
|||
id="configContent" |
|||
value={formData.configContent} |
|||
onChange={e => setFormData({ ...formData, configContent: e.target.value })} |
|||
placeholder="请输入JSON格式的配置内容" |
|||
rows={8} |
|||
required |
|||
disabled={isSubmitting} |
|||
className="font-mono text-sm" |
|||
/> |
|||
<p className="text-xs text-gray-500"> |
|||
请输入有效的JSON格式配置内容 |
|||
</p> |
|||
</div> |
|||
|
|||
<div className="space-y-2"> |
|||
<Label htmlFor="description">描述</Label> |
|||
<Textarea |
|||
id="description" |
|||
value={formData.description} |
|||
onChange={e => setFormData({ ...formData, description: e.target.value })} |
|||
placeholder="请输入配置描述" |
|||
rows={3} |
|||
disabled={isSubmitting} |
|||
/> |
|||
</div> |
|||
|
|||
<div className="flex items-center space-x-2"> |
|||
<Checkbox |
|||
id="isDisabled" |
|||
checked={formData.isDisabled} |
|||
onCheckedChange={(checked) => |
|||
setFormData({ ...formData, isDisabled: checked as boolean }) |
|||
} |
|||
disabled={isSubmitting} |
|||
/> |
|||
<Label htmlFor="isDisabled">禁用此配置</Label> |
|||
</div> |
|||
|
|||
<Button type="submit" className="w-full" disabled={isSubmitting}> |
|||
{isSubmitting ? '提交中...' : (isEdit ? '更新核心网络配置' : '创建核心网络配置')} |
|||
</Button> |
|||
</form> |
|||
); |
|||
} |
@ -0,0 +1,173 @@ |
|||
import React from 'react'; |
|||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; |
|||
import { CoreNetworkConfig } from '@/services/coreNetworkConfigService'; |
|||
import { Badge } from '@/components/ui/badge'; |
|||
|
|||
interface CoreNetworkConfigsTableProps { |
|||
coreNetworkConfigs: CoreNetworkConfig[]; |
|||
loading: boolean; |
|||
onEdit: (config: CoreNetworkConfig) => void; |
|||
onDelete: (config: CoreNetworkConfig) => void; |
|||
page: number; |
|||
pageSize: number; |
|||
total: number; |
|||
onPageChange: (page: number) => void; |
|||
hideCard?: boolean; |
|||
density?: 'relaxed' | 'default' | 'compact'; |
|||
columns?: { key: string; title: string; visible: boolean }[]; |
|||
} |
|||
|
|||
// 状态徽章组件
|
|||
const StatusBadge: React.FC<{ isDisabled: boolean }> = ({ isDisabled }) => { |
|||
return ( |
|||
<Badge className={!isDisabled ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}> |
|||
{!isDisabled ? '启用' : '禁用'} |
|||
</Badge> |
|||
); |
|||
}; |
|||
|
|||
// 日期格式化组件
|
|||
const DateDisplay: React.FC<{ date?: string }> = ({ date }) => { |
|||
if (!date) return <span className="text-gray-400">-</span>; |
|||
|
|||
const formatDate = (dateString: string) => { |
|||
const date = new Date(dateString); |
|||
return date.toLocaleDateString('zh-CN', { |
|||
year: 'numeric', |
|||
month: '2-digit', |
|||
day: '2-digit', |
|||
hour: '2-digit', |
|||
minute: '2-digit' |
|||
}); |
|||
}; |
|||
|
|||
return <span>{formatDate(date)}</span>; |
|||
}; |
|||
|
|||
// 配置内容预览组件
|
|||
const ConfigContentPreview: React.FC<{ content: string }> = ({ content }) => { |
|||
const maxLength = 50; |
|||
const truncated = content.length > maxLength ? content.substring(0, maxLength) + '...' : content; |
|||
|
|||
return ( |
|||
<div className="max-w-xs truncate text-gray-600" title={content}> |
|||
{truncated} |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default function CoreNetworkConfigsTable({ |
|||
coreNetworkConfigs, |
|||
loading, |
|||
onEdit, |
|||
onDelete, |
|||
page, |
|||
pageSize, |
|||
total, |
|||
onPageChange, |
|||
hideCard = false, |
|||
density = 'default', |
|||
columns = [] |
|||
}: CoreNetworkConfigsTableProps) { |
|||
const densityClasses = { |
|||
relaxed: 'py-3', |
|||
default: 'py-2', |
|||
compact: 'py-1', |
|||
}; |
|||
|
|||
const visibleColumns = columns.filter(col => col.visible); |
|||
|
|||
const renderCell = (config: CoreNetworkConfig, columnKey: string) => { |
|||
switch (columnKey) { |
|||
case 'name': |
|||
return ( |
|||
<div className="max-w-xs truncate" title={config.name}> |
|||
{config.name} |
|||
</div> |
|||
); |
|||
case 'description': |
|||
return ( |
|||
<div className="max-w-xs truncate text-gray-600" title={config.description || ''}> |
|||
{config.description || '-'} |
|||
</div> |
|||
); |
|||
case 'configContent': |
|||
return <ConfigContentPreview content={config.configContent} />; |
|||
case 'isDisabled': |
|||
return <StatusBadge isDisabled={config.isDisabled} />; |
|||
case 'createdAt': |
|||
return <DateDisplay date={config.createdAt} />; |
|||
case 'updatedAt': |
|||
return <DateDisplay date={config.updatedAt} />; |
|||
case 'actions': |
|||
return ( |
|||
<div className="flex justify-end gap-4"> |
|||
<span |
|||
className="cursor-pointer text-blue-600 hover:underline select-none" |
|||
onClick={() => onEdit(config)} |
|||
> |
|||
修改 |
|||
</span> |
|||
<span |
|||
className="cursor-pointer text-red-500 hover:underline select-none" |
|||
onClick={() => onDelete(config)} |
|||
> |
|||
删除 |
|||
</span> |
|||
</div> |
|||
); |
|||
default: |
|||
return null; |
|||
} |
|||
}; |
|||
|
|||
const totalPages = Math.ceil(total / pageSize); |
|||
const Wrapper = hideCard ? React.Fragment : 'div'; |
|||
const wrapperProps = hideCard ? {} : { className: 'rounded-md border bg-background' }; |
|||
const rowClass = density === 'relaxed' ? 'h-20' : density === 'compact' ? 'h-8' : 'h-12'; |
|||
const cellPadding = density === 'relaxed' ? 'py-5' : density === 'compact' ? 'py-1' : 'py-3'; |
|||
|
|||
return ( |
|||
<Wrapper {...wrapperProps}> |
|||
<Table> |
|||
<TableHeader key="header"> |
|||
<TableRow className={rowClass}> |
|||
{visibleColumns.map(col => ( |
|||
<TableHead |
|||
key={col.key} |
|||
className={`text-foreground text-center ${col.key === 'actions' ? 'text-right' : ''} ${cellPadding}`} |
|||
> |
|||
{col.title} |
|||
</TableHead> |
|||
))} |
|||
</TableRow> |
|||
</TableHeader> |
|||
<TableBody key="body"> |
|||
{loading ? ( |
|||
<TableRow key="loading" className={rowClass}> |
|||
<TableCell colSpan={visibleColumns.length} className={`text-center text-muted-foreground ${cellPadding}`}> |
|||
加载中... |
|||
</TableCell> |
|||
</TableRow> |
|||
) : coreNetworkConfigs.length === 0 ? ( |
|||
<TableRow key="empty" className={rowClass}> |
|||
<TableCell colSpan={visibleColumns.length} className={`text-center text-muted-foreground ${cellPadding}`}> |
|||
暂无数据 |
|||
</TableCell> |
|||
</TableRow> |
|||
) : ( |
|||
coreNetworkConfigs.map((config) => ( |
|||
<TableRow key={config.coreNetworkConfigId} className={rowClass}> |
|||
{visibleColumns.map((column) => ( |
|||
<TableCell key={column.key} className={`text-foreground text-center ${cellPadding}`}> |
|||
{renderCell(config, column.key)} |
|||
</TableCell> |
|||
))} |
|||
</TableRow> |
|||
)) |
|||
)} |
|||
</TableBody> |
|||
</Table> |
|||
</Wrapper> |
|||
); |
|||
} |
@ -0,0 +1,356 @@ |
|||
import React, { useEffect, useState } from 'react'; |
|||
import { coreNetworkConfigService, CoreNetworkConfig, GetCoreNetworkConfigsRequest, CreateCoreNetworkConfigRequest, UpdateCoreNetworkConfigRequest } from '@/services/coreNetworkConfigService'; |
|||
import CoreNetworkConfigsTable from './CoreNetworkConfigsTable'; |
|||
import CoreNetworkConfigForm from './CoreNetworkConfigForm'; |
|||
import { Input } from '@/components/ui/input'; |
|||
import PaginationBar from '@/components/ui/PaginationBar'; |
|||
import TableToolbar, { DensityType } from '@/components/ui/TableToolbar'; |
|||
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog'; |
|||
import { Button } from '@/components/ui/button'; |
|||
import { ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons'; |
|||
import { useToast } from '@/components/ui/use-toast'; |
|||
|
|||
const defaultColumns = [ |
|||
{ key: 'name', title: '配置名称', visible: true }, |
|||
{ key: 'description', title: '描述', visible: true }, |
|||
{ key: 'configContent', title: '配置内容', visible: false }, |
|||
{ key: 'isDisabled', title: '状态', visible: true }, |
|||
{ key: 'createdAt', title: '创建时间', visible: true }, |
|||
{ key: 'actions', title: '操作', visible: true } |
|||
]; |
|||
|
|||
// 字段类型声明
|
|||
type SearchField = |
|||
| { key: string; label: string; type: 'input'; placeholder: string } |
|||
| { key: string; label: string; type: 'select'; options: { value: string; label: string }[] }; |
|||
|
|||
// 第一行字段(收起时只显示这3个)
|
|||
const firstRowFields: SearchField[] = [ |
|||
{ key: 'searchTerm', label: '搜索关键词', type: 'input', placeholder: '请输入配置名称或描述' }, |
|||
{ key: 'isDisabled', label: '状态', type: 'select', options: [ |
|||
{ value: '', label: '请选择' }, |
|||
{ value: 'false', label: '启用' }, |
|||
{ value: 'true', label: '禁用' }, |
|||
] }, |
|||
]; |
|||
|
|||
// 高级字段(展开时才显示)
|
|||
const advancedFields: SearchField[] = [ |
|||
{ key: 'pageSize', label: '每页数量', type: 'select', options: [ |
|||
{ value: '10', label: '10条/页' }, |
|||
{ value: '20', label: '20条/页' }, |
|||
{ value: '50', label: '50条/页' }, |
|||
{ value: '100', label: '100条/页' }, |
|||
] }, |
|||
]; |
|||
|
|||
export default function CoreNetworkConfigsView() { |
|||
const [coreNetworkConfigs, setCoreNetworkConfigs] = useState<CoreNetworkConfig[]>([]); |
|||
const [loading, setLoading] = useState(false); |
|||
const [total, setTotal] = useState(0); |
|||
const [pageNumber, setPageNumber] = useState(1); |
|||
const [pageSize, setPageSize] = useState(10); |
|||
const [density, setDensity] = useState<DensityType>('default'); |
|||
const [columns, setColumns] = useState(defaultColumns); |
|||
const [showAdvanced, setShowAdvanced] = useState(false); |
|||
|
|||
// 搜索参数
|
|||
const [searchTerm, setSearchTerm] = useState(''); |
|||
const [isDisabled, setIsDisabled] = useState<boolean | undefined>(undefined); |
|||
|
|||
// 表单对话框状态
|
|||
const [open, setOpen] = useState(false); |
|||
const [editOpen, setEditOpen] = useState(false); |
|||
const [selectedConfig, setSelectedConfig] = useState<CoreNetworkConfig | null>(null); |
|||
|
|||
// 提交状态
|
|||
const [isSubmitting, setIsSubmitting] = useState(false); |
|||
|
|||
// Toast 提示
|
|||
const { toast } = useToast(); |
|||
|
|||
const fetchCoreNetworkConfigs = async (params: Partial<GetCoreNetworkConfigsRequest> = {}) => { |
|||
setLoading(true); |
|||
const queryParams: GetCoreNetworkConfigsRequest = { |
|||
pageNumber, |
|||
pageSize, |
|||
searchTerm, |
|||
isDisabled, |
|||
...params |
|||
}; |
|||
|
|||
const result = await coreNetworkConfigService.getCoreNetworkConfigs(queryParams); |
|||
if (result.isSuccess && result.data) { |
|||
setCoreNetworkConfigs(result.data.items || []); |
|||
setTotal(result.data.totalCount || 0); |
|||
} |
|||
setLoading(false); |
|||
}; |
|||
|
|||
useEffect(() => { |
|||
fetchCoreNetworkConfigs(); |
|||
// eslint-disable-next-line
|
|||
}, [pageNumber, pageSize]); |
|||
|
|||
const handleEdit = (config: CoreNetworkConfig) => { |
|||
setSelectedConfig(config); |
|||
setEditOpen(true); |
|||
}; |
|||
|
|||
const handleDelete = async (config: CoreNetworkConfig) => { |
|||
if (confirm(`确定要删除核心网络配置 "${config.name}" 吗?`)) { |
|||
try { |
|||
const result = await coreNetworkConfigService.deleteCoreNetworkConfig(config.coreNetworkConfigId); |
|||
if (result.isSuccess) { |
|||
toast({ |
|||
title: "删除成功", |
|||
description: `核心网络配置 "${config.name}" 删除成功`, |
|||
}); |
|||
fetchCoreNetworkConfigs(); |
|||
} else { |
|||
const errorMessage = result.errorMessages?.join(', ') || "删除核心网络配置时发生错误"; |
|||
console.error('删除核心网络配置失败:', errorMessage); |
|||
toast({ |
|||
title: "删除失败", |
|||
description: errorMessage, |
|||
variant: "destructive", |
|||
}); |
|||
} |
|||
} catch (error) { |
|||
console.error('删除核心网络配置异常:', error); |
|||
toast({ |
|||
title: "删除失败", |
|||
description: "网络错误,请稍后重试", |
|||
variant: "destructive", |
|||
}); |
|||
} |
|||
} |
|||
}; |
|||
|
|||
const handleCreate = async (data: CreateCoreNetworkConfigRequest) => { |
|||
if (isSubmitting) return; // 防止重复提交
|
|||
|
|||
console.log('开始创建核心网络配置:', data); |
|||
setIsSubmitting(true); |
|||
try { |
|||
const result = await coreNetworkConfigService.createCoreNetworkConfig(data); |
|||
console.log('创建核心网络配置结果:', result); |
|||
|
|||
if (result.isSuccess) { |
|||
toast({ |
|||
title: "创建成功", |
|||
description: `核心网络配置 "${data.name}" 创建成功`, |
|||
}); |
|||
setOpen(false); |
|||
fetchCoreNetworkConfigs(); |
|||
} else { |
|||
const errorMessage = result.errorMessages?.join(', ') || "创建核心网络配置时发生错误"; |
|||
console.error('创建核心网络配置失败:', errorMessage, result); |
|||
toast({ |
|||
title: "创建失败", |
|||
description: errorMessage, |
|||
variant: "destructive", |
|||
}); |
|||
} |
|||
} catch (error) { |
|||
console.error('创建核心网络配置异常:', error); |
|||
toast({ |
|||
title: "创建失败", |
|||
description: "网络错误,请稍后重试", |
|||
variant: "destructive", |
|||
}); |
|||
} finally { |
|||
setIsSubmitting(false); |
|||
} |
|||
}; |
|||
|
|||
const handleUpdate = async (data: UpdateCoreNetworkConfigRequest) => { |
|||
if (!selectedConfig || isSubmitting) return; // 防止重复提交
|
|||
|
|||
setIsSubmitting(true); |
|||
try { |
|||
const result = await coreNetworkConfigService.updateCoreNetworkConfig(selectedConfig.coreNetworkConfigId, data); |
|||
if (result.isSuccess) { |
|||
toast({ |
|||
title: "更新成功", |
|||
description: `核心网络配置 "${data.name}" 更新成功`, |
|||
}); |
|||
setEditOpen(false); |
|||
setSelectedConfig(null); |
|||
fetchCoreNetworkConfigs(); |
|||
} else { |
|||
const errorMessage = result.errorMessages?.join(', ') || "更新核心网络配置时发生错误"; |
|||
console.error('更新核心网络配置失败:', errorMessage); |
|||
toast({ |
|||
title: "更新失败", |
|||
description: errorMessage, |
|||
variant: "destructive", |
|||
}); |
|||
} |
|||
} catch (error) { |
|||
console.error('更新核心网络配置异常:', error); |
|||
toast({ |
|||
title: "更新失败", |
|||
description: "网络错误,请稍后重试", |
|||
variant: "destructive", |
|||
}); |
|||
} finally { |
|||
setIsSubmitting(false); |
|||
} |
|||
}; |
|||
|
|||
// 查询按钮
|
|||
const handleQuery = () => { |
|||
setPageNumber(1); |
|||
fetchCoreNetworkConfigs({ pageNumber: 1 }); |
|||
}; |
|||
|
|||
// 重置按钮
|
|||
const handleReset = () => { |
|||
setSearchTerm(''); |
|||
setIsDisabled(undefined); |
|||
setPageNumber(1); |
|||
fetchCoreNetworkConfigs({ |
|||
searchTerm: '', |
|||
isDisabled: undefined, |
|||
pageNumber: 1 |
|||
}); |
|||
}; |
|||
|
|||
// 每页条数选择
|
|||
const handlePageSizeChange = (size: number) => { |
|||
setPageSize(size); |
|||
setPageNumber(1); |
|||
}; |
|||
|
|||
const totalPages = Math.ceil(total / pageSize); |
|||
|
|||
return ( |
|||
<main className="flex-1 p-4 transition-all duration-300 ease-in-out sm:p-6"> |
|||
<div className="w-full space-y-4"> |
|||
{/* 丰富美化后的搜索栏 */} |
|||
<div className="flex flex-col bg-white p-4 rounded-md border mb-2"> |
|||
<form |
|||
className={`grid gap-x-8 gap-y-4 items-center ${showAdvanced ? 'md:grid-cols-3' : 'md:grid-cols-3'} grid-cols-1`} |
|||
onSubmit={e => { |
|||
e.preventDefault(); |
|||
handleQuery(); |
|||
}} |
|||
> |
|||
{(showAdvanced ? [...firstRowFields, ...advancedFields] : firstRowFields).map(field => ( |
|||
<div className="flex flex-row items-center min-w-[200px] flex-1" key={field.key}> |
|||
<label |
|||
className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right" |
|||
style={{ width: 80, minWidth: 80 }} |
|||
> |
|||
{field.label}: |
|||
</label> |
|||
{field.type === 'input' && ( |
|||
<Input |
|||
className="input flex-1" |
|||
placeholder={field.placeholder} |
|||
value={field.key === 'searchTerm' ? searchTerm : ''} |
|||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { |
|||
if (field.key === 'searchTerm') setSearchTerm(e.target.value); |
|||
}} |
|||
/> |
|||
)} |
|||
{field.type === 'select' && ( |
|||
<select |
|||
className="input h-10 rounded border border-border bg-background px-3 text-sm flex-1" |
|||
value={field.key === 'isDisabled' ? (isDisabled === undefined ? '' : isDisabled.toString()) : |
|||
field.key === 'pageSize' ? pageSize.toString() : ''} |
|||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => { |
|||
if (field.key === 'isDisabled') { |
|||
const value = e.target.value; |
|||
setIsDisabled(value === '' ? undefined : value === 'true'); |
|||
} else if (field.key === 'pageSize') { |
|||
setPageSize(parseInt(e.target.value)); |
|||
} |
|||
}} |
|||
> |
|||
{field.options.map(opt => ( |
|||
<option value={opt.value} key={opt.value}>{opt.label}</option> |
|||
))} |
|||
</select> |
|||
)} |
|||
</div> |
|||
))} |
|||
{/* 按钮组直接作为表单项之一,紧跟在最后一个表单项后面 */} |
|||
<div className="flex flex-row items-center min-w-[200px] flex-1 justify-end gap-2"> |
|||
<button type="button" className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" onClick={handleReset}>重置</button> |
|||
<button type="submit" className="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700">查询</button> |
|||
<button type="button" className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" onClick={() => setShowAdvanced(v => !v)}> |
|||
{showAdvanced ? ( |
|||
<> |
|||
收起 <ChevronUpIcon className="inline-block ml-1 w-4 h-4 align-middle" /> |
|||
</> |
|||
) : ( |
|||
<> |
|||
展开 <ChevronDownIcon className="inline-block ml-1 w-4 h-4 align-middle" /> |
|||
</> |
|||
)} |
|||
</button> |
|||
</div> |
|||
</form> |
|||
</div> |
|||
{/* 表格整体卡片区域,包括工具栏、表格、分页 */} |
|||
<div className="rounded-md border bg-background p-4"> |
|||
{/* 顶部操作栏:添加核心网络配置+工具栏 */} |
|||
<div className="flex items-center justify-between mb-2"> |
|||
<Dialog open={open} onOpenChange={setOpen}> |
|||
<DialogTrigger asChild> |
|||
<Button className="bg-primary text-primary-foreground hover:bg-primary/90">+ 添加核心网络配置</Button> |
|||
</DialogTrigger> |
|||
<DialogContent className="bg-background"> |
|||
<CoreNetworkConfigForm onSubmit={handleCreate} isSubmitting={isSubmitting} /> |
|||
</DialogContent> |
|||
</Dialog> |
|||
<TableToolbar |
|||
onRefresh={() => fetchCoreNetworkConfigs()} |
|||
onDensityChange={setDensity} |
|||
onColumnsChange={setColumns} |
|||
onColumnsReset={() => setColumns(defaultColumns)} |
|||
columns={columns} |
|||
density={density} |
|||
/> |
|||
</div> |
|||
{/* 表格区域 */} |
|||
<CoreNetworkConfigsTable |
|||
coreNetworkConfigs={coreNetworkConfigs} |
|||
loading={loading} |
|||
onEdit={handleEdit} |
|||
onDelete={handleDelete} |
|||
page={pageNumber} |
|||
pageSize={pageSize} |
|||
total={total} |
|||
onPageChange={setPageNumber} |
|||
hideCard={true} |
|||
density={density} |
|||
columns={columns} |
|||
/> |
|||
{/* 分页 */} |
|||
<PaginationBar |
|||
page={pageNumber} |
|||
pageSize={pageSize} |
|||
total={total} |
|||
onPageChange={setPageNumber} |
|||
onPageSizeChange={handlePageSizeChange} |
|||
/> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* 编辑核心网络配置对话框 */} |
|||
<Dialog open={editOpen} onOpenChange={setEditOpen}> |
|||
<DialogContent className="bg-background"> |
|||
<CoreNetworkConfigForm |
|||
onSubmit={(data: CreateCoreNetworkConfigRequest | UpdateCoreNetworkConfigRequest) => handleUpdate(data as UpdateCoreNetworkConfigRequest)} |
|||
initialData={selectedConfig || undefined} |
|||
isEdit={true} |
|||
isSubmitting={isSubmitting} |
|||
/> |
|||
</DialogContent> |
|||
</Dialog> |
|||
</main> |
|||
); |
|||
} |
@ -0,0 +1,90 @@ |
|||
import React from 'react'; |
|||
import { Button } from '@/components/ui/button'; |
|||
import { Input } from '@/components/ui/input'; |
|||
import { Label } from '@/components/ui/label'; |
|||
import { Textarea } from '@/components/ui/textarea'; |
|||
import { Checkbox } from '@/components/ui/checkbox'; |
|||
import { CreateIMSConfigurationRequest, UpdateIMSConfigurationRequest, IMSConfiguration } from '@/services/imsConfigurationService'; |
|||
|
|||
interface IMSConfigurationFormProps { |
|||
onSubmit: (data: CreateIMSConfigurationRequest | UpdateIMSConfigurationRequest) => void; |
|||
initialData?: Partial<IMSConfiguration>; |
|||
isEdit?: boolean; |
|||
isSubmitting?: boolean; |
|||
} |
|||
|
|||
export default function IMSConfigurationForm({ onSubmit, initialData, isEdit = false, isSubmitting = false }: IMSConfigurationFormProps) { |
|||
const [formData, setFormData] = React.useState<CreateIMSConfigurationRequest>({ |
|||
name: initialData?.name || '', |
|||
configContent: initialData?.configContent || '', |
|||
description: initialData?.description || '', |
|||
isDisabled: initialData?.isDisabled ?? false |
|||
}); |
|||
|
|||
const handleSubmit = (e: React.FormEvent) => { |
|||
e.preventDefault(); |
|||
if (isSubmitting) return; // 防止重复提交
|
|||
onSubmit(formData); |
|||
}; |
|||
|
|||
return ( |
|||
<form onSubmit={handleSubmit} className="space-y-4"> |
|||
<div className="space-y-2"> |
|||
<Label htmlFor="name">配置名称</Label> |
|||
<Input |
|||
id="name" |
|||
value={formData.name} |
|||
onChange={e => setFormData({ ...formData, name: e.target.value })} |
|||
placeholder="请输入配置名称" |
|||
required |
|||
disabled={isSubmitting} |
|||
/> |
|||
</div> |
|||
|
|||
<div className="space-y-2"> |
|||
<Label htmlFor="configContent">配置内容 (JSON格式)</Label> |
|||
<Textarea |
|||
id="configContent" |
|||
value={formData.configContent} |
|||
onChange={e => setFormData({ ...formData, configContent: e.target.value })} |
|||
placeholder="请输入JSON格式的配置内容" |
|||
rows={8} |
|||
required |
|||
disabled={isSubmitting} |
|||
className="font-mono text-sm" |
|||
/> |
|||
<p className="text-xs text-gray-500"> |
|||
请输入有效的JSON格式配置内容 |
|||
</p> |
|||
</div> |
|||
|
|||
<div className="space-y-2"> |
|||
<Label htmlFor="description">描述</Label> |
|||
<Textarea |
|||
id="description" |
|||
value={formData.description} |
|||
onChange={e => setFormData({ ...formData, description: e.target.value })} |
|||
placeholder="请输入配置描述" |
|||
rows={3} |
|||
disabled={isSubmitting} |
|||
/> |
|||
</div> |
|||
|
|||
<div className="flex items-center space-x-2"> |
|||
<Checkbox |
|||
id="isDisabled" |
|||
checked={formData.isDisabled} |
|||
onCheckedChange={(checked) => |
|||
setFormData({ ...formData, isDisabled: checked as boolean }) |
|||
} |
|||
disabled={isSubmitting} |
|||
/> |
|||
<Label htmlFor="isDisabled">禁用此配置</Label> |
|||
</div> |
|||
|
|||
<Button type="submit" className="w-full" disabled={isSubmitting}> |
|||
{isSubmitting ? '提交中...' : (isEdit ? '更新IMS配置' : '创建IMS配置')} |
|||
</Button> |
|||
</form> |
|||
); |
|||
} |
@ -0,0 +1,173 @@ |
|||
import React from 'react'; |
|||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; |
|||
import { IMSConfiguration } from '@/services/imsConfigurationService'; |
|||
import { Badge } from '@/components/ui/badge'; |
|||
|
|||
interface IMSConfigurationsTableProps { |
|||
imsConfigurations: IMSConfiguration[]; |
|||
loading: boolean; |
|||
onEdit: (configuration: IMSConfiguration) => void; |
|||
onDelete: (configuration: IMSConfiguration) => void; |
|||
page: number; |
|||
pageSize: number; |
|||
total: number; |
|||
onPageChange: (page: number) => void; |
|||
hideCard?: boolean; |
|||
density?: 'relaxed' | 'default' | 'compact'; |
|||
columns?: { key: string; title: string; visible: boolean }[]; |
|||
} |
|||
|
|||
// 状态徽章组件
|
|||
const StatusBadge: React.FC<{ isDisabled: boolean }> = ({ isDisabled }) => { |
|||
return ( |
|||
<Badge className={!isDisabled ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}> |
|||
{!isDisabled ? '启用' : '禁用'} |
|||
</Badge> |
|||
); |
|||
}; |
|||
|
|||
// 日期格式化组件
|
|||
const DateDisplay: React.FC<{ date?: string }> = ({ date }) => { |
|||
if (!date) return <span className="text-gray-400">-</span>; |
|||
|
|||
const formatDate = (dateString: string) => { |
|||
const date = new Date(dateString); |
|||
return date.toLocaleDateString('zh-CN', { |
|||
year: 'numeric', |
|||
month: '2-digit', |
|||
day: '2-digit', |
|||
hour: '2-digit', |
|||
minute: '2-digit' |
|||
}); |
|||
}; |
|||
|
|||
return <span>{formatDate(date)}</span>; |
|||
}; |
|||
|
|||
// 配置内容预览组件
|
|||
const ConfigContentPreview: React.FC<{ content: string }> = ({ content }) => { |
|||
const maxLength = 50; |
|||
const truncated = content.length > maxLength ? content.substring(0, maxLength) + '...' : content; |
|||
|
|||
return ( |
|||
<div className="max-w-xs truncate text-gray-600" title={content}> |
|||
{truncated} |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default function IMSConfigurationsTable({ |
|||
imsConfigurations, |
|||
loading, |
|||
onEdit, |
|||
onDelete, |
|||
page, |
|||
pageSize, |
|||
total, |
|||
onPageChange, |
|||
hideCard = false, |
|||
density = 'default', |
|||
columns = [] |
|||
}: IMSConfigurationsTableProps) { |
|||
const densityClasses = { |
|||
relaxed: 'py-3', |
|||
default: 'py-2', |
|||
compact: 'py-1', |
|||
}; |
|||
|
|||
const visibleColumns = columns.filter(col => col.visible); |
|||
|
|||
const renderCell = (configuration: IMSConfiguration, columnKey: string) => { |
|||
switch (columnKey) { |
|||
case 'name': |
|||
return ( |
|||
<div className="max-w-xs truncate" title={configuration.name}> |
|||
{configuration.name} |
|||
</div> |
|||
); |
|||
case 'description': |
|||
return ( |
|||
<div className="max-w-xs truncate text-gray-600" title={configuration.description || ''}> |
|||
{configuration.description || '-'} |
|||
</div> |
|||
); |
|||
case 'configContent': |
|||
return <ConfigContentPreview content={configuration.configContent} />; |
|||
case 'isDisabled': |
|||
return <StatusBadge isDisabled={configuration.isDisabled} />; |
|||
case 'createdAt': |
|||
return <DateDisplay date={configuration.createdAt} />; |
|||
case 'updatedAt': |
|||
return <DateDisplay date={configuration.updatedAt} />; |
|||
case 'actions': |
|||
return ( |
|||
<div className="flex justify-end gap-4"> |
|||
<span |
|||
className="cursor-pointer text-blue-600 hover:underline select-none" |
|||
onClick={() => onEdit(configuration)} |
|||
> |
|||
修改 |
|||
</span> |
|||
<span |
|||
className="cursor-pointer text-red-500 hover:underline select-none" |
|||
onClick={() => onDelete(configuration)} |
|||
> |
|||
删除 |
|||
</span> |
|||
</div> |
|||
); |
|||
default: |
|||
return null; |
|||
} |
|||
}; |
|||
|
|||
const totalPages = Math.ceil(total / pageSize); |
|||
const Wrapper = hideCard ? React.Fragment : 'div'; |
|||
const wrapperProps = hideCard ? {} : { className: 'rounded-md border bg-background' }; |
|||
const rowClass = density === 'relaxed' ? 'h-20' : density === 'compact' ? 'h-8' : 'h-12'; |
|||
const cellPadding = density === 'relaxed' ? 'py-5' : density === 'compact' ? 'py-1' : 'py-3'; |
|||
|
|||
return ( |
|||
<Wrapper {...wrapperProps}> |
|||
<Table> |
|||
<TableHeader key="header"> |
|||
<TableRow className={rowClass}> |
|||
{visibleColumns.map(col => ( |
|||
<TableHead |
|||
key={col.key} |
|||
className={`text-foreground text-center ${col.key === 'actions' ? 'text-right' : ''} ${cellPadding}`} |
|||
> |
|||
{col.title} |
|||
</TableHead> |
|||
))} |
|||
</TableRow> |
|||
</TableHeader> |
|||
<TableBody key="body"> |
|||
{loading ? ( |
|||
<TableRow key="loading" className={rowClass}> |
|||
<TableCell colSpan={visibleColumns.length} className={`text-center text-muted-foreground ${cellPadding}`}> |
|||
加载中... |
|||
</TableCell> |
|||
</TableRow> |
|||
) : imsConfigurations.length === 0 ? ( |
|||
<TableRow key="empty" className={rowClass}> |
|||
<TableCell colSpan={visibleColumns.length} className={`text-center text-muted-foreground ${cellPadding}`}> |
|||
暂无数据 |
|||
</TableCell> |
|||
</TableRow> |
|||
) : ( |
|||
imsConfigurations.map((configuration) => ( |
|||
<TableRow key={configuration.imsConfigurationId} className={rowClass}> |
|||
{visibleColumns.map((column) => ( |
|||
<TableCell key={column.key} className={`text-foreground text-center ${cellPadding}`}> |
|||
{renderCell(configuration, column.key)} |
|||
</TableCell> |
|||
))} |
|||
</TableRow> |
|||
)) |
|||
)} |
|||
</TableBody> |
|||
</Table> |
|||
</Wrapper> |
|||
); |
|||
} |
@ -0,0 +1,356 @@ |
|||
import React, { useEffect, useState } from 'react'; |
|||
import { imsConfigurationService, IMSConfiguration, GetIMSConfigurationsRequest, CreateIMSConfigurationRequest, UpdateIMSConfigurationRequest } from '@/services/imsConfigurationService'; |
|||
import IMSConfigurationsTable from './IMSConfigurationsTable'; |
|||
import IMSConfigurationForm from './IMSConfigurationForm'; |
|||
import { Input } from '@/components/ui/input'; |
|||
import PaginationBar from '@/components/ui/PaginationBar'; |
|||
import TableToolbar, { DensityType } from '@/components/ui/TableToolbar'; |
|||
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog'; |
|||
import { Button } from '@/components/ui/button'; |
|||
import { ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons'; |
|||
import { useToast } from '@/components/ui/use-toast'; |
|||
|
|||
const defaultColumns = [ |
|||
{ key: 'name', title: '配置名称', visible: true }, |
|||
{ key: 'description', title: '描述', visible: true }, |
|||
{ key: 'configContent', title: '配置内容', visible: false }, |
|||
{ key: 'isDisabled', title: '状态', visible: true }, |
|||
{ key: 'createdAt', title: '创建时间', visible: true }, |
|||
{ key: 'actions', title: '操作', visible: true } |
|||
]; |
|||
|
|||
// 字段类型声明
|
|||
type SearchField = |
|||
| { key: string; label: string; type: 'input'; placeholder: string } |
|||
| { key: string; label: string; type: 'select'; options: { value: string; label: string }[] }; |
|||
|
|||
// 第一行字段(收起时只显示这3个)
|
|||
const firstRowFields: SearchField[] = [ |
|||
{ key: 'searchTerm', label: '搜索关键词', type: 'input', placeholder: '请输入配置名称或描述' }, |
|||
{ key: 'isDisabled', label: '状态', type: 'select', options: [ |
|||
{ value: '', label: '请选择' }, |
|||
{ value: 'false', label: '启用' }, |
|||
{ value: 'true', label: '禁用' }, |
|||
] }, |
|||
]; |
|||
|
|||
// 高级字段(展开时才显示)
|
|||
const advancedFields: SearchField[] = [ |
|||
{ key: 'pageSize', label: '每页数量', type: 'select', options: [ |
|||
{ value: '10', label: '10条/页' }, |
|||
{ value: '20', label: '20条/页' }, |
|||
{ value: '50', label: '50条/页' }, |
|||
{ value: '100', label: '100条/页' }, |
|||
] }, |
|||
]; |
|||
|
|||
export default function IMSConfigurationsView() { |
|||
const [imsConfigurations, setIMSConfigurations] = useState<IMSConfiguration[]>([]); |
|||
const [loading, setLoading] = useState(false); |
|||
const [total, setTotal] = useState(0); |
|||
const [pageNumber, setPageNumber] = useState(1); |
|||
const [pageSize, setPageSize] = useState(10); |
|||
const [density, setDensity] = useState<DensityType>('default'); |
|||
const [columns, setColumns] = useState(defaultColumns); |
|||
const [showAdvanced, setShowAdvanced] = useState(false); |
|||
|
|||
// 搜索参数
|
|||
const [searchTerm, setSearchTerm] = useState(''); |
|||
const [isDisabled, setIsDisabled] = useState<boolean | undefined>(undefined); |
|||
|
|||
// 表单对话框状态
|
|||
const [open, setOpen] = useState(false); |
|||
const [editOpen, setEditOpen] = useState(false); |
|||
const [selectedConfiguration, setSelectedConfiguration] = useState<IMSConfiguration | null>(null); |
|||
|
|||
// 提交状态
|
|||
const [isSubmitting, setIsSubmitting] = useState(false); |
|||
|
|||
// Toast 提示
|
|||
const { toast } = useToast(); |
|||
|
|||
const fetchIMSConfigurations = async (params: Partial<GetIMSConfigurationsRequest> = {}) => { |
|||
setLoading(true); |
|||
const queryParams: GetIMSConfigurationsRequest = { |
|||
pageNumber, |
|||
pageSize, |
|||
searchTerm, |
|||
isDisabled, |
|||
...params |
|||
}; |
|||
|
|||
const result = await imsConfigurationService.getIMSConfigurations(queryParams); |
|||
if (result.isSuccess && result.data) { |
|||
setIMSConfigurations(result.data.items || []); |
|||
setTotal(result.data.totalCount || 0); |
|||
} |
|||
setLoading(false); |
|||
}; |
|||
|
|||
useEffect(() => { |
|||
fetchIMSConfigurations(); |
|||
// eslint-disable-next-line
|
|||
}, [pageNumber, pageSize]); |
|||
|
|||
const handleEdit = (configuration: IMSConfiguration) => { |
|||
setSelectedConfiguration(configuration); |
|||
setEditOpen(true); |
|||
}; |
|||
|
|||
const handleDelete = async (configuration: IMSConfiguration) => { |
|||
if (confirm(`确定要删除IMS配置 "${configuration.name}" 吗?`)) { |
|||
try { |
|||
const result = await imsConfigurationService.deleteIMSConfiguration(configuration.imsConfigurationId); |
|||
if (result.isSuccess) { |
|||
toast({ |
|||
title: "删除成功", |
|||
description: `IMS配置 "${configuration.name}" 删除成功`, |
|||
}); |
|||
fetchIMSConfigurations(); |
|||
} else { |
|||
const errorMessage = result.errorMessages?.join(', ') || "删除IMS配置时发生错误"; |
|||
console.error('删除IMS配置失败:', errorMessage); |
|||
toast({ |
|||
title: "删除失败", |
|||
description: errorMessage, |
|||
variant: "destructive", |
|||
}); |
|||
} |
|||
} catch (error) { |
|||
console.error('删除IMS配置异常:', error); |
|||
toast({ |
|||
title: "删除失败", |
|||
description: "网络错误,请稍后重试", |
|||
variant: "destructive", |
|||
}); |
|||
} |
|||
} |
|||
}; |
|||
|
|||
const handleCreate = async (data: CreateIMSConfigurationRequest) => { |
|||
if (isSubmitting) return; // 防止重复提交
|
|||
|
|||
console.log('开始创建IMS配置:', data); |
|||
setIsSubmitting(true); |
|||
try { |
|||
const result = await imsConfigurationService.createIMSConfiguration(data); |
|||
console.log('创建IMS配置结果:', result); |
|||
|
|||
if (result.isSuccess) { |
|||
toast({ |
|||
title: "创建成功", |
|||
description: `IMS配置 "${data.name}" 创建成功`, |
|||
}); |
|||
setOpen(false); |
|||
fetchIMSConfigurations(); |
|||
} else { |
|||
const errorMessage = result.errorMessages?.join(', ') || "创建IMS配置时发生错误"; |
|||
console.error('创建IMS配置失败:', errorMessage, result); |
|||
toast({ |
|||
title: "创建失败", |
|||
description: errorMessage, |
|||
variant: "destructive", |
|||
}); |
|||
} |
|||
} catch (error) { |
|||
console.error('创建IMS配置异常:', error); |
|||
toast({ |
|||
title: "创建失败", |
|||
description: "网络错误,请稍后重试", |
|||
variant: "destructive", |
|||
}); |
|||
} finally { |
|||
setIsSubmitting(false); |
|||
} |
|||
}; |
|||
|
|||
const handleUpdate = async (data: UpdateIMSConfigurationRequest) => { |
|||
if (!selectedConfiguration || isSubmitting) return; // 防止重复提交
|
|||
|
|||
setIsSubmitting(true); |
|||
try { |
|||
const result = await imsConfigurationService.updateIMSConfiguration(selectedConfiguration.imsConfigurationId, data); |
|||
if (result.isSuccess) { |
|||
toast({ |
|||
title: "更新成功", |
|||
description: `IMS配置 "${data.name}" 更新成功`, |
|||
}); |
|||
setEditOpen(false); |
|||
setSelectedConfiguration(null); |
|||
fetchIMSConfigurations(); |
|||
} else { |
|||
const errorMessage = result.errorMessages?.join(', ') || "更新IMS配置时发生错误"; |
|||
console.error('更新IMS配置失败:', errorMessage); |
|||
toast({ |
|||
title: "更新失败", |
|||
description: errorMessage, |
|||
variant: "destructive", |
|||
}); |
|||
} |
|||
} catch (error) { |
|||
console.error('更新IMS配置异常:', error); |
|||
toast({ |
|||
title: "更新失败", |
|||
description: "网络错误,请稍后重试", |
|||
variant: "destructive", |
|||
}); |
|||
} finally { |
|||
setIsSubmitting(false); |
|||
} |
|||
}; |
|||
|
|||
// 查询按钮
|
|||
const handleQuery = () => { |
|||
setPageNumber(1); |
|||
fetchIMSConfigurations({ pageNumber: 1 }); |
|||
}; |
|||
|
|||
// 重置按钮
|
|||
const handleReset = () => { |
|||
setSearchTerm(''); |
|||
setIsDisabled(undefined); |
|||
setPageNumber(1); |
|||
fetchIMSConfigurations({ |
|||
searchTerm: '', |
|||
isDisabled: undefined, |
|||
pageNumber: 1 |
|||
}); |
|||
}; |
|||
|
|||
// 每页条数选择
|
|||
const handlePageSizeChange = (size: number) => { |
|||
setPageSize(size); |
|||
setPageNumber(1); |
|||
}; |
|||
|
|||
const totalPages = Math.ceil(total / pageSize); |
|||
|
|||
return ( |
|||
<main className="flex-1 p-4 transition-all duration-300 ease-in-out sm:p-6"> |
|||
<div className="w-full space-y-4"> |
|||
{/* 丰富美化后的搜索栏 */} |
|||
<div className="flex flex-col bg-white p-4 rounded-md border mb-2"> |
|||
<form |
|||
className={`grid gap-x-8 gap-y-4 items-center ${showAdvanced ? 'md:grid-cols-3' : 'md:grid-cols-3'} grid-cols-1`} |
|||
onSubmit={e => { |
|||
e.preventDefault(); |
|||
handleQuery(); |
|||
}} |
|||
> |
|||
{(showAdvanced ? [...firstRowFields, ...advancedFields] : firstRowFields).map(field => ( |
|||
<div className="flex flex-row items-center min-w-[200px] flex-1" key={field.key}> |
|||
<label |
|||
className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right" |
|||
style={{ width: 80, minWidth: 80 }} |
|||
> |
|||
{field.label}: |
|||
</label> |
|||
{field.type === 'input' && ( |
|||
<Input |
|||
className="input flex-1" |
|||
placeholder={field.placeholder} |
|||
value={field.key === 'searchTerm' ? searchTerm : ''} |
|||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { |
|||
if (field.key === 'searchTerm') setSearchTerm(e.target.value); |
|||
}} |
|||
/> |
|||
)} |
|||
{field.type === 'select' && ( |
|||
<select |
|||
className="input h-10 rounded border border-border bg-background px-3 text-sm flex-1" |
|||
value={field.key === 'isDisabled' ? (isDisabled === undefined ? '' : isDisabled.toString()) : |
|||
field.key === 'pageSize' ? pageSize.toString() : ''} |
|||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => { |
|||
if (field.key === 'isDisabled') { |
|||
const value = e.target.value; |
|||
setIsDisabled(value === '' ? undefined : value === 'true'); |
|||
} else if (field.key === 'pageSize') { |
|||
setPageSize(parseInt(e.target.value)); |
|||
} |
|||
}} |
|||
> |
|||
{field.options.map(opt => ( |
|||
<option value={opt.value} key={opt.value}>{opt.label}</option> |
|||
))} |
|||
</select> |
|||
)} |
|||
</div> |
|||
))} |
|||
{/* 按钮组直接作为表单项之一,紧跟在最后一个表单项后面 */} |
|||
<div className="flex flex-row items-center min-w-[200px] flex-1 justify-end gap-2"> |
|||
<button type="button" className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" onClick={handleReset}>重置</button> |
|||
<button type="submit" className="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700">查询</button> |
|||
<button type="button" className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" onClick={() => setShowAdvanced(v => !v)}> |
|||
{showAdvanced ? ( |
|||
<> |
|||
收起 <ChevronUpIcon className="inline-block ml-1 w-4 h-4 align-middle" /> |
|||
</> |
|||
) : ( |
|||
<> |
|||
展开 <ChevronDownIcon className="inline-block ml-1 w-4 h-4 align-middle" /> |
|||
</> |
|||
)} |
|||
</button> |
|||
</div> |
|||
</form> |
|||
</div> |
|||
{/* 表格整体卡片区域,包括工具栏、表格、分页 */} |
|||
<div className="rounded-md border bg-background p-4"> |
|||
{/* 顶部操作栏:添加IMS配置+工具栏 */} |
|||
<div className="flex items-center justify-between mb-2"> |
|||
<Dialog open={open} onOpenChange={setOpen}> |
|||
<DialogTrigger asChild> |
|||
<Button className="bg-primary text-primary-foreground hover:bg-primary/90">+ 添加IMS配置</Button> |
|||
</DialogTrigger> |
|||
<DialogContent className="bg-background"> |
|||
<IMSConfigurationForm onSubmit={handleCreate} isSubmitting={isSubmitting} /> |
|||
</DialogContent> |
|||
</Dialog> |
|||
<TableToolbar |
|||
onRefresh={() => fetchIMSConfigurations()} |
|||
onDensityChange={setDensity} |
|||
onColumnsChange={setColumns} |
|||
onColumnsReset={() => setColumns(defaultColumns)} |
|||
columns={columns} |
|||
density={density} |
|||
/> |
|||
</div> |
|||
{/* 表格区域 */} |
|||
<IMSConfigurationsTable |
|||
imsConfigurations={imsConfigurations} |
|||
loading={loading} |
|||
onEdit={handleEdit} |
|||
onDelete={handleDelete} |
|||
page={pageNumber} |
|||
pageSize={pageSize} |
|||
total={total} |
|||
onPageChange={setPageNumber} |
|||
hideCard={true} |
|||
density={density} |
|||
columns={columns} |
|||
/> |
|||
{/* 分页 */} |
|||
<PaginationBar |
|||
page={pageNumber} |
|||
pageSize={pageSize} |
|||
total={total} |
|||
onPageChange={setPageNumber} |
|||
onPageSizeChange={handlePageSizeChange} |
|||
/> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* 编辑IMS配置对话框 */} |
|||
<Dialog open={editOpen} onOpenChange={setEditOpen}> |
|||
<DialogContent className="bg-background"> |
|||
<IMSConfigurationForm |
|||
onSubmit={(data: CreateIMSConfigurationRequest | UpdateIMSConfigurationRequest) => handleUpdate(data as UpdateIMSConfigurationRequest)} |
|||
initialData={selectedConfiguration || undefined} |
|||
isEdit={true} |
|||
isSubmitting={isSubmitting} |
|||
/> |
|||
</DialogContent> |
|||
</Dialog> |
|||
</main> |
|||
); |
|||
} |
@ -0,0 +1,90 @@ |
|||
import React from 'react'; |
|||
import { Button } from '@/components/ui/button'; |
|||
import { Input } from '@/components/ui/input'; |
|||
import { Label } from '@/components/ui/label'; |
|||
import { Textarea } from '@/components/ui/textarea'; |
|||
import { Checkbox } from '@/components/ui/checkbox'; |
|||
import { CreateNetworkStackConfigRequest, UpdateNetworkStackConfigRequest, NetworkStackConfig } from '@/services/networkStackConfigService'; |
|||
|
|||
interface NetworkStackConfigFormProps { |
|||
onSubmit: (data: CreateNetworkStackConfigRequest | UpdateNetworkStackConfigRequest) => void; |
|||
initialData?: Partial<NetworkStackConfig>; |
|||
isEdit?: boolean; |
|||
isSubmitting?: boolean; |
|||
} |
|||
|
|||
export default function NetworkStackConfigForm({ onSubmit, initialData, isEdit = false, isSubmitting = false }: NetworkStackConfigFormProps) { |
|||
const [formData, setFormData] = React.useState<CreateNetworkStackConfigRequest>({ |
|||
stackId: initialData?.stackId || '', |
|||
ranId: initialData?.ranId || '', |
|||
description: initialData?.description || '', |
|||
isActive: initialData?.isActive ?? true |
|||
}); |
|||
|
|||
const handleSubmit = (e: React.FormEvent) => { |
|||
e.preventDefault(); |
|||
if (isSubmitting) return; // 防止重复提交
|
|||
onSubmit(formData); |
|||
}; |
|||
|
|||
return ( |
|||
<form onSubmit={handleSubmit} className="space-y-4"> |
|||
<div className="space-y-2"> |
|||
<Label htmlFor="stackId">栈ID</Label> |
|||
<Input |
|||
id="stackId" |
|||
value={formData.stackId} |
|||
onChange={e => setFormData({ ...formData, stackId: e.target.value })} |
|||
placeholder="请输入栈ID" |
|||
required |
|||
disabled={isSubmitting || isEdit} // 编辑时不允许修改栈ID
|
|||
className={isEdit ? "bg-gray-100" : ""} |
|||
/> |
|||
{isEdit && ( |
|||
<p className="text-xs text-gray-500"> |
|||
编辑时不允许修改栈ID |
|||
</p> |
|||
)} |
|||
</div> |
|||
|
|||
<div className="space-y-2"> |
|||
<Label htmlFor="ranId">RAN ID</Label> |
|||
<Input |
|||
id="ranId" |
|||
value={formData.ranId} |
|||
onChange={e => setFormData({ ...formData, ranId: e.target.value })} |
|||
placeholder="请输入RAN ID(可选)" |
|||
disabled={isSubmitting} |
|||
/> |
|||
</div> |
|||
|
|||
<div className="space-y-2"> |
|||
<Label htmlFor="description">描述</Label> |
|||
<Textarea |
|||
id="description" |
|||
value={formData.description} |
|||
onChange={e => setFormData({ ...formData, description: e.target.value })} |
|||
placeholder="请输入配置描述" |
|||
rows={3} |
|||
disabled={isSubmitting} |
|||
/> |
|||
</div> |
|||
|
|||
<div className="flex items-center space-x-2"> |
|||
<Checkbox |
|||
id="isActive" |
|||
checked={formData.isActive} |
|||
onCheckedChange={(checked) => |
|||
setFormData({ ...formData, isActive: checked as boolean }) |
|||
} |
|||
disabled={isSubmitting} |
|||
/> |
|||
<Label htmlFor="isActive">激活此配置</Label> |
|||
</div> |
|||
|
|||
<Button type="submit" className="w-full" disabled={isSubmitting}> |
|||
{isSubmitting ? '提交中...' : (isEdit ? '更新网络栈配置' : '创建网络栈配置')} |
|||
</Button> |
|||
</form> |
|||
); |
|||
} |
@ -0,0 +1,165 @@ |
|||
import React from 'react'; |
|||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; |
|||
import { NetworkStackConfig } from '@/services/networkStackConfigService'; |
|||
import { Badge } from '@/components/ui/badge'; |
|||
|
|||
interface NetworkStackConfigsTableProps { |
|||
networkStackConfigs: NetworkStackConfig[]; |
|||
loading: boolean; |
|||
onEdit: (config: NetworkStackConfig) => void; |
|||
onDelete: (config: NetworkStackConfig) => void; |
|||
page: number; |
|||
pageSize: number; |
|||
total: number; |
|||
onPageChange: (page: number) => void; |
|||
hideCard?: boolean; |
|||
density?: 'relaxed' | 'default' | 'compact'; |
|||
columns?: { key: string; title: string; visible: boolean }[]; |
|||
} |
|||
|
|||
// 状态徽章组件
|
|||
const StatusBadge: React.FC<{ isActive: boolean }> = ({ isActive }) => { |
|||
return ( |
|||
<Badge className={isActive ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}> |
|||
{isActive ? '激活' : '非激活'} |
|||
</Badge> |
|||
); |
|||
}; |
|||
|
|||
// 日期格式化组件
|
|||
const DateDisplay: React.FC<{ date?: string }> = ({ date }) => { |
|||
if (!date) return <span className="text-gray-400">-</span>; |
|||
|
|||
const formatDate = (dateString: string) => { |
|||
const date = new Date(dateString); |
|||
return date.toLocaleDateString('zh-CN', { |
|||
year: 'numeric', |
|||
month: '2-digit', |
|||
day: '2-digit', |
|||
hour: '2-digit', |
|||
minute: '2-digit' |
|||
}); |
|||
}; |
|||
|
|||
return <span>{formatDate(date)}</span>; |
|||
}; |
|||
|
|||
export default function NetworkStackConfigsTable({ |
|||
networkStackConfigs, |
|||
loading, |
|||
onEdit, |
|||
onDelete, |
|||
page, |
|||
pageSize, |
|||
total, |
|||
onPageChange, |
|||
hideCard = false, |
|||
density = 'default', |
|||
columns = [] |
|||
}: NetworkStackConfigsTableProps) { |
|||
const densityClasses = { |
|||
relaxed: 'py-3', |
|||
default: 'py-2', |
|||
compact: 'py-1', |
|||
}; |
|||
|
|||
const visibleColumns = columns.filter(col => col.visible); |
|||
|
|||
const renderCell = (config: NetworkStackConfig, columnKey: string) => { |
|||
switch (columnKey) { |
|||
case 'stackId': |
|||
return ( |
|||
<div className="max-w-xs truncate font-mono text-sm" title={config.stackId}> |
|||
{config.stackId} |
|||
</div> |
|||
); |
|||
case 'ranId': |
|||
return ( |
|||
<div className="max-w-xs truncate text-gray-600" title={config.ranId || ''}> |
|||
{config.ranId || '-'} |
|||
</div> |
|||
); |
|||
case 'description': |
|||
return ( |
|||
<div className="max-w-xs truncate text-gray-600" title={config.description || ''}> |
|||
{config.description || '-'} |
|||
</div> |
|||
); |
|||
case 'isActive': |
|||
return <StatusBadge isActive={config.isActive} />; |
|||
case 'createdAt': |
|||
return <DateDisplay date={config.createdAt} />; |
|||
case 'updatedAt': |
|||
return <DateDisplay date={config.updatedAt} />; |
|||
case 'actions': |
|||
return ( |
|||
<div className="flex justify-end gap-4"> |
|||
<span |
|||
className="cursor-pointer text-blue-600 hover:underline select-none" |
|||
onClick={() => onEdit(config)} |
|||
> |
|||
修改 |
|||
</span> |
|||
<span |
|||
className="cursor-pointer text-red-500 hover:underline select-none" |
|||
onClick={() => onDelete(config)} |
|||
> |
|||
删除 |
|||
</span> |
|||
</div> |
|||
); |
|||
default: |
|||
return null; |
|||
} |
|||
}; |
|||
|
|||
const totalPages = Math.ceil(total / pageSize); |
|||
const Wrapper = hideCard ? React.Fragment : 'div'; |
|||
const wrapperProps = hideCard ? {} : { className: 'rounded-md border bg-background' }; |
|||
const rowClass = density === 'relaxed' ? 'h-20' : density === 'compact' ? 'h-8' : 'h-12'; |
|||
const cellPadding = density === 'relaxed' ? 'py-5' : density === 'compact' ? 'py-1' : 'py-3'; |
|||
|
|||
return ( |
|||
<Wrapper {...wrapperProps}> |
|||
<Table> |
|||
<TableHeader key="header"> |
|||
<TableRow className={rowClass}> |
|||
{visibleColumns.map(col => ( |
|||
<TableHead |
|||
key={col.key} |
|||
className={`text-foreground text-center ${col.key === 'actions' ? 'text-right' : ''} ${cellPadding}`} |
|||
> |
|||
{col.title} |
|||
</TableHead> |
|||
))} |
|||
</TableRow> |
|||
</TableHeader> |
|||
<TableBody key="body"> |
|||
{loading ? ( |
|||
<TableRow key="loading" className={rowClass}> |
|||
<TableCell colSpan={visibleColumns.length} className={`text-center text-muted-foreground ${cellPadding}`}> |
|||
加载中... |
|||
</TableCell> |
|||
</TableRow> |
|||
) : networkStackConfigs.length === 0 ? ( |
|||
<TableRow key="empty" className={rowClass}> |
|||
<TableCell colSpan={visibleColumns.length} className={`text-center text-muted-foreground ${cellPadding}`}> |
|||
暂无数据 |
|||
</TableCell> |
|||
</TableRow> |
|||
) : ( |
|||
networkStackConfigs.map((config) => ( |
|||
<TableRow key={config.networkStackConfigId} className={rowClass}> |
|||
{visibleColumns.map((column) => ( |
|||
<TableCell key={column.key} className={`text-foreground text-center ${cellPadding}`}> |
|||
{renderCell(config, column.key)} |
|||
</TableCell> |
|||
))} |
|||
</TableRow> |
|||
)) |
|||
)} |
|||
</TableBody> |
|||
</Table> |
|||
</Wrapper> |
|||
); |
|||
} |
@ -0,0 +1,356 @@ |
|||
import React, { useEffect, useState } from 'react'; |
|||
import { networkStackConfigService, NetworkStackConfig, GetNetworkStackConfigsRequest, CreateNetworkStackConfigRequest, UpdateNetworkStackConfigRequest } from '@/services/networkStackConfigService'; |
|||
import NetworkStackConfigsTable from './NetworkStackConfigsTable'; |
|||
import NetworkStackConfigForm from './NetworkStackConfigForm'; |
|||
import { Input } from '@/components/ui/input'; |
|||
import PaginationBar from '@/components/ui/PaginationBar'; |
|||
import TableToolbar, { DensityType } from '@/components/ui/TableToolbar'; |
|||
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog'; |
|||
import { Button } from '@/components/ui/button'; |
|||
import { ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons'; |
|||
import { useToast } from '@/components/ui/use-toast'; |
|||
|
|||
const defaultColumns = [ |
|||
{ key: 'stackId', title: '栈ID', visible: true }, |
|||
{ key: 'ranId', title: 'RAN ID', visible: true }, |
|||
{ key: 'description', title: '描述', visible: true }, |
|||
{ key: 'isActive', title: '状态', visible: true }, |
|||
{ key: 'createdAt', title: '创建时间', visible: true }, |
|||
{ key: 'actions', title: '操作', visible: true } |
|||
]; |
|||
|
|||
// 字段类型声明
|
|||
type SearchField = |
|||
| { key: string; label: string; type: 'input'; placeholder: string } |
|||
| { key: string; label: string; type: 'select'; options: { value: string; label: string }[] }; |
|||
|
|||
// 第一行字段(收起时只显示这3个)
|
|||
const firstRowFields: SearchField[] = [ |
|||
{ key: 'searchTerm', label: '搜索关键词', type: 'input', placeholder: '请输入栈ID或描述' }, |
|||
{ key: 'isActive', label: '状态', type: 'select', options: [ |
|||
{ value: '', label: '请选择' }, |
|||
{ value: 'true', label: '激活' }, |
|||
{ value: 'false', label: '非激活' }, |
|||
] }, |
|||
]; |
|||
|
|||
// 高级字段(展开时才显示)
|
|||
const advancedFields: SearchField[] = [ |
|||
{ key: 'pageSize', label: '每页数量', type: 'select', options: [ |
|||
{ value: '10', label: '10条/页' }, |
|||
{ value: '20', label: '20条/页' }, |
|||
{ value: '50', label: '50条/页' }, |
|||
{ value: '100', label: '100条/页' }, |
|||
] }, |
|||
]; |
|||
|
|||
export default function NetworkStackConfigsView() { |
|||
const [networkStackConfigs, setNetworkStackConfigs] = useState<NetworkStackConfig[]>([]); |
|||
const [loading, setLoading] = useState(false); |
|||
const [total, setTotal] = useState(0); |
|||
const [pageNumber, setPageNumber] = useState(1); |
|||
const [pageSize, setPageSize] = useState(10); |
|||
const [density, setDensity] = useState<DensityType>('default'); |
|||
const [columns, setColumns] = useState(defaultColumns); |
|||
const [showAdvanced, setShowAdvanced] = useState(false); |
|||
|
|||
// 搜索参数
|
|||
const [searchTerm, setSearchTerm] = useState(''); |
|||
const [isActive, setIsActive] = useState<boolean | undefined>(undefined); |
|||
|
|||
// 表单对话框状态
|
|||
const [open, setOpen] = useState(false); |
|||
const [editOpen, setEditOpen] = useState(false); |
|||
const [selectedConfig, setSelectedConfig] = useState<NetworkStackConfig | null>(null); |
|||
|
|||
// 提交状态
|
|||
const [isSubmitting, setIsSubmitting] = useState(false); |
|||
|
|||
// Toast 提示
|
|||
const { toast } = useToast(); |
|||
|
|||
const fetchNetworkStackConfigs = async (params: Partial<GetNetworkStackConfigsRequest> = {}) => { |
|||
setLoading(true); |
|||
const queryParams: GetNetworkStackConfigsRequest = { |
|||
pageNumber, |
|||
pageSize, |
|||
searchTerm, |
|||
isActive, |
|||
...params |
|||
}; |
|||
|
|||
const result = await networkStackConfigService.getNetworkStackConfigs(queryParams); |
|||
if (result.isSuccess && result.data) { |
|||
setNetworkStackConfigs(result.data.items || []); |
|||
setTotal(result.data.totalCount || 0); |
|||
} |
|||
setLoading(false); |
|||
}; |
|||
|
|||
useEffect(() => { |
|||
fetchNetworkStackConfigs(); |
|||
// eslint-disable-next-line
|
|||
}, [pageNumber, pageSize]); |
|||
|
|||
const handleEdit = (config: NetworkStackConfig) => { |
|||
setSelectedConfig(config); |
|||
setEditOpen(true); |
|||
}; |
|||
|
|||
const handleDelete = async (config: NetworkStackConfig) => { |
|||
if (confirm(`确定要删除网络栈配置 "${config.stackId}" 吗?`)) { |
|||
try { |
|||
const result = await networkStackConfigService.deleteNetworkStackConfig(config.networkStackConfigId); |
|||
if (result.isSuccess) { |
|||
toast({ |
|||
title: "删除成功", |
|||
description: `网络栈配置 "${config.stackId}" 删除成功`, |
|||
}); |
|||
fetchNetworkStackConfigs(); |
|||
} else { |
|||
const errorMessage = result.errorMessages?.join(', ') || "删除网络栈配置时发生错误"; |
|||
console.error('删除网络栈配置失败:', errorMessage); |
|||
toast({ |
|||
title: "删除失败", |
|||
description: errorMessage, |
|||
variant: "destructive", |
|||
}); |
|||
} |
|||
} catch (error) { |
|||
console.error('删除网络栈配置异常:', error); |
|||
toast({ |
|||
title: "删除失败", |
|||
description: "网络错误,请稍后重试", |
|||
variant: "destructive", |
|||
}); |
|||
} |
|||
} |
|||
}; |
|||
|
|||
const handleCreate = async (data: CreateNetworkStackConfigRequest) => { |
|||
if (isSubmitting) return; // 防止重复提交
|
|||
|
|||
console.log('开始创建网络栈配置:', data); |
|||
setIsSubmitting(true); |
|||
try { |
|||
const result = await networkStackConfigService.createNetworkStackConfig(data); |
|||
console.log('创建网络栈配置结果:', result); |
|||
|
|||
if (result.isSuccess) { |
|||
toast({ |
|||
title: "创建成功", |
|||
description: `网络栈配置 "${data.stackId}" 创建成功`, |
|||
}); |
|||
setOpen(false); |
|||
fetchNetworkStackConfigs(); |
|||
} else { |
|||
const errorMessage = result.errorMessages?.join(', ') || "创建网络栈配置时发生错误"; |
|||
console.error('创建网络栈配置失败:', errorMessage, result); |
|||
toast({ |
|||
title: "创建失败", |
|||
description: errorMessage, |
|||
variant: "destructive", |
|||
}); |
|||
} |
|||
} catch (error) { |
|||
console.error('创建网络栈配置异常:', error); |
|||
toast({ |
|||
title: "创建失败", |
|||
description: "网络错误,请稍后重试", |
|||
variant: "destructive", |
|||
}); |
|||
} finally { |
|||
setIsSubmitting(false); |
|||
} |
|||
}; |
|||
|
|||
const handleUpdate = async (data: UpdateNetworkStackConfigRequest) => { |
|||
if (!selectedConfig || isSubmitting) return; // 防止重复提交
|
|||
|
|||
setIsSubmitting(true); |
|||
try { |
|||
const result = await networkStackConfigService.updateNetworkStackConfig(selectedConfig.networkStackConfigId, data); |
|||
if (result.isSuccess) { |
|||
toast({ |
|||
title: "更新成功", |
|||
description: `网络栈配置 "${selectedConfig.stackId}" 更新成功`, |
|||
}); |
|||
setEditOpen(false); |
|||
setSelectedConfig(null); |
|||
fetchNetworkStackConfigs(); |
|||
} else { |
|||
const errorMessage = result.errorMessages?.join(', ') || "更新网络栈配置时发生错误"; |
|||
console.error('更新网络栈配置失败:', errorMessage); |
|||
toast({ |
|||
title: "更新失败", |
|||
description: errorMessage, |
|||
variant: "destructive", |
|||
}); |
|||
} |
|||
} catch (error) { |
|||
console.error('更新网络栈配置异常:', error); |
|||
toast({ |
|||
title: "更新失败", |
|||
description: "网络错误,请稍后重试", |
|||
variant: "destructive", |
|||
}); |
|||
} finally { |
|||
setIsSubmitting(false); |
|||
} |
|||
}; |
|||
|
|||
// 查询按钮
|
|||
const handleQuery = () => { |
|||
setPageNumber(1); |
|||
fetchNetworkStackConfigs({ pageNumber: 1 }); |
|||
}; |
|||
|
|||
// 重置按钮
|
|||
const handleReset = () => { |
|||
setSearchTerm(''); |
|||
setIsActive(undefined); |
|||
setPageNumber(1); |
|||
fetchNetworkStackConfigs({ |
|||
searchTerm: '', |
|||
isActive: undefined, |
|||
pageNumber: 1 |
|||
}); |
|||
}; |
|||
|
|||
// 每页条数选择
|
|||
const handlePageSizeChange = (size: number) => { |
|||
setPageSize(size); |
|||
setPageNumber(1); |
|||
}; |
|||
|
|||
const totalPages = Math.ceil(total / pageSize); |
|||
|
|||
return ( |
|||
<main className="flex-1 p-4 transition-all duration-300 ease-in-out sm:p-6"> |
|||
<div className="w-full space-y-4"> |
|||
{/* 丰富美化后的搜索栏 */} |
|||
<div className="flex flex-col bg-white p-4 rounded-md border mb-2"> |
|||
<form |
|||
className={`grid gap-x-8 gap-y-4 items-center ${showAdvanced ? 'md:grid-cols-3' : 'md:grid-cols-3'} grid-cols-1`} |
|||
onSubmit={e => { |
|||
e.preventDefault(); |
|||
handleQuery(); |
|||
}} |
|||
> |
|||
{(showAdvanced ? [...firstRowFields, ...advancedFields] : firstRowFields).map(field => ( |
|||
<div className="flex flex-row items-center min-w-[200px] flex-1" key={field.key}> |
|||
<label |
|||
className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right" |
|||
style={{ width: 80, minWidth: 80 }} |
|||
> |
|||
{field.label}: |
|||
</label> |
|||
{field.type === 'input' && ( |
|||
<Input |
|||
className="input flex-1" |
|||
placeholder={field.placeholder} |
|||
value={field.key === 'searchTerm' ? searchTerm : ''} |
|||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { |
|||
if (field.key === 'searchTerm') setSearchTerm(e.target.value); |
|||
}} |
|||
/> |
|||
)} |
|||
{field.type === 'select' && ( |
|||
<select |
|||
className="input h-10 rounded border border-border bg-background px-3 text-sm flex-1" |
|||
value={field.key === 'isActive' ? (isActive === undefined ? '' : isActive.toString()) : |
|||
field.key === 'pageSize' ? pageSize.toString() : ''} |
|||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => { |
|||
if (field.key === 'isActive') { |
|||
const value = e.target.value; |
|||
setIsActive(value === '' ? undefined : value === 'true'); |
|||
} else if (field.key === 'pageSize') { |
|||
setPageSize(parseInt(e.target.value)); |
|||
} |
|||
}} |
|||
> |
|||
{field.options.map(opt => ( |
|||
<option value={opt.value} key={opt.value}>{opt.label}</option> |
|||
))} |
|||
</select> |
|||
)} |
|||
</div> |
|||
))} |
|||
{/* 按钮组直接作为表单项之一,紧跟在最后一个表单项后面 */} |
|||
<div className="flex flex-row items-center min-w-[200px] flex-1 justify-end gap-2"> |
|||
<button type="button" className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" onClick={handleReset}>重置</button> |
|||
<button type="submit" className="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700">查询</button> |
|||
<button type="button" className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" onClick={() => setShowAdvanced(v => !v)}> |
|||
{showAdvanced ? ( |
|||
<> |
|||
收起 <ChevronUpIcon className="inline-block ml-1 w-4 h-4 align-middle" /> |
|||
</> |
|||
) : ( |
|||
<> |
|||
展开 <ChevronDownIcon className="inline-block ml-1 w-4 h-4 align-middle" /> |
|||
</> |
|||
)} |
|||
</button> |
|||
</div> |
|||
</form> |
|||
</div> |
|||
{/* 表格整体卡片区域,包括工具栏、表格、分页 */} |
|||
<div className="rounded-md border bg-background p-4"> |
|||
{/* 顶部操作栏:添加网络栈配置+工具栏 */} |
|||
<div className="flex items-center justify-between mb-2"> |
|||
<Dialog open={open} onOpenChange={setOpen}> |
|||
<DialogTrigger asChild> |
|||
<Button className="bg-primary text-primary-foreground hover:bg-primary/90">+ 添加网络栈配置</Button> |
|||
</DialogTrigger> |
|||
<DialogContent className="bg-background"> |
|||
<NetworkStackConfigForm onSubmit={handleCreate} isSubmitting={isSubmitting} /> |
|||
</DialogContent> |
|||
</Dialog> |
|||
<TableToolbar |
|||
onRefresh={() => fetchNetworkStackConfigs()} |
|||
onDensityChange={setDensity} |
|||
onColumnsChange={setColumns} |
|||
onColumnsReset={() => setColumns(defaultColumns)} |
|||
columns={columns} |
|||
density={density} |
|||
/> |
|||
</div> |
|||
{/* 表格区域 */} |
|||
<NetworkStackConfigsTable |
|||
networkStackConfigs={networkStackConfigs} |
|||
loading={loading} |
|||
onEdit={handleEdit} |
|||
onDelete={handleDelete} |
|||
page={pageNumber} |
|||
pageSize={pageSize} |
|||
total={total} |
|||
onPageChange={setPageNumber} |
|||
hideCard={true} |
|||
density={density} |
|||
columns={columns} |
|||
/> |
|||
{/* 分页 */} |
|||
<PaginationBar |
|||
page={pageNumber} |
|||
pageSize={pageSize} |
|||
total={total} |
|||
onPageChange={setPageNumber} |
|||
onPageSizeChange={handlePageSizeChange} |
|||
/> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* 编辑网络栈配置对话框 */} |
|||
<Dialog open={editOpen} onOpenChange={setEditOpen}> |
|||
<DialogContent className="bg-background"> |
|||
<NetworkStackConfigForm |
|||
onSubmit={(data: CreateNetworkStackConfigRequest | UpdateNetworkStackConfigRequest) => handleUpdate(data as UpdateNetworkStackConfigRequest)} |
|||
initialData={selectedConfig || undefined} |
|||
isEdit={true} |
|||
isSubmitting={isSubmitting} |
|||
/> |
|||
</DialogContent> |
|||
</Dialog> |
|||
</main> |
|||
); |
|||
} |
@ -0,0 +1,90 @@ |
|||
import React from 'react'; |
|||
import { Button } from '@/components/ui/button'; |
|||
import { Input } from '@/components/ui/input'; |
|||
import { Label } from '@/components/ui/label'; |
|||
import { Textarea } from '@/components/ui/textarea'; |
|||
import { Checkbox } from '@/components/ui/checkbox'; |
|||
import { CreateRANConfigurationRequest, UpdateRANConfigurationRequest, RANConfiguration } from '@/services/ranConfigurationService'; |
|||
|
|||
interface RANConfigurationFormProps { |
|||
onSubmit: (data: CreateRANConfigurationRequest | UpdateRANConfigurationRequest) => void; |
|||
initialData?: Partial<RANConfiguration>; |
|||
isEdit?: boolean; |
|||
isSubmitting?: boolean; |
|||
} |
|||
|
|||
export default function RANConfigurationForm({ onSubmit, initialData, isEdit = false, isSubmitting = false }: RANConfigurationFormProps) { |
|||
const [formData, setFormData] = React.useState<CreateRANConfigurationRequest>({ |
|||
name: initialData?.name || '', |
|||
configContent: initialData?.configContent || '', |
|||
description: initialData?.description || '', |
|||
isDisabled: initialData?.isDisabled ?? false |
|||
}); |
|||
|
|||
const handleSubmit = (e: React.FormEvent) => { |
|||
e.preventDefault(); |
|||
if (isSubmitting) return; // 防止重复提交
|
|||
onSubmit(formData); |
|||
}; |
|||
|
|||
return ( |
|||
<form onSubmit={handleSubmit} className="space-y-4"> |
|||
<div className="space-y-2"> |
|||
<Label htmlFor="name">配置名称</Label> |
|||
<Input |
|||
id="name" |
|||
value={formData.name} |
|||
onChange={e => setFormData({ ...formData, name: e.target.value })} |
|||
placeholder="请输入配置名称" |
|||
required |
|||
disabled={isSubmitting} |
|||
/> |
|||
</div> |
|||
|
|||
<div className="space-y-2"> |
|||
<Label htmlFor="configContent">配置内容 (JSON格式)</Label> |
|||
<Textarea |
|||
id="configContent" |
|||
value={formData.configContent} |
|||
onChange={e => setFormData({ ...formData, configContent: e.target.value })} |
|||
placeholder="请输入JSON格式的配置内容" |
|||
rows={8} |
|||
required |
|||
disabled={isSubmitting} |
|||
className="font-mono text-sm" |
|||
/> |
|||
<p className="text-xs text-gray-500"> |
|||
请输入有效的JSON格式配置内容 |
|||
</p> |
|||
</div> |
|||
|
|||
<div className="space-y-2"> |
|||
<Label htmlFor="description">描述</Label> |
|||
<Textarea |
|||
id="description" |
|||
value={formData.description} |
|||
onChange={e => setFormData({ ...formData, description: e.target.value })} |
|||
placeholder="请输入配置描述" |
|||
rows={3} |
|||
disabled={isSubmitting} |
|||
/> |
|||
</div> |
|||
|
|||
<div className="flex items-center space-x-2"> |
|||
<Checkbox |
|||
id="isDisabled" |
|||
checked={formData.isDisabled} |
|||
onCheckedChange={(checked) => |
|||
setFormData({ ...formData, isDisabled: checked as boolean }) |
|||
} |
|||
disabled={isSubmitting} |
|||
/> |
|||
<Label htmlFor="isDisabled">禁用此配置</Label> |
|||
</div> |
|||
|
|||
<Button type="submit" className="w-full" disabled={isSubmitting}> |
|||
{isSubmitting ? '提交中...' : (isEdit ? '更新RAN配置' : '创建RAN配置')} |
|||
</Button> |
|||
</form> |
|||
); |
|||
} |
@ -0,0 +1,173 @@ |
|||
import React from 'react'; |
|||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; |
|||
import { RANConfiguration } from '@/services/ranConfigurationService'; |
|||
import { Badge } from '@/components/ui/badge'; |
|||
|
|||
interface RANConfigurationsTableProps { |
|||
ranConfigurations: RANConfiguration[]; |
|||
loading: boolean; |
|||
onEdit: (configuration: RANConfiguration) => void; |
|||
onDelete: (configuration: RANConfiguration) => void; |
|||
page: number; |
|||
pageSize: number; |
|||
total: number; |
|||
onPageChange: (page: number) => void; |
|||
hideCard?: boolean; |
|||
density?: 'relaxed' | 'default' | 'compact'; |
|||
columns?: { key: string; title: string; visible: boolean }[]; |
|||
} |
|||
|
|||
// 状态徽章组件
|
|||
const StatusBadge: React.FC<{ isDisabled: boolean }> = ({ isDisabled }) => { |
|||
return ( |
|||
<Badge className={!isDisabled ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}> |
|||
{!isDisabled ? '启用' : '禁用'} |
|||
</Badge> |
|||
); |
|||
}; |
|||
|
|||
// 日期格式化组件
|
|||
const DateDisplay: React.FC<{ date?: string }> = ({ date }) => { |
|||
if (!date) return <span className="text-gray-400">-</span>; |
|||
|
|||
const formatDate = (dateString: string) => { |
|||
const date = new Date(dateString); |
|||
return date.toLocaleDateString('zh-CN', { |
|||
year: 'numeric', |
|||
month: '2-digit', |
|||
day: '2-digit', |
|||
hour: '2-digit', |
|||
minute: '2-digit' |
|||
}); |
|||
}; |
|||
|
|||
return <span>{formatDate(date)}</span>; |
|||
}; |
|||
|
|||
// 配置内容预览组件
|
|||
const ConfigContentPreview: React.FC<{ content: string }> = ({ content }) => { |
|||
const maxLength = 50; |
|||
const truncated = content.length > maxLength ? content.substring(0, maxLength) + '...' : content; |
|||
|
|||
return ( |
|||
<div className="max-w-xs truncate text-gray-600" title={content}> |
|||
{truncated} |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default function RANConfigurationsTable({ |
|||
ranConfigurations, |
|||
loading, |
|||
onEdit, |
|||
onDelete, |
|||
page, |
|||
pageSize, |
|||
total, |
|||
onPageChange, |
|||
hideCard = false, |
|||
density = 'default', |
|||
columns = [] |
|||
}: RANConfigurationsTableProps) { |
|||
const densityClasses = { |
|||
relaxed: 'py-3', |
|||
default: 'py-2', |
|||
compact: 'py-1', |
|||
}; |
|||
|
|||
const visibleColumns = columns.filter(col => col.visible); |
|||
|
|||
const renderCell = (configuration: RANConfiguration, columnKey: string) => { |
|||
switch (columnKey) { |
|||
case 'name': |
|||
return ( |
|||
<div className="max-w-xs truncate" title={configuration.name}> |
|||
{configuration.name} |
|||
</div> |
|||
); |
|||
case 'description': |
|||
return ( |
|||
<div className="max-w-xs truncate text-gray-600" title={configuration.description || ''}> |
|||
{configuration.description || '-'} |
|||
</div> |
|||
); |
|||
case 'configContent': |
|||
return <ConfigContentPreview content={configuration.configContent} />; |
|||
case 'isDisabled': |
|||
return <StatusBadge isDisabled={configuration.isDisabled} />; |
|||
case 'createdAt': |
|||
return <DateDisplay date={configuration.createdAt} />; |
|||
case 'updatedAt': |
|||
return <DateDisplay date={configuration.updatedAt} />; |
|||
case 'actions': |
|||
return ( |
|||
<div className="flex justify-end gap-4"> |
|||
<span |
|||
className="cursor-pointer text-blue-600 hover:underline select-none" |
|||
onClick={() => onEdit(configuration)} |
|||
> |
|||
修改 |
|||
</span> |
|||
<span |
|||
className="cursor-pointer text-red-500 hover:underline select-none" |
|||
onClick={() => onDelete(configuration)} |
|||
> |
|||
删除 |
|||
</span> |
|||
</div> |
|||
); |
|||
default: |
|||
return null; |
|||
} |
|||
}; |
|||
|
|||
const totalPages = Math.ceil(total / pageSize); |
|||
const Wrapper = hideCard ? React.Fragment : 'div'; |
|||
const wrapperProps = hideCard ? {} : { className: 'rounded-md border bg-background' }; |
|||
const rowClass = density === 'relaxed' ? 'h-20' : density === 'compact' ? 'h-8' : 'h-12'; |
|||
const cellPadding = density === 'relaxed' ? 'py-5' : density === 'compact' ? 'py-1' : 'py-3'; |
|||
|
|||
return ( |
|||
<Wrapper {...wrapperProps}> |
|||
<Table> |
|||
<TableHeader key="header"> |
|||
<TableRow className={rowClass}> |
|||
{visibleColumns.map(col => ( |
|||
<TableHead |
|||
key={col.key} |
|||
className={`text-foreground text-center ${col.key === 'actions' ? 'text-right' : ''} ${cellPadding}`} |
|||
> |
|||
{col.title} |
|||
</TableHead> |
|||
))} |
|||
</TableRow> |
|||
</TableHeader> |
|||
<TableBody key="body"> |
|||
{loading ? ( |
|||
<TableRow key="loading" className={rowClass}> |
|||
<TableCell colSpan={visibleColumns.length} className={`text-center text-muted-foreground ${cellPadding}`}> |
|||
加载中... |
|||
</TableCell> |
|||
</TableRow> |
|||
) : ranConfigurations.length === 0 ? ( |
|||
<TableRow key="empty" className={rowClass}> |
|||
<TableCell colSpan={visibleColumns.length} className={`text-center text-muted-foreground ${cellPadding}`}> |
|||
暂无数据 |
|||
</TableCell> |
|||
</TableRow> |
|||
) : ( |
|||
ranConfigurations.map((configuration) => ( |
|||
<TableRow key={configuration.ranConfigurationId} className={rowClass}> |
|||
{visibleColumns.map((column) => ( |
|||
<TableCell key={column.key} className={`text-foreground text-center ${cellPadding}`}> |
|||
{renderCell(configuration, column.key)} |
|||
</TableCell> |
|||
))} |
|||
</TableRow> |
|||
)) |
|||
)} |
|||
</TableBody> |
|||
</Table> |
|||
</Wrapper> |
|||
); |
|||
} |
@ -0,0 +1,356 @@ |
|||
import React, { useEffect, useState } from 'react'; |
|||
import { ranConfigurationService, RANConfiguration, GetRANConfigurationsRequest, CreateRANConfigurationRequest, UpdateRANConfigurationRequest } from '@/services/ranConfigurationService'; |
|||
import RANConfigurationsTable from './RANConfigurationsTable'; |
|||
import RANConfigurationForm from './RANConfigurationForm'; |
|||
import { Input } from '@/components/ui/input'; |
|||
import PaginationBar from '@/components/ui/PaginationBar'; |
|||
import TableToolbar, { DensityType } from '@/components/ui/TableToolbar'; |
|||
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog'; |
|||
import { Button } from '@/components/ui/button'; |
|||
import { ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons'; |
|||
import { useToast } from '@/components/ui/use-toast'; |
|||
|
|||
const defaultColumns = [ |
|||
{ key: 'name', title: '配置名称', visible: true }, |
|||
{ key: 'description', title: '描述', visible: true }, |
|||
{ key: 'configContent', title: '配置内容', visible: false }, |
|||
{ key: 'isDisabled', title: '状态', visible: true }, |
|||
{ key: 'createdAt', title: '创建时间', visible: true }, |
|||
{ key: 'actions', title: '操作', visible: true } |
|||
]; |
|||
|
|||
// 字段类型声明
|
|||
type SearchField = |
|||
| { key: string; label: string; type: 'input'; placeholder: string } |
|||
| { key: string; label: string; type: 'select'; options: { value: string; label: string }[] }; |
|||
|
|||
// 第一行字段(收起时只显示这3个)
|
|||
const firstRowFields: SearchField[] = [ |
|||
{ key: 'searchTerm', label: '搜索关键词', type: 'input', placeholder: '请输入配置名称或描述' }, |
|||
{ key: 'isDisabled', label: '状态', type: 'select', options: [ |
|||
{ value: '', label: '请选择' }, |
|||
{ value: 'false', label: '启用' }, |
|||
{ value: 'true', label: '禁用' }, |
|||
] }, |
|||
]; |
|||
|
|||
// 高级字段(展开时才显示)
|
|||
const advancedFields: SearchField[] = [ |
|||
{ key: 'pageSize', label: '每页数量', type: 'select', options: [ |
|||
{ value: '10', label: '10条/页' }, |
|||
{ value: '20', label: '20条/页' }, |
|||
{ value: '50', label: '50条/页' }, |
|||
{ value: '100', label: '100条/页' }, |
|||
] }, |
|||
]; |
|||
|
|||
export default function RANConfigurationsView() { |
|||
const [ranConfigurations, setRANConfigurations] = useState<RANConfiguration[]>([]); |
|||
const [loading, setLoading] = useState(false); |
|||
const [total, setTotal] = useState(0); |
|||
const [pageNumber, setPageNumber] = useState(1); |
|||
const [pageSize, setPageSize] = useState(10); |
|||
const [density, setDensity] = useState<DensityType>('default'); |
|||
const [columns, setColumns] = useState(defaultColumns); |
|||
const [showAdvanced, setShowAdvanced] = useState(false); |
|||
|
|||
// 搜索参数
|
|||
const [searchTerm, setSearchTerm] = useState(''); |
|||
const [isDisabled, setIsDisabled] = useState<boolean | undefined>(undefined); |
|||
|
|||
// 表单对话框状态
|
|||
const [open, setOpen] = useState(false); |
|||
const [editOpen, setEditOpen] = useState(false); |
|||
const [selectedConfiguration, setSelectedConfiguration] = useState<RANConfiguration | null>(null); |
|||
|
|||
// 提交状态
|
|||
const [isSubmitting, setIsSubmitting] = useState(false); |
|||
|
|||
// Toast 提示
|
|||
const { toast } = useToast(); |
|||
|
|||
const fetchRANConfigurations = async (params: Partial<GetRANConfigurationsRequest> = {}) => { |
|||
setLoading(true); |
|||
const queryParams: GetRANConfigurationsRequest = { |
|||
pageNumber, |
|||
pageSize, |
|||
searchTerm, |
|||
isDisabled, |
|||
...params |
|||
}; |
|||
|
|||
const result = await ranConfigurationService.getRANConfigurations(queryParams); |
|||
if (result.isSuccess && result.data) { |
|||
setRANConfigurations(result.data.items || []); |
|||
setTotal(result.data.totalCount || 0); |
|||
} |
|||
setLoading(false); |
|||
}; |
|||
|
|||
useEffect(() => { |
|||
fetchRANConfigurations(); |
|||
// eslint-disable-next-line
|
|||
}, [pageNumber, pageSize]); |
|||
|
|||
const handleEdit = (configuration: RANConfiguration) => { |
|||
setSelectedConfiguration(configuration); |
|||
setEditOpen(true); |
|||
}; |
|||
|
|||
const handleDelete = async (configuration: RANConfiguration) => { |
|||
if (confirm(`确定要删除RAN配置 "${configuration.name}" 吗?`)) { |
|||
try { |
|||
const result = await ranConfigurationService.deleteRANConfiguration(configuration.ranConfigurationId); |
|||
if (result.isSuccess) { |
|||
toast({ |
|||
title: "删除成功", |
|||
description: `RAN配置 "${configuration.name}" 删除成功`, |
|||
}); |
|||
fetchRANConfigurations(); |
|||
} else { |
|||
const errorMessage = result.errorMessages?.join(', ') || "删除RAN配置时发生错误"; |
|||
console.error('删除RAN配置失败:', errorMessage); |
|||
toast({ |
|||
title: "删除失败", |
|||
description: errorMessage, |
|||
variant: "destructive", |
|||
}); |
|||
} |
|||
} catch (error) { |
|||
console.error('删除RAN配置异常:', error); |
|||
toast({ |
|||
title: "删除失败", |
|||
description: "网络错误,请稍后重试", |
|||
variant: "destructive", |
|||
}); |
|||
} |
|||
} |
|||
}; |
|||
|
|||
const handleCreate = async (data: CreateRANConfigurationRequest) => { |
|||
if (isSubmitting) return; // 防止重复提交
|
|||
|
|||
console.log('开始创建RAN配置:', data); |
|||
setIsSubmitting(true); |
|||
try { |
|||
const result = await ranConfigurationService.createRANConfiguration(data); |
|||
console.log('创建RAN配置结果:', result); |
|||
|
|||
if (result.isSuccess) { |
|||
toast({ |
|||
title: "创建成功", |
|||
description: `RAN配置 "${data.name}" 创建成功`, |
|||
}); |
|||
setOpen(false); |
|||
fetchRANConfigurations(); |
|||
} else { |
|||
const errorMessage = result.errorMessages?.join(', ') || "创建RAN配置时发生错误"; |
|||
console.error('创建RAN配置失败:', errorMessage, result); |
|||
toast({ |
|||
title: "创建失败", |
|||
description: errorMessage, |
|||
variant: "destructive", |
|||
}); |
|||
} |
|||
} catch (error) { |
|||
console.error('创建RAN配置异常:', error); |
|||
toast({ |
|||
title: "创建失败", |
|||
description: "网络错误,请稍后重试", |
|||
variant: "destructive", |
|||
}); |
|||
} finally { |
|||
setIsSubmitting(false); |
|||
} |
|||
}; |
|||
|
|||
const handleUpdate = async (data: UpdateRANConfigurationRequest) => { |
|||
if (!selectedConfiguration || isSubmitting) return; // 防止重复提交
|
|||
|
|||
setIsSubmitting(true); |
|||
try { |
|||
const result = await ranConfigurationService.updateRANConfiguration(selectedConfiguration.ranConfigurationId, data); |
|||
if (result.isSuccess) { |
|||
toast({ |
|||
title: "更新成功", |
|||
description: `RAN配置 "${data.name}" 更新成功`, |
|||
}); |
|||
setEditOpen(false); |
|||
setSelectedConfiguration(null); |
|||
fetchRANConfigurations(); |
|||
} else { |
|||
const errorMessage = result.errorMessages?.join(', ') || "更新RAN配置时发生错误"; |
|||
console.error('更新RAN配置失败:', errorMessage); |
|||
toast({ |
|||
title: "更新失败", |
|||
description: errorMessage, |
|||
variant: "destructive", |
|||
}); |
|||
} |
|||
} catch (error) { |
|||
console.error('更新RAN配置异常:', error); |
|||
toast({ |
|||
title: "更新失败", |
|||
description: "网络错误,请稍后重试", |
|||
variant: "destructive", |
|||
}); |
|||
} finally { |
|||
setIsSubmitting(false); |
|||
} |
|||
}; |
|||
|
|||
// 查询按钮
|
|||
const handleQuery = () => { |
|||
setPageNumber(1); |
|||
fetchRANConfigurations({ pageNumber: 1 }); |
|||
}; |
|||
|
|||
// 重置按钮
|
|||
const handleReset = () => { |
|||
setSearchTerm(''); |
|||
setIsDisabled(undefined); |
|||
setPageNumber(1); |
|||
fetchRANConfigurations({ |
|||
searchTerm: '', |
|||
isDisabled: undefined, |
|||
pageNumber: 1 |
|||
}); |
|||
}; |
|||
|
|||
// 每页条数选择
|
|||
const handlePageSizeChange = (size: number) => { |
|||
setPageSize(size); |
|||
setPageNumber(1); |
|||
}; |
|||
|
|||
const totalPages = Math.ceil(total / pageSize); |
|||
|
|||
return ( |
|||
<main className="flex-1 p-4 transition-all duration-300 ease-in-out sm:p-6"> |
|||
<div className="w-full space-y-4"> |
|||
{/* 丰富美化后的搜索栏 */} |
|||
<div className="flex flex-col bg-white p-4 rounded-md border mb-2"> |
|||
<form |
|||
className={`grid gap-x-8 gap-y-4 items-center ${showAdvanced ? 'md:grid-cols-3' : 'md:grid-cols-3'} grid-cols-1`} |
|||
onSubmit={e => { |
|||
e.preventDefault(); |
|||
handleQuery(); |
|||
}} |
|||
> |
|||
{(showAdvanced ? [...firstRowFields, ...advancedFields] : firstRowFields).map(field => ( |
|||
<div className="flex flex-row items-center min-w-[200px] flex-1" key={field.key}> |
|||
<label |
|||
className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right" |
|||
style={{ width: 80, minWidth: 80 }} |
|||
> |
|||
{field.label}: |
|||
</label> |
|||
{field.type === 'input' && ( |
|||
<Input |
|||
className="input flex-1" |
|||
placeholder={field.placeholder} |
|||
value={field.key === 'searchTerm' ? searchTerm : ''} |
|||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { |
|||
if (field.key === 'searchTerm') setSearchTerm(e.target.value); |
|||
}} |
|||
/> |
|||
)} |
|||
{field.type === 'select' && ( |
|||
<select |
|||
className="input h-10 rounded border border-border bg-background px-3 text-sm flex-1" |
|||
value={field.key === 'isDisabled' ? (isDisabled === undefined ? '' : isDisabled.toString()) : |
|||
field.key === 'pageSize' ? pageSize.toString() : ''} |
|||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => { |
|||
if (field.key === 'isDisabled') { |
|||
const value = e.target.value; |
|||
setIsDisabled(value === '' ? undefined : value === 'true'); |
|||
} else if (field.key === 'pageSize') { |
|||
setPageSize(parseInt(e.target.value)); |
|||
} |
|||
}} |
|||
> |
|||
{field.options.map(opt => ( |
|||
<option value={opt.value} key={opt.value}>{opt.label}</option> |
|||
))} |
|||
</select> |
|||
)} |
|||
</div> |
|||
))} |
|||
{/* 按钮组直接作为表单项之一,紧跟在最后一个表单项后面 */} |
|||
<div className="flex flex-row items-center min-w-[200px] flex-1 justify-end gap-2"> |
|||
<button type="button" className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" onClick={handleReset}>重置</button> |
|||
<button type="submit" className="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700">查询</button> |
|||
<button type="button" className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" onClick={() => setShowAdvanced(v => !v)}> |
|||
{showAdvanced ? ( |
|||
<> |
|||
收起 <ChevronUpIcon className="inline-block ml-1 w-4 h-4 align-middle" /> |
|||
</> |
|||
) : ( |
|||
<> |
|||
展开 <ChevronDownIcon className="inline-block ml-1 w-4 h-4 align-middle" /> |
|||
</> |
|||
)} |
|||
</button> |
|||
</div> |
|||
</form> |
|||
</div> |
|||
{/* 表格整体卡片区域,包括工具栏、表格、分页 */} |
|||
<div className="rounded-md border bg-background p-4"> |
|||
{/* 顶部操作栏:添加RAN配置+工具栏 */} |
|||
<div className="flex items-center justify-between mb-2"> |
|||
<Dialog open={open} onOpenChange={setOpen}> |
|||
<DialogTrigger asChild> |
|||
<Button className="bg-primary text-primary-foreground hover:bg-primary/90">+ 添加RAN配置</Button> |
|||
</DialogTrigger> |
|||
<DialogContent className="bg-background"> |
|||
<RANConfigurationForm onSubmit={handleCreate} isSubmitting={isSubmitting} /> |
|||
</DialogContent> |
|||
</Dialog> |
|||
<TableToolbar |
|||
onRefresh={() => fetchRANConfigurations()} |
|||
onDensityChange={setDensity} |
|||
onColumnsChange={setColumns} |
|||
onColumnsReset={() => setColumns(defaultColumns)} |
|||
columns={columns} |
|||
density={density} |
|||
/> |
|||
</div> |
|||
{/* 表格区域 */} |
|||
<RANConfigurationsTable |
|||
ranConfigurations={ranConfigurations} |
|||
loading={loading} |
|||
onEdit={handleEdit} |
|||
onDelete={handleDelete} |
|||
page={pageNumber} |
|||
pageSize={pageSize} |
|||
total={total} |
|||
onPageChange={setPageNumber} |
|||
hideCard={true} |
|||
density={density} |
|||
columns={columns} |
|||
/> |
|||
{/* 分页 */} |
|||
<PaginationBar |
|||
page={pageNumber} |
|||
pageSize={pageSize} |
|||
total={total} |
|||
onPageChange={setPageNumber} |
|||
onPageSizeChange={handlePageSizeChange} |
|||
/> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* 编辑RAN配置对话框 */} |
|||
<Dialog open={editOpen} onOpenChange={setEditOpen}> |
|||
<DialogContent className="bg-background"> |
|||
<RANConfigurationForm |
|||
onSubmit={(data: CreateRANConfigurationRequest | UpdateRANConfigurationRequest) => handleUpdate(data as UpdateRANConfigurationRequest)} |
|||
initialData={selectedConfiguration || undefined} |
|||
isEdit={true} |
|||
isSubmitting={isSubmitting} |
|||
/> |
|||
</DialogContent> |
|||
</Dialog> |
|||
</main> |
|||
); |
|||
} |
@ -1,120 +0,0 @@ |
|||
import { httpClient } from '@/lib/http-client'; |
|||
import { OperationResult } from '@/types/auth'; |
|||
import { API_PATHS } from '@/constants/api'; |
|||
|
|||
// 配置类型
|
|||
export type ConfigType = 'system' | 'network' | 'security' | 'communication' | 'monitoring'; |
|||
|
|||
// 配置接口定义
|
|||
export interface Config { |
|||
id: string; |
|||
configId: string; |
|||
configName: string; |
|||
configType: ConfigType; |
|||
description: string; |
|||
isActive: boolean; |
|||
isDefault: boolean; |
|||
version: string; |
|||
parameters: { |
|||
[key: string]: any; |
|||
}; |
|||
networkSettings: { |
|||
ipAddress: string; |
|||
subnet: string; |
|||
gateway: string; |
|||
dns: string[]; |
|||
port: number; |
|||
}; |
|||
securitySettings: { |
|||
encryptionEnabled: boolean; |
|||
authenticationRequired: boolean; |
|||
sslEnabled: boolean; |
|||
certificatePath: string; |
|||
keyPath: string; |
|||
}; |
|||
communicationSettings: { |
|||
protocol: string; |
|||
timeout: number; |
|||
retryCount: number; |
|||
heartbeatInterval: number; |
|||
}; |
|||
monitoringSettings: { |
|||
dataCollectionInterval: number; |
|||
alertThresholds: { |
|||
[key: string]: number; |
|||
}; |
|||
logLevel: 'debug' | 'info' | 'warn' | 'error'; |
|||
}; |
|||
appliedTo: string[]; // 应用此配置的设备ID列表
|
|||
createdAt: string; |
|||
updatedAt: string; |
|||
createdBy: string; |
|||
updatedBy: string; |
|||
} |
|||
|
|||
// 获取配置列表请求接口
|
|||
export interface GetAllConfigsRequest { |
|||
configId?: string; |
|||
configName?: string; |
|||
configType?: ConfigType; |
|||
isActive?: boolean; |
|||
isDefault?: boolean; |
|||
version?: string; |
|||
createdBy?: string; |
|||
page?: number; |
|||
pageSize?: number; |
|||
} |
|||
|
|||
// 获取配置列表响应接口
|
|||
export interface GetAllConfigsResponse { |
|||
configs: Config[]; |
|||
totalCount: number; |
|||
} |
|||
|
|||
class ConfigService { |
|||
private readonly baseUrl = API_PATHS.CONFIGS; |
|||
|
|||
// 获取所有配置
|
|||
async getAllConfigs(params: GetAllConfigsRequest = {}): Promise<OperationResult<GetAllConfigsResponse>> { |
|||
const queryParams = new URLSearchParams(); |
|||
|
|||
if (params.configId) queryParams.append('configId', params.configId); |
|||
if (params.configName) queryParams.append('configName', params.configName); |
|||
if (params.configType) queryParams.append('configType', params.configType); |
|||
if (params.isActive !== undefined) queryParams.append('isActive', params.isActive.toString()); |
|||
if (params.isDefault !== undefined) queryParams.append('isDefault', params.isDefault.toString()); |
|||
if (params.version) queryParams.append('version', params.version); |
|||
if (params.createdBy) queryParams.append('createdBy', params.createdBy); |
|||
if (params.page) queryParams.append('page', params.page.toString()); |
|||
if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString()); |
|||
|
|||
const url = `${this.baseUrl}?${queryParams.toString()}`; |
|||
return httpClient.get<GetAllConfigsResponse>(url); |
|||
} |
|||
|
|||
// 根据ID获取配置
|
|||
async getConfigById(id: string): Promise<OperationResult<Config>> { |
|||
return httpClient.get<Config>(`${this.baseUrl}/${id}`); |
|||
} |
|||
|
|||
// 验证配置
|
|||
async validateConfig(configId: string): Promise<OperationResult<{ |
|||
valid: boolean; |
|||
errors: string[]; |
|||
warnings: string[]; |
|||
}>> { |
|||
return httpClient.post(`${this.baseUrl}/${configId}/validate`); |
|||
} |
|||
|
|||
// 应用配置到设备
|
|||
async applyConfigToDevice(configId: string, deviceIds: string[]): Promise<OperationResult<void>> { |
|||
return httpClient.post(`${this.baseUrl}/${configId}/apply`, { deviceIds }); |
|||
} |
|||
|
|||
// 导出配置列表
|
|||
async exportConfigs(format: 'pdf' | 'excel' | 'csv', filters?: any): Promise<OperationResult<{ downloadUrl: string }>> { |
|||
return httpClient.post(`${this.baseUrl}/export`, { format, filters }); |
|||
} |
|||
} |
|||
|
|||
export const configService = new ConfigService(); |
@ -0,0 +1,111 @@ |
|||
import { httpClient } from '@/lib/http-client'; |
|||
import { OperationResult } from '@/types/auth'; |
|||
import { API_PATHS } from '@/constants/api'; |
|||
|
|||
// 核心网络配置接口定义
|
|||
export interface CoreNetworkConfig { |
|||
coreNetworkConfigId: string; |
|||
name: string; |
|||
configContent: string; |
|||
description?: string; |
|||
isDisabled: boolean; |
|||
createdAt: string; |
|||
updatedAt?: string; |
|||
createdBy: string; |
|||
updatedBy: string; |
|||
} |
|||
|
|||
// 获取核心网络配置列表请求接口
|
|||
export interface GetCoreNetworkConfigsRequest { |
|||
pageNumber?: number; |
|||
pageSize?: number; |
|||
searchTerm?: string; |
|||
isDisabled?: boolean; |
|||
} |
|||
|
|||
// 获取核心网络配置列表响应接口
|
|||
export interface GetCoreNetworkConfigsResponse { |
|||
totalCount: number; |
|||
pageNumber: number; |
|||
pageSize: number; |
|||
totalPages: number; |
|||
hasPreviousPage: boolean; |
|||
hasNextPage: boolean; |
|||
items: CoreNetworkConfig[]; |
|||
} |
|||
|
|||
// 创建核心网络配置请求接口
|
|||
export interface CreateCoreNetworkConfigRequest { |
|||
name: string; |
|||
configContent: string; |
|||
description?: string; |
|||
isDisabled?: boolean; |
|||
} |
|||
|
|||
// 创建核心网络配置响应接口
|
|||
export interface CreateCoreNetworkConfigResponse { |
|||
coreNetworkConfigId: string; |
|||
name: string; |
|||
configContent: string; |
|||
description?: string; |
|||
isDisabled: boolean; |
|||
createdAt: string; |
|||
} |
|||
|
|||
// 更新核心网络配置请求接口
|
|||
export interface UpdateCoreNetworkConfigRequest { |
|||
coreNetworkConfigId: string; |
|||
name: string; |
|||
configContent: string; |
|||
description?: string; |
|||
isDisabled?: boolean; |
|||
} |
|||
|
|||
// 更新核心网络配置响应接口
|
|||
export interface UpdateCoreNetworkConfigResponse { |
|||
coreNetworkConfigId: string; |
|||
name: string; |
|||
configContent: string; |
|||
description?: string; |
|||
isDisabled: boolean; |
|||
updatedAt: string; |
|||
} |
|||
|
|||
class CoreNetworkConfigService { |
|||
private readonly baseUrl = API_PATHS.CORE_NETWORK_CONFIGS; |
|||
|
|||
// 获取核心网络配置列表
|
|||
async getCoreNetworkConfigs(params: GetCoreNetworkConfigsRequest = {}): Promise<OperationResult<GetCoreNetworkConfigsResponse>> { |
|||
const queryParams = new URLSearchParams(); |
|||
|
|||
if (params.pageNumber) queryParams.append('pageNumber', params.pageNumber.toString()); |
|||
if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString()); |
|||
if (params.searchTerm) queryParams.append('searchTerm', params.searchTerm); |
|||
if (params.isDisabled !== undefined) queryParams.append('isDisabled', params.isDisabled.toString()); |
|||
|
|||
const url = `${this.baseUrl}?${queryParams.toString()}`; |
|||
return httpClient.get<GetCoreNetworkConfigsResponse>(url); |
|||
} |
|||
|
|||
// 根据ID获取核心网络配置详情
|
|||
async getCoreNetworkConfigById(id: string): Promise<OperationResult<CoreNetworkConfig>> { |
|||
return httpClient.get<CoreNetworkConfig>(`${this.baseUrl}/${id}`); |
|||
} |
|||
|
|||
// 创建核心网络配置
|
|||
async createCoreNetworkConfig(data: CreateCoreNetworkConfigRequest): Promise<OperationResult<CreateCoreNetworkConfigResponse>> { |
|||
return httpClient.post<CreateCoreNetworkConfigResponse>(this.baseUrl, data); |
|||
} |
|||
|
|||
// 更新核心网络配置
|
|||
async updateCoreNetworkConfig(id: string, data: UpdateCoreNetworkConfigRequest): Promise<OperationResult<UpdateCoreNetworkConfigResponse>> { |
|||
return httpClient.put<UpdateCoreNetworkConfigResponse>(`${this.baseUrl}/${id}`, data); |
|||
} |
|||
|
|||
// 删除核心网络配置
|
|||
async deleteCoreNetworkConfig(id: string): Promise<OperationResult<boolean>> { |
|||
return httpClient.delete<boolean>(`${this.baseUrl}/${id}`); |
|||
} |
|||
} |
|||
|
|||
export const coreNetworkConfigService = new CoreNetworkConfigService(); |
@ -0,0 +1,111 @@ |
|||
import { httpClient } from '@/lib/http-client'; |
|||
import { OperationResult } from '@/types/auth'; |
|||
import { API_PATHS } from '@/constants/api'; |
|||
|
|||
// IMS配置接口定义
|
|||
export interface IMSConfiguration { |
|||
imsConfigurationId: string; |
|||
name: string; |
|||
configContent: string; |
|||
description?: string; |
|||
isDisabled: boolean; |
|||
createdAt: string; |
|||
updatedAt?: string; |
|||
createdBy: string; |
|||
updatedBy: string; |
|||
} |
|||
|
|||
// 获取IMS配置列表请求接口
|
|||
export interface GetIMSConfigurationsRequest { |
|||
pageNumber?: number; |
|||
pageSize?: number; |
|||
searchTerm?: string; |
|||
isDisabled?: boolean; |
|||
} |
|||
|
|||
// 获取IMS配置列表响应接口
|
|||
export interface GetIMSConfigurationsResponse { |
|||
totalCount: number; |
|||
pageNumber: number; |
|||
pageSize: number; |
|||
totalPages: number; |
|||
hasPreviousPage: boolean; |
|||
hasNextPage: boolean; |
|||
items: IMSConfiguration[]; |
|||
} |
|||
|
|||
// 创建IMS配置请求接口
|
|||
export interface CreateIMSConfigurationRequest { |
|||
name: string; |
|||
configContent: string; |
|||
description?: string; |
|||
isDisabled?: boolean; |
|||
} |
|||
|
|||
// 创建IMS配置响应接口
|
|||
export interface CreateIMSConfigurationResponse { |
|||
imsConfigurationId: string; |
|||
name: string; |
|||
configContent: string; |
|||
description?: string; |
|||
isDisabled: boolean; |
|||
createdAt: string; |
|||
} |
|||
|
|||
// 更新IMS配置请求接口
|
|||
export interface UpdateIMSConfigurationRequest { |
|||
imsConfigurationId: string; |
|||
name: string; |
|||
configContent: string; |
|||
description?: string; |
|||
isDisabled?: boolean; |
|||
} |
|||
|
|||
// 更新IMS配置响应接口
|
|||
export interface UpdateIMSConfigurationResponse { |
|||
imsConfigurationId: string; |
|||
name: string; |
|||
configContent: string; |
|||
description?: string; |
|||
isDisabled: boolean; |
|||
updatedAt: string; |
|||
} |
|||
|
|||
class IMSConfigurationService { |
|||
private readonly baseUrl = API_PATHS.IMS_CONFIGURATIONS; |
|||
|
|||
// 获取IMS配置列表
|
|||
async getIMSConfigurations(params: GetIMSConfigurationsRequest = {}): Promise<OperationResult<GetIMSConfigurationsResponse>> { |
|||
const queryParams = new URLSearchParams(); |
|||
|
|||
if (params.pageNumber) queryParams.append('pageNumber', params.pageNumber.toString()); |
|||
if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString()); |
|||
if (params.searchTerm) queryParams.append('searchTerm', params.searchTerm); |
|||
if (params.isDisabled !== undefined) queryParams.append('isDisabled', params.isDisabled.toString()); |
|||
|
|||
const url = `${this.baseUrl}?${queryParams.toString()}`; |
|||
return httpClient.get<GetIMSConfigurationsResponse>(url); |
|||
} |
|||
|
|||
// 根据ID获取IMS配置详情
|
|||
async getIMSConfigurationById(id: string): Promise<OperationResult<IMSConfiguration>> { |
|||
return httpClient.get<IMSConfiguration>(`${this.baseUrl}/${id}`); |
|||
} |
|||
|
|||
// 创建IMS配置
|
|||
async createIMSConfiguration(data: CreateIMSConfigurationRequest): Promise<OperationResult<CreateIMSConfigurationResponse>> { |
|||
return httpClient.post<CreateIMSConfigurationResponse>(this.baseUrl, data); |
|||
} |
|||
|
|||
// 更新IMS配置
|
|||
async updateIMSConfiguration(id: string, data: UpdateIMSConfigurationRequest): Promise<OperationResult<UpdateIMSConfigurationResponse>> { |
|||
return httpClient.put<UpdateIMSConfigurationResponse>(`${this.baseUrl}/${id}`, data); |
|||
} |
|||
|
|||
// 删除IMS配置
|
|||
async deleteIMSConfiguration(id: string): Promise<OperationResult<boolean>> { |
|||
return httpClient.delete<boolean>(`${this.baseUrl}/${id}`); |
|||
} |
|||
} |
|||
|
|||
export const imsConfigurationService = new IMSConfigurationService(); |
@ -0,0 +1,112 @@ |
|||
import { httpClient } from '@/lib/http-client'; |
|||
import { OperationResult } from '@/types/auth'; |
|||
import { API_PATHS } from '@/constants/api'; |
|||
|
|||
// 网络栈配置接口定义
|
|||
export interface NetworkStackConfig { |
|||
networkStackConfigId: string; |
|||
stackId: string; |
|||
ranId?: string; |
|||
description?: string; |
|||
isActive: boolean; |
|||
createdAt: string; |
|||
updatedAt?: string; |
|||
createdBy: string; |
|||
updatedBy?: string; |
|||
} |
|||
|
|||
// 获取网络栈配置列表请求接口
|
|||
export interface GetNetworkStackConfigsRequest { |
|||
pageNumber?: number; |
|||
pageSize?: number; |
|||
searchTerm?: string; |
|||
isActive?: boolean; |
|||
ranId?: string; |
|||
} |
|||
|
|||
// 获取网络栈配置列表响应接口
|
|||
export interface GetNetworkStackConfigsResponse { |
|||
totalCount: number; |
|||
pageNumber: number; |
|||
pageSize: number; |
|||
totalPages: number; |
|||
hasPreviousPage: boolean; |
|||
hasNextPage: boolean; |
|||
items: NetworkStackConfig[]; |
|||
} |
|||
|
|||
// 创建网络栈配置请求接口
|
|||
export interface CreateNetworkStackConfigRequest { |
|||
stackId: string; |
|||
ranId?: string; |
|||
description?: string; |
|||
isActive?: boolean; |
|||
} |
|||
|
|||
// 创建网络栈配置响应接口
|
|||
export interface CreateNetworkStackConfigResponse { |
|||
networkStackConfigId: string; |
|||
stackId: string; |
|||
ranId?: string; |
|||
description?: string; |
|||
isActive: boolean; |
|||
createdAt: string; |
|||
} |
|||
|
|||
// 更新网络栈配置请求接口
|
|||
export interface UpdateNetworkStackConfigRequest { |
|||
networkStackConfigId: string; |
|||
ranId?: string; |
|||
description?: string; |
|||
isActive?: boolean; |
|||
} |
|||
|
|||
// 更新网络栈配置响应接口
|
|||
export interface UpdateNetworkStackConfigResponse { |
|||
networkStackConfigId: string; |
|||
stackId: string; |
|||
ranId?: string; |
|||
description?: string; |
|||
isActive: boolean; |
|||
updatedAt: string; |
|||
} |
|||
|
|||
class NetworkStackConfigService { |
|||
private readonly baseUrl = API_PATHS.NETWORK_STACK_CONFIGS; |
|||
|
|||
// 获取网络栈配置列表
|
|||
async getNetworkStackConfigs(params: GetNetworkStackConfigsRequest = {}): Promise<OperationResult<GetNetworkStackConfigsResponse>> { |
|||
const queryParams = new URLSearchParams(); |
|||
|
|||
if (params.pageNumber) queryParams.append('pageNumber', params.pageNumber.toString()); |
|||
if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString()); |
|||
if (params.searchTerm) queryParams.append('searchTerm', params.searchTerm); |
|||
if (params.isActive !== undefined) queryParams.append('isActive', params.isActive.toString()); |
|||
if (params.ranId) queryParams.append('ranId', params.ranId); |
|||
|
|||
const url = `${this.baseUrl}?${queryParams.toString()}`; |
|||
return httpClient.get<GetNetworkStackConfigsResponse>(url); |
|||
} |
|||
|
|||
// 根据ID获取网络栈配置详情
|
|||
async getNetworkStackConfigById(id: string): Promise<OperationResult<NetworkStackConfig>> { |
|||
return httpClient.get<NetworkStackConfig>(`${this.baseUrl}/${id}`); |
|||
} |
|||
|
|||
// 创建网络栈配置
|
|||
async createNetworkStackConfig(data: CreateNetworkStackConfigRequest): Promise<OperationResult<CreateNetworkStackConfigResponse>> { |
|||
return httpClient.post<CreateNetworkStackConfigResponse>(this.baseUrl, data); |
|||
} |
|||
|
|||
// 更新网络栈配置
|
|||
async updateNetworkStackConfig(id: string, data: UpdateNetworkStackConfigRequest): Promise<OperationResult<UpdateNetworkStackConfigResponse>> { |
|||
return httpClient.put<UpdateNetworkStackConfigResponse>(`${this.baseUrl}/${id}`, data); |
|||
} |
|||
|
|||
// 删除网络栈配置
|
|||
async deleteNetworkStackConfig(id: string): Promise<OperationResult<boolean>> { |
|||
return httpClient.delete<boolean>(`${this.baseUrl}/${id}`); |
|||
} |
|||
} |
|||
|
|||
export const networkStackConfigService = new NetworkStackConfigService(); |
@ -0,0 +1,109 @@ |
|||
import { httpClient } from '@/lib/http-client'; |
|||
import { OperationResult } from '@/types/auth'; |
|||
import { API_PATHS } from '@/constants/api'; |
|||
|
|||
// RAN配置接口定义
|
|||
export interface RANConfiguration { |
|||
ranConfigurationId: string; |
|||
name: string; |
|||
configContent: string; |
|||
description?: string; |
|||
isDisabled: boolean; |
|||
createdAt: string; |
|||
updatedAt?: string; |
|||
} |
|||
|
|||
// 获取RAN配置列表请求接口
|
|||
export interface GetRANConfigurationsRequest { |
|||
pageNumber?: number; |
|||
pageSize?: number; |
|||
searchTerm?: string; |
|||
isDisabled?: boolean; |
|||
} |
|||
|
|||
// 获取RAN配置列表响应接口
|
|||
export interface GetRANConfigurationsResponse { |
|||
totalCount: number; |
|||
pageNumber: number; |
|||
pageSize: number; |
|||
totalPages: number; |
|||
hasPreviousPage: boolean; |
|||
hasNextPage: boolean; |
|||
items: RANConfiguration[]; |
|||
} |
|||
|
|||
// 创建RAN配置请求接口
|
|||
export interface CreateRANConfigurationRequest { |
|||
name: string; |
|||
configContent: string; |
|||
description?: string; |
|||
isDisabled?: boolean; |
|||
} |
|||
|
|||
// 创建RAN配置响应接口
|
|||
export interface CreateRANConfigurationResponse { |
|||
ranConfigurationId: string; |
|||
name: string; |
|||
configContent: string; |
|||
description?: string; |
|||
isDisabled: boolean; |
|||
createdAt: string; |
|||
} |
|||
|
|||
// 更新RAN配置请求接口
|
|||
export interface UpdateRANConfigurationRequest { |
|||
ranConfigurationId: string; |
|||
name: string; |
|||
configContent: string; |
|||
description?: string; |
|||
isDisabled?: boolean; |
|||
} |
|||
|
|||
// 更新RAN配置响应接口
|
|||
export interface UpdateRANConfigurationResponse { |
|||
ranConfigurationId: string; |
|||
name: string; |
|||
configContent: string; |
|||
description?: string; |
|||
isDisabled: boolean; |
|||
updatedAt: string; |
|||
} |
|||
|
|||
class RANConfigurationService { |
|||
private readonly baseUrl = API_PATHS.RAN_CONFIGURATIONS; |
|||
|
|||
// 获取RAN配置列表
|
|||
async getRANConfigurations(params: GetRANConfigurationsRequest = {}): Promise<OperationResult<GetRANConfigurationsResponse>> { |
|||
const queryParams = new URLSearchParams(); |
|||
|
|||
if (params.pageNumber) queryParams.append('pageNumber', params.pageNumber.toString()); |
|||
if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString()); |
|||
if (params.searchTerm) queryParams.append('searchTerm', params.searchTerm); |
|||
if (params.isDisabled !== undefined) queryParams.append('isDisabled', params.isDisabled.toString()); |
|||
|
|||
const url = `${this.baseUrl}?${queryParams.toString()}`; |
|||
return httpClient.get<GetRANConfigurationsResponse>(url); |
|||
} |
|||
|
|||
// 根据ID获取RAN配置详情
|
|||
async getRANConfigurationById(id: string): Promise<OperationResult<RANConfiguration>> { |
|||
return httpClient.get<RANConfiguration>(`${this.baseUrl}/${id}`); |
|||
} |
|||
|
|||
// 创建RAN配置
|
|||
async createRANConfiguration(data: CreateRANConfigurationRequest): Promise<OperationResult<CreateRANConfigurationResponse>> { |
|||
return httpClient.post<CreateRANConfigurationResponse>(this.baseUrl, data); |
|||
} |
|||
|
|||
// 更新RAN配置
|
|||
async updateRANConfiguration(id: string, data: UpdateRANConfigurationRequest): Promise<OperationResult<UpdateRANConfigurationResponse>> { |
|||
return httpClient.put<UpdateRANConfigurationResponse>(`${this.baseUrl}/${id}`, data); |
|||
} |
|||
|
|||
// 删除RAN配置
|
|||
async deleteRANConfiguration(id: string): Promise<OperationResult<boolean>> { |
|||
return httpClient.delete<boolean>(`${this.baseUrl}/${id}`); |
|||
} |
|||
} |
|||
|
|||
export const ranConfigurationService = new RANConfigurationService(); |
@ -0,0 +1,112 @@ |
|||
import { httpClient } from '@/lib/http-client'; |
|||
import { OperationResult } from '@/types/auth'; |
|||
import { API_PATHS } from '@/constants/api'; |
|||
|
|||
// 栈核心网IMS绑定接口定义
|
|||
export interface StackCoreIMSBinding { |
|||
stackCoreIMSBindingId: string; |
|||
stackId: string; |
|||
index: number; |
|||
cnId: string; |
|||
imsId: string; |
|||
createdAt: string; |
|||
updatedAt?: string; |
|||
createdBy: string; |
|||
updatedBy: string; |
|||
} |
|||
|
|||
// 获取栈核心网IMS绑定列表请求接口
|
|||
export interface GetStackCoreIMSBindingsRequest { |
|||
pageNumber?: number; |
|||
pageSize?: number; |
|||
stackId?: string; |
|||
cnId?: string; |
|||
imsId?: string; |
|||
} |
|||
|
|||
// 获取栈核心网IMS绑定列表响应接口
|
|||
export interface GetStackCoreIMSBindingsResponse { |
|||
totalCount: number; |
|||
pageNumber: number; |
|||
pageSize: number; |
|||
totalPages: number; |
|||
hasPreviousPage: boolean; |
|||
hasNextPage: boolean; |
|||
items: StackCoreIMSBinding[]; |
|||
} |
|||
|
|||
// 创建栈核心网IMS绑定请求接口
|
|||
export interface CreateStackCoreIMSBindingRequest { |
|||
stackId: string; |
|||
index: number; |
|||
cnId: string; |
|||
imsId: string; |
|||
} |
|||
|
|||
// 创建栈核心网IMS绑定响应接口
|
|||
export interface CreateStackCoreIMSBindingResponse { |
|||
stackCoreIMSBindingId: string; |
|||
stackId: string; |
|||
index: number; |
|||
cnId: string; |
|||
imsId: string; |
|||
createdAt: string; |
|||
} |
|||
|
|||
// 更新栈核心网IMS绑定请求接口
|
|||
export interface UpdateStackCoreIMSBindingRequest { |
|||
stackCoreIMSBindingId: string; |
|||
index: number; |
|||
cnId: string; |
|||
imsId: string; |
|||
} |
|||
|
|||
// 更新栈核心网IMS绑定响应接口
|
|||
export interface UpdateStackCoreIMSBindingResponse { |
|||
stackCoreIMSBindingId: string; |
|||
stackId: string; |
|||
index: number; |
|||
cnId: string; |
|||
imsId: string; |
|||
updatedAt: string; |
|||
} |
|||
|
|||
class StackCoreIMSBindingService { |
|||
private readonly baseUrl = API_PATHS.STACK_CORE_IMS_BINDINGS; |
|||
|
|||
// 获取栈核心网IMS绑定列表
|
|||
async getStackCoreIMSBindings(params: GetStackCoreIMSBindingsRequest = {}): Promise<OperationResult<GetStackCoreIMSBindingsResponse>> { |
|||
const queryParams = new URLSearchParams(); |
|||
|
|||
if (params.pageNumber) queryParams.append('pageNumber', params.pageNumber.toString()); |
|||
if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString()); |
|||
if (params.stackId) queryParams.append('stackId', params.stackId); |
|||
if (params.cnId) queryParams.append('cnId', params.cnId); |
|||
if (params.imsId) queryParams.append('imsId', params.imsId); |
|||
|
|||
const url = `${this.baseUrl}?${queryParams.toString()}`; |
|||
return httpClient.get<GetStackCoreIMSBindingsResponse>(url); |
|||
} |
|||
|
|||
// 根据ID获取栈核心网IMS绑定详情
|
|||
async getStackCoreIMSBindingById(id: string): Promise<OperationResult<StackCoreIMSBinding>> { |
|||
return httpClient.get<StackCoreIMSBinding>(`${this.baseUrl}/${id}`); |
|||
} |
|||
|
|||
// 创建栈核心网IMS绑定
|
|||
async createStackCoreIMSBinding(data: CreateStackCoreIMSBindingRequest): Promise<OperationResult<CreateStackCoreIMSBindingResponse>> { |
|||
return httpClient.post<CreateStackCoreIMSBindingResponse>(this.baseUrl, data); |
|||
} |
|||
|
|||
// 更新栈核心网IMS绑定
|
|||
async updateStackCoreIMSBinding(id: string, data: UpdateStackCoreIMSBindingRequest): Promise<OperationResult<UpdateStackCoreIMSBindingResponse>> { |
|||
return httpClient.put<UpdateStackCoreIMSBindingResponse>(`${this.baseUrl}/${id}`, data); |
|||
} |
|||
|
|||
// 删除栈核心网IMS绑定
|
|||
async deleteStackCoreIMSBinding(id: string): Promise<OperationResult<boolean>> { |
|||
return httpClient.delete<boolean>(`${this.baseUrl}/${id}`); |
|||
} |
|||
} |
|||
|
|||
export const stackCoreIMSBindingService = new StackCoreIMSBindingService(); |
Loading…
Reference in new issue