Browse Source

feat: 实现网络栈配置管理页面,重构路由和菜单结构

- 新增网络栈配置管理页面(RAN配置、IMS配置、核心网络配置、网络栈配置)
- 将网络栈配置从仪表管理中分离,创建独立的路由组和菜单项
- 添加网络栈配置相关权限到AuthContext
- 更新路由配置和菜单配置
- 完善修改记录文档
feature/x1-web-request
hyh 7 days ago
parent
commit
46dd653605
  1. 4
      src/X1.WebAPI/Properties/launchSettings.json
  2. 7
      src/X1.WebAPI/appsettings.json
  3. 3187
      src/X1.WebAPI/logs/app-20250728.log
  4. 1456
      src/X1.WebAPI/logs/error-20250728.log
  5. 2
      src/X1.WebUI/src/config/core/env.config.ts
  6. 16
      src/X1.WebUI/src/constants/api.ts
  7. 40
      src/X1.WebUI/src/constants/menuConfig.ts
  8. 11
      src/X1.WebUI/src/contexts/AuthContext.tsx
  9. 251
      src/X1.WebUI/src/pages/configs/ConfigsTable.tsx
  10. 217
      src/X1.WebUI/src/pages/configs/ConfigsView.tsx
  11. 90
      src/X1.WebUI/src/pages/core-network-configs/CoreNetworkConfigForm.tsx
  12. 173
      src/X1.WebUI/src/pages/core-network-configs/CoreNetworkConfigsTable.tsx
  13. 356
      src/X1.WebUI/src/pages/core-network-configs/CoreNetworkConfigsView.tsx
  14. 90
      src/X1.WebUI/src/pages/ims-configurations/IMSConfigurationForm.tsx
  15. 173
      src/X1.WebUI/src/pages/ims-configurations/IMSConfigurationsTable.tsx
  16. 356
      src/X1.WebUI/src/pages/ims-configurations/IMSConfigurationsView.tsx
  17. 90
      src/X1.WebUI/src/pages/network-stack-configs/NetworkStackConfigForm.tsx
  18. 165
      src/X1.WebUI/src/pages/network-stack-configs/NetworkStackConfigsTable.tsx
  19. 356
      src/X1.WebUI/src/pages/network-stack-configs/NetworkStackConfigsView.tsx
  20. 90
      src/X1.WebUI/src/pages/ran-configurations/RANConfigurationForm.tsx
  21. 173
      src/X1.WebUI/src/pages/ran-configurations/RANConfigurationsTable.tsx
  22. 356
      src/X1.WebUI/src/pages/ran-configurations/RANConfigurationsView.tsx
  23. 43
      src/X1.WebUI/src/routes/AppRouter.tsx
  24. 120
      src/X1.WebUI/src/services/configService.ts
  25. 111
      src/X1.WebUI/src/services/coreNetworkConfigService.ts
  26. 111
      src/X1.WebUI/src/services/imsConfigurationService.ts
  27. 1
      src/X1.WebUI/src/services/instrumentService.ts
  28. 112
      src/X1.WebUI/src/services/networkStackConfigService.ts
  29. 109
      src/X1.WebUI/src/services/ranConfigurationService.ts
  30. 112
      src/X1.WebUI/src/services/stackCoreIMSBindingService.ts
  31. 746
      src/modify.md

4
src/X1.WebAPI/Properties/launchSettings.json

@ -24,8 +24,8 @@
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
//"applicationUrl": "https://localhost:7268;http://localhost:5000;https://192.168.3.147:7268;http://192.168.3.147:5000",
"applicationUrl": "https://localhost:7268;http://localhost:5000",
"applicationUrl": "https://localhost:7268;http://localhost:5000;https://192.168.2.142:7268;http://192.168.2.142:5000",
//"applicationUrl": "https://localhost:7268;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}

7
src/X1.WebAPI/appsettings.json

@ -52,10 +52,11 @@
"https://192.168.10.2:5173",
"http://192.168.11.2:5173",
"https://192.168.11.2:5173",
"http://192.168.12.3:5173",
"https://192.168.12.3:5173",
"http://192.168.2.142:5173",
"https://192.168.2.142:5173",
"http://localhost:5000",
"https://localhost:7268"
"https://localhost:7268",
"https://192.168.2.142:7268"
],
"AllowedMethods": [
"GET",

3187
src/X1.WebAPI/logs/app-20250728.log

File diff suppressed because it is too large

1456
src/X1.WebAPI/logs/error-20250728.log

File diff suppressed because it is too large

2
src/X1.WebUI/src/config/core/env.config.ts

@ -4,7 +4,7 @@ import type { ApiConfig, AuthConfig, AppConfig, MockConfig, Environment } from '
// 默认配置
const DEFAULT_CONFIG = {
// API配置
VITE_API_BASE_URL: 'https://localhost:7268/api',
VITE_API_BASE_URL: 'https://192.168.2.142:7268/api',
VITE_API_TIMEOUT: '30000',
VITE_API_VERSION: 'v1',
VITE_API_MAX_RETRIES: '3',

16
src/X1.WebUI/src/constants/api.ts

@ -6,8 +6,20 @@ export const API_PATHS = {
// 协议相关
PROTOCOLS: '/protocolversions',
// 配置相关
CONFIGS: '/instruments/configs',
// RAN配置相关
RAN_CONFIGURATIONS: '/ranconfigurations',
// IMS配置相关
IMS_CONFIGURATIONS: '/imsconfigurations',
// 核心网络配置相关
CORE_NETWORK_CONFIGS: '/corenetworkconfigs',
// 栈核心网IMS绑定相关
STACK_CORE_IMS_BINDINGS: '/stackcoreimsbindings',
// 网络栈配置相关
NETWORK_STACK_CONFIGS: '/networkstackconfigs',
// 用户相关
USERS: '/users',

40
src/X1.WebUI/src/constants/menuConfig.ts

@ -46,8 +46,15 @@ export type Permission =
| 'devices.manage'
| 'protocols.view'
| 'protocols.manage'
| 'configs.view'
| 'configs.manage';
| 'ranconfigurations.view'
| 'ranconfigurations.manage'
| 'imsconfigurations.view'
| 'imsconfigurations.manage'
| 'corenetworkconfigs.view'
| 'corenetworkconfigs.manage'
| 'networkstackconfigs.view'
| 'networkstackconfigs.manage'
export interface MenuItem {
title: string;
@ -180,11 +187,34 @@ export const menuItems: MenuItem[] = [
title: '协议列表',
href: '/dashboard/instruments/protocols',
permission: 'protocols.view',
}
],
},
{
title: '网络栈配置',
icon: Gauge,
href: '/dashboard/network-stack-configs',
permission: 'ranconfigurations.view',
children: [
{
title: 'RAN配置',
href: '/dashboard/network-stack-configs/ran-configurations',
permission: 'ranconfigurations.view',
},
{
title: 'IMS配置',
href: '/dashboard/network-stack-configs/ims-configurations',
permission: 'imsconfigurations.view',
},
{
title: '核心网络配置',
href: '/dashboard/network-stack-configs/core-network-configs',
permission: 'corenetworkconfigs.view',
},
{
title: '配置列表',
href: '/dashboard/instruments/configs',
permission: 'configs.view',
title: '网络栈配置',
href: '/dashboard/network-stack-configs/network-stack-configs',
permission: 'networkstackconfigs.view',
},
],
},

11
src/X1.WebUI/src/contexts/AuthContext.tsx

@ -73,8 +73,15 @@ const getDefaultPermissions = (userPermissions: Record<string, boolean> = {}) =>
'devices.manage',
'protocols.view',
'protocols.manage',
'configs.view',
'configs.manage'
'ranconfigurations.view',
'ranconfigurations.manage',
'imsconfigurations.view',
'imsconfigurations.manage',
'corenetworkconfigs.view',
'corenetworkconfigs.manage',
'networkstackconfigs.view',
'networkstackconfigs.manage',
])
];

251
src/X1.WebUI/src/pages/configs/ConfigsTable.tsx

@ -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>
);
}

217
src/X1.WebUI/src/pages/configs/ConfigsView.tsx

@ -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>
);
}

90
src/X1.WebUI/src/pages/core-network-configs/CoreNetworkConfigForm.tsx

@ -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>
);
}

173
src/X1.WebUI/src/pages/core-network-configs/CoreNetworkConfigsTable.tsx

@ -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>
);
}

356
src/X1.WebUI/src/pages/core-network-configs/CoreNetworkConfigsView.tsx

@ -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>
);
}

90
src/X1.WebUI/src/pages/ims-configurations/IMSConfigurationForm.tsx

@ -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>
);
}

173
src/X1.WebUI/src/pages/ims-configurations/IMSConfigurationsTable.tsx

@ -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>
);
}

356
src/X1.WebUI/src/pages/ims-configurations/IMSConfigurationsView.tsx

@ -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>
);
}

90
src/X1.WebUI/src/pages/network-stack-configs/NetworkStackConfigForm.tsx

@ -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>
);
}

165
src/X1.WebUI/src/pages/network-stack-configs/NetworkStackConfigsTable.tsx

@ -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>
);
}

356
src/X1.WebUI/src/pages/network-stack-configs/NetworkStackConfigsView.tsx

@ -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>
);
}

90
src/X1.WebUI/src/pages/ran-configurations/RANConfigurationForm.tsx

@ -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>
);
}

173
src/X1.WebUI/src/pages/ran-configurations/RANConfigurationsTable.tsx

@ -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>
);
}

356
src/X1.WebUI/src/pages/ran-configurations/RANConfigurationsView.tsx

@ -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>
);
}

43
src/X1.WebUI/src/routes/AppRouter.tsx

@ -33,8 +33,15 @@ const UEAnalysisView = lazy(() => import('@/pages/analysis/UEAnalysisView'));
const DevicesView = lazy(() => import('@/pages/instruments/DevicesView'));
// 协议管理页面
const ProtocolsView = lazy(() => import('@/pages/protocols/ProtocolsView'));
// 配置管理页面
const ConfigsView = lazy(() => import('@/pages/configs/ConfigsView'));
// RAN配置管理页面
const RANConfigurationsView = lazy(() => import('@/pages/ran-configurations/RANConfigurationsView'));
// IMS配置管理页面
const IMSConfigurationsView = lazy(() => import('@/pages/ims-configurations/IMSConfigurationsView'));
// 核心网络配置管理页面
const CoreNetworkConfigsView = lazy(() => import('@/pages/core-network-configs/CoreNetworkConfigsView'));
// 网络栈配置管理页面
const NetworkStackConfigsView = lazy(() => import('@/pages/network-stack-configs/NetworkStackConfigsView'));
// 加载中的占位组件
const LoadingFallback = () => (
@ -201,10 +208,36 @@ export function AppRouter() {
</AnimatedContainer>
</ProtectedRoute>
} />
<Route path="configs" element={
<ProtectedRoute requiredPermission="configs.view">
</Route>
{/* 网络栈配置管理路由 */}
<Route path="network-stack-configs">
<Route index element={<Navigate to="ran-configurations" replace />} />
<Route path="ran-configurations" element={
<ProtectedRoute requiredPermission="ranconfigurations.view">
<AnimatedContainer>
<RANConfigurationsView />
</AnimatedContainer>
</ProtectedRoute>
} />
<Route path="ims-configurations" element={
<ProtectedRoute requiredPermission="imsconfigurations.view">
<AnimatedContainer>
<IMSConfigurationsView />
</AnimatedContainer>
</ProtectedRoute>
} />
<Route path="core-network-configs" element={
<ProtectedRoute requiredPermission="corenetworkconfigs.view">
<AnimatedContainer>
<CoreNetworkConfigsView />
</AnimatedContainer>
</ProtectedRoute>
} />
<Route path="network-stack-configs" element={
<ProtectedRoute requiredPermission="networkstackconfigs.view">
<AnimatedContainer>
<ConfigsView />
<NetworkStackConfigsView />
</AnimatedContainer>
</ProtectedRoute>
} />

120
src/X1.WebUI/src/services/configService.ts

@ -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();

111
src/X1.WebUI/src/services/coreNetworkConfigService.ts

@ -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();

111
src/X1.WebUI/src/services/imsConfigurationService.ts

@ -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();

1
src/X1.WebUI/src/services/instrumentService.ts

@ -227,7 +227,6 @@ export const deviceService = new DeviceService();
// 重新导出其他服务,保持向后兼容
export * from './protocolService';
export * from './configService';
// 为了向后兼容,保留原有的 instrumentService 导出
export { deviceService as instrumentService };

112
src/X1.WebUI/src/services/networkStackConfigService.ts

@ -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();

109
src/X1.WebUI/src/services/ranConfigurationService.ts

@ -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();

112
src/X1.WebUI/src/services/stackCoreIMSBindingService.ts

@ -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();

746
src/modify.md

@ -1,5 +1,354 @@
# 修改记录
## 2024-12-19 网络栈配置管理页面实现
### 新增文件
- `X1.WebUI/src/pages/network-stack-configs/NetworkStackConfigsView.tsx` - 网络栈配置页面视图组件
- `X1.WebUI/src/pages/network-stack-configs/NetworkStackConfigsTable.tsx` - 网络栈配置表格组件
- `X1.WebUI/src/pages/network-stack-configs/NetworkStackConfigForm.tsx` - 网络栈配置表单组件
### 修改文件
- `X1.WebUI/src/routes/AppRouter.tsx` - 添加网络栈配置页面路由,将网络栈配置相关页面从仪表管理中分离
- `X1.WebUI/src/constants/menuConfig.ts` - 添加网络栈配置权限和菜单项,将网络栈配置作为独立菜单项
- `X1.WebUI/src/contexts/AuthContext.tsx` - 在 getDefaultPermissions 函数中添加网络栈配置相关权限
### 功能特性
#### 1. 页面组件结构
- **NetworkStackConfigsView**: 主页面视图,包含搜索、表格、分页等功能
- **NetworkStackConfigsTable**: 数据表格组件,支持列显示控制、密度调整
- **NetworkStackConfigForm**: 创建/编辑表单组件,支持栈ID、RAN ID等字段编辑
#### 2. 搜索功能
- 支持按栈ID和描述搜索
- 支持按激活/非激活状态过滤
- 支持分页大小调整
- 提供展开/收起高级搜索选项
#### 3. 表格功能
- 支持列显示控制(栈ID、RAN ID、描述、状态、创建时间、操作)
- 支持表格密度调整(宽松、默认、紧凑)
- 栈ID使用等宽字体显示,便于识别
- 状态徽章显示(激活/非激活)
- 日期格式化显示
#### 4. 表单功能
- 栈ID输入(必填,编辑时不允许修改)
- RAN ID输入(可选)
- 描述信息输入(可选)
- 激活/非激活状态切换
- 表单验证和提交状态管理
#### 5. 权限控制
- 添加 `networkstackconfigs.view``networkstackconfigs.manage` 权限
- 路由级别的权限保护
- 菜单项权限控制
#### 6. 用户体验
- 加载状态显示
- 操作成功/失败提示
- 删除确认对话框
- 防重复提交保护
- 响应式设计
- 编辑时栈ID字段禁用,防止误操作
### 技术实现
#### 1. 组件设计
- 参考协议管理页面的设计模式
- 使用TypeScript确保类型安全
- 采用React Hooks管理状态
- 使用shadcn/ui组件库
#### 2. 数据管理
- 使用网络栈配置服务进行API调用
- 支持分页、搜索、过滤功能
- 完整的CRUD操作支持
#### 3. 路由配置
- 懒加载组件提高性能
- 权限保护路由
- 动画容器包装
#### 4. 样式设计
- 统一的UI风格
- 响应式布局
- 美观的表格和表单设计
- 栈ID使用等宽字体,提高可读性
### 使用方式
1. 通过菜单 "网络栈配置" 访问页面
2. 支持创建、编辑、删除网络栈配置
3. 支持按栈ID、描述搜索配置
4. 支持按激活状态过滤配置
5. 支持调整表格显示密度和列显示
6. 编辑时栈ID字段自动禁用,防止误操作
### 路由结构调整
- 将网络栈配置相关页面从 `/dashboard/instruments` 下分离出来
- 创建独立的 `/dashboard/network-stack-configs` 路由组
- 包含以下子路由:
- `/dashboard/network-stack-configs/ran-configurations` - RAN配置管理
- `/dashboard/network-stack-configs/ims-configurations` - IMS配置管理
- `/dashboard/network-stack-configs/core-network-configs` - 核心网络配置管理
- `/dashboard/network-stack-configs/network-stack-configs` - 网络栈配置管理
### 菜单结构调整
- 将网络栈配置从仪表管理菜单中分离出来
- 创建独立的"网络栈配置"顶级菜单项
- 包含四个子菜单项:RAN配置、IMS配置、核心网络配置、网络栈配置
- 使用 `ranconfigurations.view` 权限作为主菜单权限
### 权限配置
- 在 `AuthContext.tsx``getDefaultPermissions` 函数中添加了网络栈配置相关权限:
- `ranconfigurations.view``ranconfigurations.manage`
- `imsconfigurations.view``imsconfigurations.manage`
- `corenetworkconfigs.view``corenetworkconfigs.manage`
- `networkstackconfigs.view``networkstackconfigs.manage`
- 确保用户登录后能够正确获取网络栈配置相关权限
## 2024-12-19 核心网络配置管理页面实现
### 新增文件
- `X1.WebUI/src/pages/core-network-configs/CoreNetworkConfigsView.tsx` - 核心网络配置页面视图组件
- `X1.WebUI/src/pages/core-network-configs/CoreNetworkConfigsTable.tsx` - 核心网络配置表格组件
- `X1.WebUI/src/pages/core-network-configs/CoreNetworkConfigForm.tsx` - 核心网络配置表单组件
### 修改文件
- `X1.WebUI/src/routes/AppRouter.tsx` - 添加核心网络配置页面路由
- `X1.WebUI/src/constants/menuConfig.ts` - 添加核心网络配置权限和菜单项
### 功能特性
#### 1. 页面组件结构
- **CoreNetworkConfigsView**: 主页面视图,包含搜索、表格、分页等功能
- **CoreNetworkConfigsTable**: 数据表格组件,支持列显示控制、密度调整
- **CoreNetworkConfigForm**: 创建/编辑表单组件,支持JSON配置内容编辑
#### 2. 搜索功能
- 支持按配置名称和描述搜索
- 支持按启用/禁用状态过滤
- 支持分页大小调整
- 提供展开/收起高级搜索选项
#### 3. 表格功能
- 支持列显示控制(配置名称、描述、状态、创建时间、操作)
- 支持表格密度调整(宽松、默认、紧凑)
- 配置内容预览(截断显示,鼠标悬停显示完整内容)
- 状态徽章显示(启用/禁用)
- 日期格式化显示
#### 4. 表单功能
- 配置名称输入(必填)
- JSON格式配置内容编辑(必填,支持多行编辑)
- 描述信息输入(可选)
- 启用/禁用状态切换
- 表单验证和提交状态管理
#### 5. 权限控制
- 添加 `corenetworkconfigs.view``corenetworkconfigs.manage` 权限
- 路由级别的权限保护
- 菜单项权限控制
#### 6. 用户体验
- 加载状态显示
- 操作成功/失败提示
- 删除确认对话框
- 防重复提交保护
- 响应式设计
### 技术实现
#### 1. 组件设计
- 参考协议管理页面的设计模式
- 使用TypeScript确保类型安全
- 采用React Hooks管理状态
- 使用shadcn/ui组件库
#### 2. 数据管理
- 使用核心网络配置服务进行API调用
- 支持分页、搜索、过滤功能
- 完整的CRUD操作支持
#### 3. 路由配置
- 懒加载组件提高性能
- 权限保护路由
- 动画容器包装
#### 4. 样式设计
- 统一的UI风格
- 响应式布局
- 美观的表格和表单设计
### 使用方式
1. 通过菜单 "仪表管理" -> "核心网络配置" 访问页面
2. 支持创建、编辑、删除核心网络配置
3. 支持按名称、描述搜索配置
4. 支持按启用状态过滤配置
5. 支持调整表格显示密度和列显示
## 2024-12-19 IMS配置管理页面实现
### 新增文件
- `X1.WebUI/src/pages/ims-configurations/IMSConfigurationsView.tsx` - IMS配置页面视图组件
- `X1.WebUI/src/pages/ims-configurations/IMSConfigurationsTable.tsx` - IMS配置表格组件
- `X1.WebUI/src/pages/ims-configurations/IMSConfigurationForm.tsx` - IMS配置表单组件
### 修改文件
- `X1.WebUI/src/routes/AppRouter.tsx` - 添加IMS配置页面路由
- `X1.WebUI/src/constants/menuConfig.ts` - 添加IMS配置权限和菜单项
### 功能特性
#### 1. 页面组件结构
- **IMSConfigurationsView**: 主页面视图,包含搜索、表格、分页等功能
- **IMSConfigurationsTable**: 数据表格组件,支持列显示控制、密度调整
- **IMSConfigurationForm**: 创建/编辑表单组件,支持JSON配置内容编辑
#### 2. 搜索功能
- 支持按配置名称和描述搜索
- 支持按启用/禁用状态过滤
- 支持分页大小调整
- 提供展开/收起高级搜索选项
#### 3. 表格功能
- 支持列显示控制(配置名称、描述、状态、创建时间、操作)
- 支持表格密度调整(宽松、默认、紧凑)
- 配置内容预览(截断显示,鼠标悬停显示完整内容)
- 状态徽章显示(启用/禁用)
- 日期格式化显示
#### 4. 表单功能
- 配置名称输入(必填)
- JSON格式配置内容编辑(必填,支持多行编辑)
- 描述信息输入(可选)
- 启用/禁用状态切换
- 表单验证和提交状态管理
#### 5. 权限控制
- 添加 `imsconfigurations.view``imsconfigurations.manage` 权限
- 路由级别的权限保护
- 菜单项权限控制
#### 6. 用户体验
- 加载状态显示
- 操作成功/失败提示
- 删除确认对话框
- 防重复提交保护
- 响应式设计
### 技术实现
#### 1. 组件设计
- 参考协议管理页面的设计模式
- 使用TypeScript确保类型安全
- 采用React Hooks管理状态
- 使用shadcn/ui组件库
#### 2. 数据管理
- 使用IMS配置服务进行API调用
- 支持分页、搜索、过滤功能
- 完整的CRUD操作支持
#### 3. 路由配置
- 懒加载组件提高性能
- 权限保护路由
- 动画容器包装
#### 4. 样式设计
- 统一的UI风格
- 响应式布局
- 美观的表格和表单设计
### 使用方式
1. 通过菜单 "仪表管理" -> "IMS配置" 访问页面
2. 支持创建、编辑、删除IMS配置
3. 支持按名称、描述搜索配置
4. 支持按启用状态过滤配置
5. 支持调整表格显示密度和列显示
## 2024-12-19 RAN配置管理页面实现
### 新增文件
- `X1.WebUI/src/pages/ran-configurations/RANConfigurationsView.tsx` - RAN配置页面视图组件
- `X1.WebUI/src/pages/ran-configurations/RANConfigurationsTable.tsx` - RAN配置表格组件
- `X1.WebUI/src/pages/ran-configurations/RANConfigurationForm.tsx` - RAN配置表单组件
### 修改文件
- `X1.WebUI/src/routes/AppRouter.tsx` - 添加RAN配置页面路由
- `X1.WebUI/src/constants/menuConfig.ts` - 添加RAN配置权限和菜单项
### 功能特性
#### 1. 页面组件结构
- **RANConfigurationsView**: 主页面视图,包含搜索、表格、分页等功能
- **RANConfigurationsTable**: 数据表格组件,支持列显示控制、密度调整
- **RANConfigurationForm**: 创建/编辑表单组件,支持JSON配置内容编辑
#### 2. 搜索功能
- 支持按配置名称和描述搜索
- 支持按启用/禁用状态过滤
- 支持分页大小调整
- 提供展开/收起高级搜索选项
#### 3. 表格功能
- 支持列显示控制(配置名称、描述、状态、创建时间、操作)
- 支持表格密度调整(宽松、默认、紧凑)
- 配置内容预览(截断显示,鼠标悬停显示完整内容)
- 状态徽章显示(启用/禁用)
- 日期格式化显示
#### 4. 表单功能
- 配置名称输入(必填)
- JSON格式配置内容编辑(必填,支持多行编辑)
- 描述信息输入(可选)
- 启用/禁用状态切换
- 表单验证和提交状态管理
#### 5. 权限控制
- 添加 `ranconfigurations.view``ranconfigurations.manage` 权限
- 路由级别的权限保护
- 菜单项权限控制
#### 6. 用户体验
- 加载状态显示
- 操作成功/失败提示
- 删除确认对话框
- 防重复提交保护
- 响应式设计
### 技术实现
#### 1. 组件设计
- 参考协议管理页面的设计模式
- 使用TypeScript确保类型安全
- 采用React Hooks管理状态
- 使用shadcn/ui组件库
#### 2. 数据管理
- 使用RAN配置服务进行API调用
- 支持分页、搜索、过滤功能
- 完整的CRUD操作支持
#### 3. 路由配置
- 懒加载组件提高性能
- 权限保护路由
- 动画容器包装
#### 4. 样式设计
- 统一的UI风格
- 响应式布局
- 美观的表格和表单设计
### 使用方式
1. 通过菜单 "仪表管理" -> "RAN配置" 访问页面
2. 支持创建、编辑、删除RAN配置
3. 支持按名称、描述搜索配置
4. 支持按启用状态过滤配置
5. 支持调整表格显示密度和列显示
## 2024-12-19 ProtocolMessageHandler 优化
## 2024-12-19 ProtocolMessageHandler 优化
### 修改文件
@ -687,3 +1036,400 @@ if (message.Data.Length > actualBufferSize)
- **处理层负责必要验证**:只检查缓冲区容量
- **避免重复验证**:提高性能和代码清晰度
- **保持数据完整性**:确保系统稳定性和一致性
## 2024-12-19 移除配置管理相关功能
### 修改概述
根据用户要求,移除了与配置管理相关的所有功能,包括服务、页面组件和路由配置。
### 删除的文件
- `X1.WebUI/src/services/configService.ts` - 配置管理服务
- `X1.WebUI/src/pages/configs/ConfigsView.tsx` - 配置管理页面组件
- `X1.WebUI/src/pages/configs/ConfigsTable.tsx` - 配置表格组件
### 修改的文件
#### 1. `X1.WebUI/src/routes/AppRouter.tsx`
- 移除了 `ConfigsView` 的 lazy 导入
- 删除了 `/dashboard/instruments/configs` 路由配置
#### 2. `X1.WebUI/src/services/instrumentService.ts`
- 移除了对 `configService` 的导出语句
- 保持其他服务的导出不变
#### 3. `X1.WebUI/src/constants/api.ts`
- 移除了 `CONFIGS: '/instruments/configs'` API 路径配置
#### 4. `X1.WebUI/src/constants/menuConfig.ts`
- 移除了 `configs.view``configs.manage` 权限类型定义
- 删除了菜单配置中的"配置列表"菜单项
#### 5. `X1.WebUI/src/contexts/AuthContext.tsx`
- 移除了默认权限中的 `configs.view``configs.manage` 权限
### 影响范围
- **功能影响**:用户无法再访问配置管理功能
- **路由影响**:`/dashboard/instruments/configs` 路径不再可用
- **权限影响**:配置相关的权限检查被移除
- **菜单影响**:仪表管理菜单中不再显示配置列表选项
### 技术细节
- 所有相关的 TypeScript 类型定义已被移除
- 路由配置已更新,移除了配置相关的路由
- 权限系统已更新,移除了配置相关的权限检查
- 菜单系统已更新,移除了配置相关的菜单项
### 注意事项
- 如果后端 API 仍然存在配置相关的端点,前端将无法访问
- 建议同时清理后端相关的配置管理功能
- 确保没有其他地方引用已删除的组件或服务
## 2024-12-19 创建 RAN 配置前端服务
### 修改概述
参考 `protocolService` 的实现模式,为 `RANConfigurationController` 创建对应的前端服务。
### 新增文件
- `X1.WebUI/src/services/ranConfigurationService.ts` - RAN 配置管理服务
### 修改的文件
#### 1. `X1.WebUI/src/constants/api.ts`
- 添加了 `RAN_CONFIGURATIONS: '/ranconfigurations'` API 路径配置
#### 2. `X1.WebUI/src/services/ranConfigurationService.ts`(新增)
- 实现了完整的 RAN 配置管理服务
- 包含所有 CRUD 操作的接口定义和方法实现
- 参考 `protocolService` 的设计模式和代码结构
### 功能特性
#### 1. 接口定义
- `RANConfiguration` - RAN 配置实体接口
- `GetRANConfigurationsRequest` - 获取列表请求接口
- `GetRANConfigurationsResponse` - 获取列表响应接口
- `CreateRANConfigurationRequest` - 创建请求接口
- `CreateRANConfigurationResponse` - 创建响应接口
- `UpdateRANConfigurationRequest` - 更新请求接口
- `UpdateRANConfigurationResponse` - 更新响应接口
#### 2. 服务方法
- `getRANConfigurations()` - 获取 RAN 配置列表(支持分页和搜索)
- `getRANConfigurationById()` - 根据 ID 获取配置详情
- `createRANConfiguration()` - 创建新的 RAN 配置
- `updateRANConfiguration()` - 更新现有 RAN 配置
- `deleteRANConfiguration()` - 删除 RAN 配置
#### 3. 查询参数支持
- 分页参数:`pageNumber`、`pageSize`
- 搜索参数:`searchTerm`
- 过滤参数:`isDisabled`
### 技术实现
#### 1. 设计模式
- 采用与 `protocolService` 相同的设计模式
- 使用 TypeScript 接口定义数据结构
- 统一的错误处理和响应格式
#### 2. HTTP 客户端集成
- 使用 `httpClient` 进行 API 调用
- 支持 GET、POST、PUT、DELETE 方法
- 自动处理查询参数构建
#### 3. 类型安全
- 完整的 TypeScript 类型定义
- 与后端 API 响应格式完全匹配
- 提供良好的开发体验和代码提示
### 使用示例
```typescript
// 获取 RAN 配置列表
const result = await ranConfigurationService.getRANConfigurations({
pageNumber: 1,
pageSize: 10,
searchTerm: 'LTE'
});
// 创建新的 RAN 配置
const createResult = await ranConfigurationService.createRANConfiguration({
name: 'LTE Configuration',
configContent: JSON.stringify(configData),
description: 'LTE 网络配置'
});
```
### 影响范围
- **前端服务**:提供了完整的 RAN 配置管理前端服务
- **API 集成**:与后端 `RANConfigurationController` 完全对应
- **类型安全**:提供了完整的 TypeScript 类型定义
- **开发体验**:统一的服务接口,便于前端开发使用
## 2024-12-19 创建 IMS 配置前端服务
### 修改概述
参考 `protocolService``ranConfigurationService` 的实现模式,为 `IMSConfigurationController` 创建对应的前端服务。
### 新增文件
- `X1.WebUI/src/services/imsConfigurationService.ts` - IMS 配置管理服务
### 修改的文件
#### 1. `X1.WebUI/src/constants/api.ts`
- 添加了 `IMS_CONFIGURATIONS: '/imsconfigurations'` API 路径配置
#### 2. `X1.WebUI/src/services/imsConfigurationService.ts`(新增)
- 实现了完整的 IMS 配置管理服务
- 包含所有 CRUD 操作的接口定义和方法实现
- 参考 `protocolService``ranConfigurationService` 的设计模式和代码结构
### 功能特性
#### 1. 接口定义
- `IMSConfiguration` - IMS 配置实体接口
- `GetIMSConfigurationsRequest` - 获取列表请求接口
- `GetIMSConfigurationsResponse` - 获取列表响应接口
- `CreateIMSConfigurationRequest` - 创建请求接口
- `CreateIMSConfigurationResponse` - 创建响应接口
- `UpdateIMSConfigurationRequest` - 更新请求接口
- `UpdateIMSConfigurationResponse` - 更新响应接口
#### 2. 服务方法
- `getIMSConfigurations()` - 获取 IMS 配置列表(支持分页和搜索)
- `getIMSConfigurationById()` - 根据 ID 获取配置详情
- `createIMSConfiguration()` - 创建新的 IMS 配置
- `updateIMSConfiguration()` - 更新现有 IMS 配置
- `deleteIMSConfiguration()` - 删除 IMS 配置
#### 3. 查询参数支持
- 分页参数:`pageNumber`、`pageSize`
- 搜索参数:`searchTerm`
- 过滤参数:`isDisabled`
### 技术实现
#### 1. 设计模式
- 采用与 `protocolService``ranConfigurationService` 相同的设计模式
- 使用 TypeScript 接口定义数据结构
- 统一的错误处理和响应格式
#### 2. HTTP 客户端集成
- 使用 `httpClient` 进行 API 调用
- 支持 GET、POST、PUT、DELETE 方法
- 自动处理查询参数构建
#### 3. 类型安全
- 完整的 TypeScript 类型定义
- 与后端 API 响应格式完全匹配
- 提供良好的开发体验和代码提示
### 使用示例
```typescript
// 获取 IMS 配置列表
const result = await imsConfigurationService.getIMSConfigurations({
pageNumber: 1,
pageSize: 10,
searchTerm: 'VoIP'
});
// 创建新的 IMS 配置
const createResult = await imsConfigurationService.createIMSConfiguration({
name: 'VoIP Configuration',
configContent: JSON.stringify(configData),
description: 'VoIP 服务配置'
});
```
### 影响范围
- **前端服务**:提供了完整的 IMS 配置管理前端服务
- **API 集成**:与后端 `IMSConfigurationController` 完全对应
- **类型安全**:提供了完整的 TypeScript 类型定义
- **开发体验**:统一的服务接口,便于前端开发使用
## 2024-12-19 创建核心网络配置前端服务
### 修改概述
参考其他配置服务的实现模式,为 `CoreNetworkConfigsController` 创建对应的前端服务。
### 新增文件
- `X1.WebUI/src/services/coreNetworkConfigService.ts` - 核心网络配置管理服务
### 修改的文件
#### 1. `X1.WebUI/src/constants/api.ts`
- 添加了 `CORE_NETWORK_CONFIGS: '/corenetworkconfigs'` API 路径配置
#### 2. `X1.WebUI/src/services/coreNetworkConfigService.ts`(新增)
- 实现了完整的核心网络配置管理服务
- 包含所有 CRUD 操作的接口定义和方法实现
- 参考其他配置服务的设计模式和代码结构
### 功能特性
#### 1. 接口定义
- `CoreNetworkConfig` - 核心网络配置实体接口
- `GetCoreNetworkConfigsRequest` - 获取列表请求接口
- `GetCoreNetworkConfigsResponse` - 获取列表响应接口
- `CreateCoreNetworkConfigRequest` - 创建请求接口
- `CreateCoreNetworkConfigResponse` - 创建响应接口
- `UpdateCoreNetworkConfigRequest` - 更新请求接口
- `UpdateCoreNetworkConfigResponse` - 更新响应接口
#### 2. 服务方法
- `getCoreNetworkConfigs()` - 获取核心网络配置列表(支持分页和搜索)
- `getCoreNetworkConfigById()` - 根据 ID 获取配置详情
- `createCoreNetworkConfig()` - 创建新的核心网络配置
- `updateCoreNetworkConfig()` - 更新现有核心网络配置
- `deleteCoreNetworkConfig()` - 删除核心网络配置
#### 3. 查询参数支持
- 分页参数:`pageNumber`、`pageSize`
- 搜索参数:`searchTerm`
- 过滤参数:`isDisabled`
### 技术实现
#### 1. 设计模式
- 采用与其他配置服务相同的设计模式
- 使用 TypeScript 接口定义数据结构
- 统一的错误处理和响应格式
#### 2. HTTP 客户端集成
- 使用 `httpClient` 进行 API 调用
- 支持 GET、POST、PUT、DELETE 方法
- 自动处理查询参数构建
#### 3. 类型安全
- 完整的 TypeScript 类型定义
- 与后端 API 响应格式完全匹配
- 提供良好的开发体验和代码提示
### 使用示例
```typescript
// 获取核心网络配置列表
const result = await coreNetworkConfigService.getCoreNetworkConfigs({
pageNumber: 1,
pageSize: 10,
searchTerm: 'EPC'
});
// 创建新的核心网络配置
const createResult = await coreNetworkConfigService.createCoreNetworkConfig({
name: 'EPC Configuration',
configContent: JSON.stringify(configData),
description: '演进分组核心网配置'
});
```
### 影响范围
- **前端服务**:提供了完整的核心网络配置管理前端服务
- **API 集成**:与后端 `CoreNetworkConfigsController` 完全对应
- **类型安全**:提供了完整的 TypeScript 类型定义
- **开发体验**:统一的服务接口,便于前端开发使用
## 2024-12-19 创建栈核心网IMS绑定前端服务
### 修改概述
参考其他配置服务的实现模式,为 `StackCoreIMSBindingsController` 创建对应的前端服务。
### 新增文件
- `X1.WebUI/src/services/stackCoreIMSBindingService.ts` - 栈核心网IMS绑定管理服务
### 修改的文件
#### 1. `X1.WebUI/src/constants/api.ts`
- 添加了 `STACK_CORE_IMS_BINDINGS: '/stackcoreimsbindings'` API 路径配置
#### 2. `X1.WebUI/src/services/stackCoreIMSBindingService.ts`(新增)
- 实现了完整的栈核心网IMS绑定管理服务
- 包含所有 CRUD 操作的接口定义和方法实现
- 参考其他配置服务的设计模式和代码结构
### 功能特性
#### 1. 接口定义
- `StackCoreIMSBinding` - 栈核心网IMS绑定实体接口
- `GetStackCoreIMSBindingsRequest` - 获取列表请求接口
- `GetStackCoreIMSBindingsResponse` - 获取列表响应接口
- `CreateStackCoreIMSBindingRequest` - 创建请求接口
- `CreateStackCoreIMSBindingResponse` - 创建响应接口
- `UpdateStackCoreIMSBindingRequest` - 更新请求接口
- `UpdateStackCoreIMSBindingResponse` - 更新响应接口
#### 2. 服务方法
- `getStackCoreIMSBindings()` - 获取列表(支持分页、按栈ID/核心网ID/IMS ID过滤)
- `getStackCoreIMSBindingById()` - 获取详情
- `createStackCoreIMSBinding()` - 创建绑定关系
- `updateStackCoreIMSBinding()` - 更新绑定关系
- `deleteStackCoreIMSBinding()` - 删除绑定关系
#### 3. 特殊功能
- **复合键支持**:支持栈ID和索引的复合唯一键
- **多维度过滤**:支持按栈ID、核心网配置ID、IMS配置ID进行过滤
- **关系管理**:管理网络栈与核心网/IMS配置之间的绑定关系
### 技术特点
- **类型安全**:完整的 TypeScript 类型定义
- **错误处理**:统一的错误处理机制
- **查询参数**:支持灵活的查询参数组合
- **RESTful API**:遵循 RESTful API 设计规范
### 影响范围
- **前端服务**:提供了完整的栈核心网IMS绑定管理前端服务
- **API 集成**:与后端 `StackCoreIMSBindingsController` 完全对应
- **类型安全**:提供了完整的 TypeScript 类型定义
- **开发体验**:统一的服务接口,便于前端开发使用
## 2024-12-19 创建网络栈配置前端服务
### 修改概述
参考其他配置服务的实现模式,为 `NetworkStackConfigsController` 创建对应的前端服务。
### 新增文件
- `X1.WebUI/src/services/networkStackConfigService.ts` - 网络栈配置管理服务
### 修改的文件
#### 1. `X1.WebUI/src/constants/api.ts`
- 添加了 `NETWORK_STACK_CONFIGS: '/networkstackconfigs'` API 路径配置
#### 2. `X1.WebUI/src/services/networkStackConfigService.ts`(新增)
- 实现了完整的网络栈配置管理服务
- 包含所有 CRUD 操作的接口定义和方法实现
- 参考其他配置服务的设计模式和代码结构
### 功能特性
#### 1. 接口定义
- `NetworkStackConfig` - 网络栈配置实体接口
- `GetNetworkStackConfigsRequest` - 获取列表请求接口
- `GetNetworkStackConfigsResponse` - 获取列表响应接口
- `CreateNetworkStackConfigRequest` - 创建请求接口
- `CreateNetworkStackConfigResponse` - 创建响应接口
- `UpdateNetworkStackConfigRequest` - 更新请求接口
- `UpdateNetworkStackConfigResponse` - 更新响应接口
#### 2. 服务方法
- `getNetworkStackConfigs()` - 获取列表(支持分页、搜索、按激活状态和RAN ID过滤)
- `getNetworkStackConfigById()` - 获取详情
- `createNetworkStackConfig()` - 创建配置
- `updateNetworkStackConfig()` - 更新配置
- `deleteNetworkStackConfig()` - 删除配置
#### 3. 特殊功能
- **栈ID唯一性**:栈ID作为唯一标识符
- **RAN关联**:支持与RAN配置的关联关系
- **激活状态管理**:支持配置的激活/停用状态管理
- **多维度过滤**:支持按激活状态、RAN ID、搜索关键词进行过滤
### 技术特点
- **类型安全**:完整的 TypeScript 类型定义
- **错误处理**:统一的错误处理机制
- **查询参数**:支持灵活的查询参数组合
- **RESTful API**:遵循 RESTful API 设计规范
### 影响范围
- **前端服务**:提供了完整的网络栈配置管理前端服务
- **API 集成**:与后端 `NetworkStackConfigsController` 完全对应
- **类型安全**:提供了完整的 TypeScript 类型定义
- **开发体验**:统一的服务接口,便于前端开发使用
Loading…
Cancel
Save