Browse Source

修复 instrumentService.ts 中的 protocolVersion 字段问题 - 移除前端接口中不存在的 protocolVersion 和 protocolVersionId 字段 - 修复 DeviceForm、DevicesTable、DevicesView 组件 - 更新设备管理功能,使其与后端API完全匹配 - 简化用户界面,移除协议版本选择功能

feature/x1-web-request
hyh 6 days ago
parent
commit
05ce544631
  1. 2
      src/X1.Application/Features/Auth/Commands/BaseLoginCommandHandler.cs
  2. 582
      src/X1.Infrastructure/Migrations/20250705165102_InitialCreate.Designer.cs
  3. 582
      src/X1.Infrastructure/Migrations/20250705173130_InitProtocolVersionAndDevice.Designer.cs
  4. 183
      src/X1.Infrastructure/Migrations/20250705173130_InitProtocolVersionAndDevice.cs
  5. 64
      src/X1.Infrastructure/Migrations/20250705174217_UpdateProtocolVersionAndCellularDevice.cs
  6. 384
      src/X1.Infrastructure/Migrations/20250728081332_InitialCreate.Designer.cs
  7. 266
      src/X1.Infrastructure/Migrations/20250728081332_InitialCreate.cs
  8. 380
      src/X1.Infrastructure/Migrations/AppDbContextModelSnapshot.cs
  9. 7781
      src/X1.WebAPI/logs/app-20250728.log
  10. 374
      src/X1.WebAPI/logs/error-20250728.log
  11. 209
      src/X1.WebUI/src/components/ui/ConfigContentEditor.tsx
  12. 2
      src/X1.WebUI/src/components/ui/dialog.tsx
  13. 78
      src/X1.WebUI/src/components/ui/drawer.tsx
  14. 4
      src/X1.WebUI/src/constants/menuConfig.ts
  15. 165
      src/X1.WebUI/src/pages/core-network-configs/CoreNetworkConfigDrawer.tsx
  16. 90
      src/X1.WebUI/src/pages/core-network-configs/CoreNetworkConfigForm.tsx
  17. 80
      src/X1.WebUI/src/pages/core-network-configs/CoreNetworkConfigsTable.tsx
  18. 99
      src/X1.WebUI/src/pages/core-network-configs/CoreNetworkConfigsView.tsx
  19. 165
      src/X1.WebUI/src/pages/ims-configurations/IMSConfigurationDrawer.tsx
  20. 90
      src/X1.WebUI/src/pages/ims-configurations/IMSConfigurationForm.tsx
  21. 80
      src/X1.WebUI/src/pages/ims-configurations/IMSConfigurationsTable.tsx
  22. 103
      src/X1.WebUI/src/pages/ims-configurations/IMSConfigurationsView.tsx
  23. 74
      src/X1.WebUI/src/pages/instruments/DeviceForm.tsx
  24. 59
      src/X1.WebUI/src/pages/instruments/DevicesTable.tsx
  25. 4
      src/X1.WebUI/src/pages/instruments/DevicesView.tsx
  26. 170
      src/X1.WebUI/src/pages/ran-configurations/RANConfigurationDrawer.tsx
  27. 90
      src/X1.WebUI/src/pages/ran-configurations/RANConfigurationForm.tsx
  28. 80
      src/X1.WebUI/src/pages/ran-configurations/RANConfigurationsTable.tsx
  29. 134
      src/X1.WebUI/src/pages/ran-configurations/RANConfigurationsView.tsx
  30. 8
      src/X1.WebUI/src/services/imsConfigurationService.ts
  31. 5
      src/X1.WebUI/src/services/instrumentService.ts
  32. 8
      src/X1.WebUI/src/services/ranConfigurationService.ts
  33. 581
      src/modify.md

2
src/X1.Application/Features/Auth/Commands/BaseLoginCommandHandler.cs

@ -130,7 +130,7 @@ public abstract class BaseLoginCommandHandler<TCommand, TResponse> : IRequestHan
loginLog.UpdateFailureReason("用户不存在");
await _loginLogRepository.RecordLoginAsync(loginLog, cancellationToken);
_logger.LogWarning("用户 {UserIdentifier} 不存在", userIdentifier);
return OperationResult<TResponse>.CreateFailure("账号或密码错误");
return OperationResult<TResponse>.CreateFailure("用户不存在");
}
// 检查用户是否已删除

582
src/X1.Infrastructure/Migrations/20250705165102_InitialCreate.Designer.cs

@ -1,582 +0,0 @@
// <auto-generated />
using System;
using CellularManagement.Infrastructure.Context;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace X1.Infrastructure.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20250705165102_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("CellularManagement.Domain.Entities.AppRole", b =>
{
b.Property<string>("Id")
.HasColumnType("text")
.HasComment("角色ID,主键");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text")
.HasComment("并发控制戳");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasComment("角色描述");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("角色名称");
b.Property<string>("NormalizedName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("标准化角色名称(大写)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("更新时间");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique()
.HasDatabaseName("IX_Roles_Name");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("Roles", null, t =>
{
t.HasComment("角色表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.AppUser", b =>
{
b.Property<string>("Id")
.HasColumnType("text")
.HasComment("用户ID,主键");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer")
.HasComment("登录失败次数");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text")
.HasComment("并发控制戳");
b.Property<DateTime>("CreatedTime")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("电子邮箱");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean")
.HasComment("邮箱是否已验证");
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true)
.HasComment("用户状态(true: 启用, false: 禁用)");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false)
.HasComment("是否已删除");
b.Property<DateTime?>("LastLoginTime")
.HasColumnType("timestamp with time zone")
.HasComment("最后登录时间");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean")
.HasComment("是否启用账户锁定");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone")
.HasComment("账户锁定结束时间");
b.Property<DateTime?>("ModifiedTime")
.HasColumnType("timestamp with time zone")
.HasComment("修改时间");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("标准化电子邮箱(大写)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("标准化账号(大写)");
b.Property<string>("PasswordHash")
.HasColumnType("text")
.HasComment("密码哈希值");
b.Property<string>("PhoneNumber")
.IsRequired()
.HasColumnType("text")
.HasComment("电话号码");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean")
.HasComment("电话号码是否已验证");
b.Property<string>("RealName")
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("用户名");
b.Property<string>("SecurityStamp")
.HasColumnType("text")
.HasComment("安全戳,用于并发控制");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean")
.HasComment("是否启用双因素认证");
b.Property<string>("UserName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("账号");
b.HasKey("Id");
b.HasIndex("Email")
.IsUnique()
.HasDatabaseName("IX_Users_Email");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.HasIndex("PhoneNumber")
.IsUnique()
.HasDatabaseName("IX_Users_PhoneNumber");
b.HasIndex("UserName")
.IsUnique()
.HasDatabaseName("IX_Users_UserName");
b.ToTable("Users", null, t =>
{
t.HasComment("用户表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Device.CellularDevice", b =>
{
b.Property<string>("Id")
.HasColumnType("text")
.HasComment("设备ID");
b.Property<int>("AgentPort")
.HasColumnType("integer")
.HasComment("Agent端口");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间");
b.Property<string>("CreatedBy")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasComment("设备描述");
b.Property<string>("IpAddress")
.IsRequired()
.HasMaxLength(45)
.HasColumnType("character varying(45)");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<bool>("IsEnabled")
.HasColumnType("boolean")
.HasComment("是否启用");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasComment("设备名称");
b.Property<string>("ProtocolVersionId")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("协议版本ID");
b.Property<string>("SerialNumber")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("序列号");
b.Property<DateTime?>("UpdatedAt")
.IsRequired()
.HasColumnType("timestamp with time zone")
.HasComment("更新时间");
b.Property<string>("UpdatedBy")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("ProtocolVersionId")
.HasDatabaseName("IX_CellularDevices_ProtocolVersionId");
b.HasIndex("SerialNumber")
.IsUnique()
.HasDatabaseName("IX_CellularDevices_SerialNumber");
b.ToTable("CellularDevices", null, t =>
{
t.HasComment("蜂窝设备表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Device.ProtocolVersion", b =>
{
b.Property<string>("Id")
.HasColumnType("text")
.HasComment("版本ID");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间");
b.Property<string>("CreatedBy")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasComment("版本描述");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<bool>("IsEnabled")
.HasColumnType("boolean");
b.Property<bool>("IsForceUpdate")
.HasColumnType("boolean");
b.Property<string>("MinimumSupportedVersion")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("ProtocolType")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime?>("ReleaseDate")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("UpdatedAt")
.IsRequired()
.HasColumnType("timestamp with time zone")
.HasComment("更新时间");
b.Property<string>("UpdatedBy")
.HasColumnType("text");
b.Property<string>("Version")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("版本号");
b.HasKey("Id");
b.HasIndex("Version")
.IsUnique()
.HasDatabaseName("IX_ProtocolVersions_Version");
b.ToTable("ProtocolVersions", null, t =>
{
t.HasComment("协议版本表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Logging.LoginLog", b =>
{
b.Property<string>("Id")
.HasColumnType("text")
.HasComment("日志ID");
b.Property<string>("Browser")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasComment("浏览器信息");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("FailureReason")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasComment("失败原因");
b.Property<string>("IpAddress")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("登录IP");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<bool>("IsSuccess")
.HasColumnType("boolean")
.HasComment("登录状态(成功/失败)");
b.Property<string>("Location")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasComment("登录位置");
b.Property<string>("LoginSource")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime>("LoginTime")
.HasColumnType("timestamp with time zone")
.HasComment("登录时间");
b.Property<string>("LoginType")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("OperatingSystem")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasComment("操作系统信息");
b.Property<string>("SessionId")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("UserAgent")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasComment("设备信息");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(450)
.HasColumnType("character varying(450)")
.HasComment("用户ID");
b.HasKey("Id");
b.HasIndex("IpAddress")
.HasDatabaseName("IX_LoginLogs_IpAddress");
b.HasIndex("LoginTime")
.HasDatabaseName("IX_LoginLogs_LoginTime");
b.HasIndex("UserId")
.HasDatabaseName("IX_LoginLogs_UserId");
b.HasIndex("UserId", "LoginTime")
.HasDatabaseName("IX_LoginLogs_UserId_LoginTime");
b.ToTable("LoginLogs", null, t =>
{
t.HasComment("用户登录日志表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Permission", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.ToTable("Permissions", (string)null);
});
modelBuilder.Entity("CellularManagement.Domain.Entities.RolePermission", b =>
{
b.Property<string>("RoleId")
.HasColumnType("text");
b.Property<string>("PermissionId")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("RoleId", "PermissionId");
b.HasIndex("PermissionId");
b.ToTable("RolePermissions", (string)null);
});
modelBuilder.Entity("CellularManagement.Domain.Entities.UserRole", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("RoleId")
.HasColumnType("text");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("UserRoles", null, t =>
{
t.HasComment("用户角色关系表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Device.CellularDevice", b =>
{
b.HasOne("CellularManagement.Domain.Entities.Device.ProtocolVersion", "ProtocolVersion")
.WithMany()
.HasForeignKey("ProtocolVersionId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("ProtocolVersion");
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Logging.LoginLog", b =>
{
b.HasOne("CellularManagement.Domain.Entities.AppUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("CellularManagement.Domain.Entities.RolePermission", b =>
{
b.HasOne("CellularManagement.Domain.Entities.Permission", "Permission")
.WithMany("RolePermissions")
.HasForeignKey("PermissionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CellularManagement.Domain.Entities.AppRole", "Role")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Permission");
b.Navigation("Role");
});
modelBuilder.Entity("CellularManagement.Domain.Entities.UserRole", b =>
{
b.HasOne("CellularManagement.Domain.Entities.AppRole", "Role")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CellularManagement.Domain.Entities.AppUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Role");
b.Navigation("User");
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Permission", b =>
{
b.Navigation("RolePermissions");
});
#pragma warning restore 612, 618
}
}
}

582
src/X1.Infrastructure/Migrations/20250705173130_InitProtocolVersionAndDevice.Designer.cs

@ -1,582 +0,0 @@
// <auto-generated />
using System;
using CellularManagement.Infrastructure.Context;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace X1.Infrastructure.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20250705173130_InitProtocolVersionAndDevice")]
partial class InitProtocolVersionAndDevice
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("CellularManagement.Domain.Entities.AppRole", b =>
{
b.Property<string>("Id")
.HasColumnType("text")
.HasComment("角色ID,主键");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text")
.HasComment("并发控制戳");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasComment("角色描述");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("角色名称");
b.Property<string>("NormalizedName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("标准化角色名称(大写)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("更新时间");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique()
.HasDatabaseName("IX_Roles_Name");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("Roles", null, t =>
{
t.HasComment("角色表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.AppUser", b =>
{
b.Property<string>("Id")
.HasColumnType("text")
.HasComment("用户ID,主键");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer")
.HasComment("登录失败次数");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text")
.HasComment("并发控制戳");
b.Property<DateTime>("CreatedTime")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("电子邮箱");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean")
.HasComment("邮箱是否已验证");
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true)
.HasComment("用户状态(true: 启用, false: 禁用)");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false)
.HasComment("是否已删除");
b.Property<DateTime?>("LastLoginTime")
.HasColumnType("timestamp with time zone")
.HasComment("最后登录时间");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean")
.HasComment("是否启用账户锁定");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone")
.HasComment("账户锁定结束时间");
b.Property<DateTime?>("ModifiedTime")
.HasColumnType("timestamp with time zone")
.HasComment("修改时间");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("标准化电子邮箱(大写)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("标准化账号(大写)");
b.Property<string>("PasswordHash")
.HasColumnType("text")
.HasComment("密码哈希值");
b.Property<string>("PhoneNumber")
.IsRequired()
.HasColumnType("text")
.HasComment("电话号码");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean")
.HasComment("电话号码是否已验证");
b.Property<string>("RealName")
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("用户名");
b.Property<string>("SecurityStamp")
.HasColumnType("text")
.HasComment("安全戳,用于并发控制");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean")
.HasComment("是否启用双因素认证");
b.Property<string>("UserName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("账号");
b.HasKey("Id");
b.HasIndex("Email")
.IsUnique()
.HasDatabaseName("IX_Users_Email");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.HasIndex("PhoneNumber")
.IsUnique()
.HasDatabaseName("IX_Users_PhoneNumber");
b.HasIndex("UserName")
.IsUnique()
.HasDatabaseName("IX_Users_UserName");
b.ToTable("Users", null, t =>
{
t.HasComment("用户表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Device.CellularDevice", b =>
{
b.Property<string>("Id")
.HasColumnType("text")
.HasComment("设备ID");
b.Property<int>("AgentPort")
.HasColumnType("integer")
.HasComment("Agent端口");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间");
b.Property<string>("CreatedBy")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasComment("设备描述");
b.Property<string>("IpAddress")
.IsRequired()
.HasMaxLength(45)
.HasColumnType("character varying(45)");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<bool>("IsEnabled")
.HasColumnType("boolean")
.HasComment("是否启用");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasComment("设备名称");
b.Property<string>("ProtocolVersionId")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("协议版本ID");
b.Property<string>("SerialNumber")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("序列号");
b.Property<DateTime?>("UpdatedAt")
.IsRequired()
.HasColumnType("timestamp with time zone")
.HasComment("更新时间");
b.Property<string>("UpdatedBy")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("ProtocolVersionId")
.HasDatabaseName("IX_CellularDevices_ProtocolVersionId");
b.HasIndex("SerialNumber")
.IsUnique()
.HasDatabaseName("IX_CellularDevices_SerialNumber");
b.ToTable("CellularDevices", null, t =>
{
t.HasComment("蜂窝设备表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Device.ProtocolVersion", b =>
{
b.Property<string>("Id")
.HasColumnType("text")
.HasComment("版本ID");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间");
b.Property<string>("CreatedBy")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasComment("版本描述");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<bool>("IsEnabled")
.HasColumnType("boolean")
.HasComment("是否启用");
b.Property<bool>("IsForceUpdate")
.HasColumnType("boolean")
.HasComment("是否强制更新");
b.Property<string>("MinimumSupportedVersion")
.HasMaxLength(20)
.HasColumnType("character varying(20)")
.HasComment("最低支持版本");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("版本名称");
b.Property<DateTime?>("ReleaseDate")
.HasColumnType("timestamp with time zone")
.HasComment("发布日期");
b.Property<DateTime?>("UpdatedAt")
.IsRequired()
.HasColumnType("timestamp with time zone")
.HasComment("更新时间");
b.Property<string>("UpdatedBy")
.HasColumnType("text");
b.Property<string>("Version")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)")
.HasComment("版本号");
b.HasKey("Id");
b.HasIndex("Version")
.IsUnique()
.HasDatabaseName("IX_ProtocolVersions_Version");
b.ToTable("ProtocolVersions", null, t =>
{
t.HasComment("协议版本表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Logging.LoginLog", b =>
{
b.Property<string>("Id")
.HasColumnType("text")
.HasComment("日志ID");
b.Property<string>("Browser")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasComment("浏览器信息");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("FailureReason")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasComment("失败原因");
b.Property<string>("IpAddress")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("登录IP");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<bool>("IsSuccess")
.HasColumnType("boolean")
.HasComment("登录状态(成功/失败)");
b.Property<string>("Location")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasComment("登录位置");
b.Property<string>("LoginSource")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime>("LoginTime")
.HasColumnType("timestamp with time zone")
.HasComment("登录时间");
b.Property<string>("LoginType")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("OperatingSystem")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasComment("操作系统信息");
b.Property<string>("SessionId")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("UserAgent")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasComment("设备信息");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(450)
.HasColumnType("character varying(450)")
.HasComment("用户ID");
b.HasKey("Id");
b.HasIndex("IpAddress")
.HasDatabaseName("IX_LoginLogs_IpAddress");
b.HasIndex("LoginTime")
.HasDatabaseName("IX_LoginLogs_LoginTime");
b.HasIndex("UserId")
.HasDatabaseName("IX_LoginLogs_UserId");
b.HasIndex("UserId", "LoginTime")
.HasDatabaseName("IX_LoginLogs_UserId_LoginTime");
b.ToTable("LoginLogs", null, t =>
{
t.HasComment("用户登录日志表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Permission", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.ToTable("Permissions", (string)null);
});
modelBuilder.Entity("CellularManagement.Domain.Entities.RolePermission", b =>
{
b.Property<string>("RoleId")
.HasColumnType("text");
b.Property<string>("PermissionId")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("RoleId", "PermissionId");
b.HasIndex("PermissionId");
b.ToTable("RolePermissions", (string)null);
});
modelBuilder.Entity("CellularManagement.Domain.Entities.UserRole", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("RoleId")
.HasColumnType("text");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("UserRoles", null, t =>
{
t.HasComment("用户角色关系表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Device.CellularDevice", b =>
{
b.HasOne("CellularManagement.Domain.Entities.Device.ProtocolVersion", "ProtocolVersion")
.WithMany()
.HasForeignKey("ProtocolVersionId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("ProtocolVersion");
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Logging.LoginLog", b =>
{
b.HasOne("CellularManagement.Domain.Entities.AppUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("CellularManagement.Domain.Entities.RolePermission", b =>
{
b.HasOne("CellularManagement.Domain.Entities.Permission", "Permission")
.WithMany("RolePermissions")
.HasForeignKey("PermissionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CellularManagement.Domain.Entities.AppRole", "Role")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Permission");
b.Navigation("Role");
});
modelBuilder.Entity("CellularManagement.Domain.Entities.UserRole", b =>
{
b.HasOne("CellularManagement.Domain.Entities.AppRole", "Role")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CellularManagement.Domain.Entities.AppUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Role");
b.Navigation("User");
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Permission", b =>
{
b.Navigation("RolePermissions");
});
#pragma warning restore 612, 618
}
}
}

183
src/X1.Infrastructure/Migrations/20250705173130_InitProtocolVersionAndDevice.cs

@ -1,183 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace X1.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class InitProtocolVersionAndDevice : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ProtocolType",
table: "ProtocolVersions");
migrationBuilder.AlterColumn<string>(
name: "Version",
table: "ProtocolVersions",
type: "character varying(20)",
maxLength: 20,
nullable: false,
comment: "版本号",
oldClrType: typeof(string),
oldType: "character varying(50)",
oldMaxLength: 50,
oldComment: "版本号");
migrationBuilder.AlterColumn<DateTime>(
name: "ReleaseDate",
table: "ProtocolVersions",
type: "timestamp with time zone",
nullable: true,
comment: "发布日期",
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "ProtocolVersions",
type: "character varying(50)",
maxLength: 50,
nullable: false,
comment: "版本名称",
oldClrType: typeof(string),
oldType: "character varying(50)",
oldMaxLength: 50);
migrationBuilder.AlterColumn<string>(
name: "MinimumSupportedVersion",
table: "ProtocolVersions",
type: "character varying(20)",
maxLength: 20,
nullable: true,
comment: "最低支持版本",
oldClrType: typeof(string),
oldType: "character varying(20)",
oldMaxLength: 20,
oldNullable: true);
migrationBuilder.AlterColumn<bool>(
name: "IsForceUpdate",
table: "ProtocolVersions",
type: "boolean",
nullable: false,
comment: "是否强制更新",
oldClrType: typeof(bool),
oldType: "boolean");
migrationBuilder.AlterColumn<bool>(
name: "IsEnabled",
table: "ProtocolVersions",
type: "boolean",
nullable: false,
comment: "是否启用",
oldClrType: typeof(bool),
oldType: "boolean");
migrationBuilder.AlterColumn<string>(
name: "Description",
table: "ProtocolVersions",
type: "character varying(500)",
maxLength: 500,
nullable: true,
comment: "版本描述",
oldClrType: typeof(string),
oldType: "character varying(200)",
oldMaxLength: 200,
oldNullable: true,
oldComment: "版本描述");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Version",
table: "ProtocolVersions",
type: "character varying(50)",
maxLength: 50,
nullable: false,
comment: "版本号",
oldClrType: typeof(string),
oldType: "character varying(20)",
oldMaxLength: 20,
oldComment: "版本号");
migrationBuilder.AlterColumn<DateTime>(
name: "ReleaseDate",
table: "ProtocolVersions",
type: "timestamp with time zone",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true,
oldComment: "发布日期");
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "ProtocolVersions",
type: "character varying(50)",
maxLength: 50,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(50)",
oldMaxLength: 50,
oldComment: "版本名称");
migrationBuilder.AlterColumn<string>(
name: "MinimumSupportedVersion",
table: "ProtocolVersions",
type: "character varying(20)",
maxLength: 20,
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(20)",
oldMaxLength: 20,
oldNullable: true,
oldComment: "最低支持版本");
migrationBuilder.AlterColumn<bool>(
name: "IsForceUpdate",
table: "ProtocolVersions",
type: "boolean",
nullable: false,
oldClrType: typeof(bool),
oldType: "boolean",
oldComment: "是否强制更新");
migrationBuilder.AlterColumn<bool>(
name: "IsEnabled",
table: "ProtocolVersions",
type: "boolean",
nullable: false,
oldClrType: typeof(bool),
oldType: "boolean",
oldComment: "是否启用");
migrationBuilder.AlterColumn<string>(
name: "Description",
table: "ProtocolVersions",
type: "character varying(200)",
maxLength: 200,
nullable: true,
comment: "版本描述",
oldClrType: typeof(string),
oldType: "character varying(500)",
oldMaxLength: 500,
oldNullable: true,
oldComment: "版本描述");
migrationBuilder.AddColumn<string>(
name: "ProtocolType",
table: "ProtocolVersions",
type: "character varying(50)",
maxLength: 50,
nullable: false,
defaultValue: "");
}
}
}

64
src/X1.Infrastructure/Migrations/20250705174217_UpdateProtocolVersionAndCellularDevice.cs

@ -1,64 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace X1.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class UpdateProtocolVersionAndCellularDevice : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IsForceUpdate",
table: "ProtocolVersions");
migrationBuilder.AlterColumn<string>(
name: "IpAddress",
table: "CellularDevices",
type: "character varying(45)",
maxLength: 45,
nullable: false,
comment: "IP地址",
oldClrType: typeof(string),
oldType: "character varying(45)",
oldMaxLength: 45);
migrationBuilder.AddColumn<bool>(
name: "IsRunning",
table: "CellularDevices",
type: "boolean",
nullable: false,
defaultValue: false,
comment: "设备状态(启动/未启动)");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IsRunning",
table: "CellularDevices");
migrationBuilder.AddColumn<bool>(
name: "IsForceUpdate",
table: "ProtocolVersions",
type: "boolean",
nullable: false,
defaultValue: false,
comment: "是否强制更新");
migrationBuilder.AlterColumn<string>(
name: "IpAddress",
table: "CellularDevices",
type: "character varying(45)",
maxLength: 45,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(45)",
oldMaxLength: 45,
oldComment: "IP地址");
}
}
}

384
src/X1.Infrastructure/Migrations/20250705174217_UpdateProtocolVersionAndCellularDevice.Designer.cs → src/X1.Infrastructure/Migrations/20250728081332_InitialCreate.Designer.cs

@ -12,8 +12,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace X1.Infrastructure.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20250705174217_UpdateProtocolVersionAndCellularDevice")]
partial class UpdateProtocolVersionAndCellularDevice
[Migration("20250728081332_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -250,12 +250,6 @@ namespace X1.Infrastructure.Migrations
.HasColumnType("character varying(100)")
.HasComment("设备名称");
b.Property<string>("ProtocolVersionId")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("协议版本ID");
b.Property<string>("SerialNumber")
.IsRequired()
.HasMaxLength(50)
@ -272,9 +266,6 @@ namespace X1.Infrastructure.Migrations
b.HasKey("Id");
b.HasIndex("ProtocolVersionId")
.HasDatabaseName("IX_CellularDevices_ProtocolVersionId");
b.HasIndex("SerialNumber")
.IsUnique()
.HasDatabaseName("IX_CellularDevices_SerialNumber");
@ -326,6 +317,12 @@ namespace X1.Infrastructure.Migrations
.HasColumnType("timestamp with time zone")
.HasComment("发布日期");
b.Property<string>("SerialNumber")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("设备序列号");
b.Property<DateTime?>("UpdatedAt")
.IsRequired()
.HasColumnType("timestamp with time zone")
@ -342,8 +339,10 @@ namespace X1.Infrastructure.Migrations
b.HasKey("Id");
b.HasIndex("SerialNumber")
.HasDatabaseName("IX_ProtocolVersions_SerialNumber");
b.HasIndex("Version")
.IsUnique()
.HasDatabaseName("IX_ProtocolVersions_Version");
b.ToTable("ProtocolVersions", null, t =>
@ -447,6 +446,308 @@ namespace X1.Infrastructure.Migrations
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.NetworkProfile.CoreNetworkConfig", b =>
{
b.Property<string>("Id")
.HasColumnType("text")
.HasComment("配置ID");
b.Property<string>("ConfigContent")
.IsRequired()
.HasColumnType("text")
.HasComment("配置内容(JSON格式)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间");
b.Property<string>("CreatedBy")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasComment("配置描述");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<bool>("IsDisabled")
.HasColumnType("boolean")
.HasComment("是否禁用");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasComment("配置名称");
b.Property<DateTime?>("UpdatedAt")
.IsRequired()
.HasColumnType("timestamp with time zone")
.HasComment("更新时间");
b.Property<string>("UpdatedBy")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Name")
.HasDatabaseName("IX_CoreNetworkConfigs_Name");
b.ToTable("CoreNetworkConfigs", null, t =>
{
t.HasComment("核心网配置表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.NetworkProfile.IMS_Configuration", b =>
{
b.Property<string>("Id")
.HasColumnType("text")
.HasComment("配置ID");
b.Property<string>("ConfigContent")
.IsRequired()
.HasColumnType("text")
.HasComment("配置内容(JSON格式)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间");
b.Property<string>("CreatedBy")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasComment("配置描述");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<bool>("IsDisabled")
.HasColumnType("boolean")
.HasComment("是否禁用");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasComment("配置名称");
b.Property<DateTime?>("UpdatedAt")
.IsRequired()
.HasColumnType("timestamp with time zone")
.HasComment("更新时间");
b.Property<string>("UpdatedBy")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Name")
.HasDatabaseName("IX_IMS_Configurations_Name");
b.ToTable("IMS_Configurations", null, t =>
{
t.HasComment("IMS配置表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.NetworkProfile.NetworkStackConfig", b =>
{
b.Property<string>("Id")
.HasColumnType("text")
.HasComment("配置ID");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间");
b.Property<string>("CreatedBy")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasComment("描述");
b.Property<bool>("IsActive")
.HasColumnType("boolean")
.HasComment("是否激活");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<string>("RanId")
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("RAN配置ID");
b.Property<string>("StackId")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("栈ID");
b.Property<DateTime?>("UpdatedAt")
.IsRequired()
.HasColumnType("timestamp with time zone")
.HasComment("更新时间");
b.Property<string>("UpdatedBy")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("IsActive")
.HasDatabaseName("IX_NetworkStackConfigs_IsActive");
b.HasIndex("RanId")
.HasDatabaseName("IX_NetworkStackConfigs_RanId");
b.HasIndex("StackId")
.IsUnique()
.HasDatabaseName("IX_NetworkStackConfigs_StackId");
b.ToTable("NetworkStackConfigs", null, t =>
{
t.HasComment("网络栈配置表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.NetworkProfile.RAN_Configuration", b =>
{
b.Property<string>("Id")
.HasColumnType("text")
.HasComment("配置ID");
b.Property<string>("ConfigContent")
.IsRequired()
.HasColumnType("text")
.HasComment("配置内容(JSON格式)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间");
b.Property<string>("CreatedBy")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasComment("配置描述");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<bool>("IsDisabled")
.HasColumnType("boolean")
.HasComment("是否禁用");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasComment("配置名称");
b.Property<DateTime?>("UpdatedAt")
.IsRequired()
.HasColumnType("timestamp with time zone")
.HasComment("更新时间");
b.Property<string>("UpdatedBy")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Name")
.HasDatabaseName("IX_RAN_Configurations_Name");
b.ToTable("RAN_Configurations", null, t =>
{
t.HasComment("RAN配置表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.NetworkProfile.Stack_CoreIMS_Binding", b =>
{
b.Property<string>("Id")
.HasColumnType("text")
.HasComment("绑定关系ID");
b.Property<string>("CnId")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("核心网配置ID");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间");
b.Property<string>("CreatedBy")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ImsId")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("IMS配置ID");
b.Property<int>("Index")
.HasColumnType("integer")
.HasComment("索引");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<string>("StackId")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("栈ID");
b.Property<DateTime?>("UpdatedAt")
.IsRequired()
.HasColumnType("timestamp with time zone")
.HasComment("更新时间");
b.Property<string>("UpdatedBy")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("CnId")
.HasDatabaseName("IX_Stack_CoreIMS_Bindings_CnId");
b.HasIndex("ImsId")
.HasDatabaseName("IX_Stack_CoreIMS_Bindings_ImsId");
b.HasIndex("StackId")
.HasDatabaseName("IX_Stack_CoreIMS_Bindings_StackId");
b.HasIndex("StackId", "Index")
.IsUnique()
.HasDatabaseName("IX_Stack_CoreIMS_Bindings_StackId_Index");
b.ToTable("Stack_CoreIMS_Bindings", null, t =>
{
t.HasComment("栈与核心网/IMS绑定关系表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Permission", b =>
{
b.Property<string>("Id")
@ -515,15 +816,14 @@ namespace X1.Infrastructure.Migrations
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Device.CellularDevice", b =>
modelBuilder.Entity("CellularManagement.Domain.Entities.Device.ProtocolVersion", b =>
{
b.HasOne("CellularManagement.Domain.Entities.Device.ProtocolVersion", "ProtocolVersion")
.WithMany()
.HasForeignKey("ProtocolVersionId")
.OnDelete(DeleteBehavior.Restrict)
b.HasOne("CellularManagement.Domain.Entities.Device.CellularDevice", null)
.WithMany("ProtocolVersions")
.HasForeignKey("SerialNumber")
.HasPrincipalKey("SerialNumber")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ProtocolVersion");
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Logging.LoginLog", b =>
@ -535,6 +835,42 @@ namespace X1.Infrastructure.Migrations
.IsRequired();
});
modelBuilder.Entity("CellularManagement.Domain.Entities.NetworkProfile.NetworkStackConfig", b =>
{
b.HasOne("CellularManagement.Domain.Entities.NetworkProfile.RAN_Configuration", null)
.WithMany()
.HasForeignKey("RanId")
.OnDelete(DeleteBehavior.SetNull);
});
modelBuilder.Entity("CellularManagement.Domain.Entities.NetworkProfile.Stack_CoreIMS_Binding", b =>
{
b.HasOne("CellularManagement.Domain.Entities.NetworkProfile.CoreNetworkConfig", "CoreNetworkConfig")
.WithMany()
.HasForeignKey("CnId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CellularManagement.Domain.Entities.NetworkProfile.IMS_Configuration", "IMSConfiguration")
.WithMany()
.HasForeignKey("ImsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CellularManagement.Domain.Entities.NetworkProfile.NetworkStackConfig", "NetworkStackConfig")
.WithMany("StackCoreIMSBindings")
.HasForeignKey("StackId")
.HasPrincipalKey("StackId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("CoreNetworkConfig");
b.Navigation("IMSConfiguration");
b.Navigation("NetworkStackConfig");
});
modelBuilder.Entity("CellularManagement.Domain.Entities.RolePermission", b =>
{
b.HasOne("CellularManagement.Domain.Entities.Permission", "Permission")
@ -573,6 +909,16 @@ namespace X1.Infrastructure.Migrations
b.Navigation("User");
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Device.CellularDevice", b =>
{
b.Navigation("ProtocolVersions");
});
modelBuilder.Entity("CellularManagement.Domain.Entities.NetworkProfile.NetworkStackConfig", b =>
{
b.Navigation("StackCoreIMSBindings");
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Permission", b =>
{
b.Navigation("RolePermissions");

266
src/X1.Infrastructure/Migrations/20250705165102_InitialCreate.cs → src/X1.Infrastructure/Migrations/20250728081332_InitialCreate.cs

@ -11,6 +11,73 @@ namespace X1.Infrastructure.Migrations
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "CellularDevices",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false, comment: "设备ID"),
Name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false, comment: "设备名称"),
SerialNumber = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false, comment: "序列号"),
Description = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false, comment: "设备描述"),
AgentPort = table.Column<int>(type: "integer", nullable: false, comment: "Agent端口"),
IpAddress = table.Column<string>(type: "character varying(45)", maxLength: 45, nullable: false, comment: "IP地址"),
IsEnabled = table.Column<bool>(type: "boolean", nullable: false, comment: "是否启用"),
IsRunning = table.Column<bool>(type: "boolean", nullable: false, comment: "设备状态(启动/未启动)"),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "创建时间"),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "更新时间"),
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
CreatedBy = table.Column<string>(type: "text", nullable: false),
UpdatedBy = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_CellularDevices", x => x.Id);
table.UniqueConstraint("AK_CellularDevices_SerialNumber", x => x.SerialNumber);
},
comment: "蜂窝设备表");
migrationBuilder.CreateTable(
name: "CoreNetworkConfigs",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false, comment: "配置ID"),
Name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false, comment: "配置名称"),
ConfigContent = table.Column<string>(type: "text", nullable: false, comment: "配置内容(JSON格式)"),
Description = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false, comment: "配置描述"),
IsDisabled = table.Column<bool>(type: "boolean", nullable: false, comment: "是否禁用"),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "创建时间"),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "更新时间"),
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
CreatedBy = table.Column<string>(type: "text", nullable: false),
UpdatedBy = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_CoreNetworkConfigs", x => x.Id);
},
comment: "核心网配置表");
migrationBuilder.CreateTable(
name: "IMS_Configurations",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false, comment: "配置ID"),
Name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false, comment: "配置名称"),
ConfigContent = table.Column<string>(type: "text", nullable: false, comment: "配置内容(JSON格式)"),
Description = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false, comment: "配置描述"),
IsDisabled = table.Column<bool>(type: "boolean", nullable: false, comment: "是否禁用"),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "创建时间"),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "更新时间"),
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
CreatedBy = table.Column<string>(type: "text", nullable: false),
UpdatedBy = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_IMS_Configurations", x => x.Id);
},
comment: "IMS配置表");
migrationBuilder.CreateTable(
name: "Permissions",
columns: table => new
@ -28,18 +95,14 @@ namespace X1.Infrastructure.Migrations
});
migrationBuilder.CreateTable(
name: "ProtocolVersions",
name: "RAN_Configurations",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false, comment: "版本ID"),
Name = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
Version = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false, comment: "版本号"),
ProtocolType = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
Description = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true, comment: "版本描述"),
IsEnabled = table.Column<bool>(type: "boolean", nullable: false),
ReleaseDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
IsForceUpdate = table.Column<bool>(type: "boolean", nullable: false),
MinimumSupportedVersion = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: true),
Id = table.Column<string>(type: "text", nullable: false, comment: "配置ID"),
Name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false, comment: "配置名称"),
ConfigContent = table.Column<string>(type: "text", nullable: false, comment: "配置内容(JSON格式)"),
Description = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false, comment: "配置描述"),
IsDisabled = table.Column<bool>(type: "boolean", nullable: false, comment: "是否禁用"),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "创建时间"),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "更新时间"),
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
@ -48,9 +111,9 @@ namespace X1.Infrastructure.Migrations
},
constraints: table =>
{
table.PrimaryKey("PK_ProtocolVersions", x => x.Id);
table.PrimaryKey("PK_RAN_Configurations", x => x.Id);
},
comment: "协议版本表");
comment: "RAN配置表");
migrationBuilder.CreateTable(
name: "Roles",
@ -103,17 +166,17 @@ namespace X1.Infrastructure.Migrations
comment: "用户表");
migrationBuilder.CreateTable(
name: "CellularDevices",
name: "ProtocolVersions",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false, comment: "设备ID"),
Name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false, comment: "设备名称"),
SerialNumber = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false, comment: "序列号"),
Description = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false, comment: "设备描述"),
ProtocolVersionId = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false, comment: "协议版本ID"),
AgentPort = table.Column<int>(type: "integer", nullable: false, comment: "Agent端口"),
IpAddress = table.Column<string>(type: "character varying(45)", maxLength: 45, nullable: false),
Id = table.Column<string>(type: "text", nullable: false, comment: "版本ID"),
Name = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false, comment: "版本名称"),
Version = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false, comment: "版本号"),
Description = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true, comment: "版本描述"),
IsEnabled = table.Column<bool>(type: "boolean", nullable: false, comment: "是否启用"),
ReleaseDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "发布日期"),
MinimumSupportedVersion = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: true, comment: "最低支持版本"),
SerialNumber = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false, comment: "设备序列号"),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "创建时间"),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "更新时间"),
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
@ -122,15 +185,43 @@ namespace X1.Infrastructure.Migrations
},
constraints: table =>
{
table.PrimaryKey("PK_CellularDevices", x => x.Id);
table.PrimaryKey("PK_ProtocolVersions", x => x.Id);
table.ForeignKey(
name: "FK_CellularDevices_ProtocolVersions_ProtocolVersionId",
column: x => x.ProtocolVersionId,
principalTable: "ProtocolVersions",
name: "FK_ProtocolVersions_CellularDevices_SerialNumber",
column: x => x.SerialNumber,
principalTable: "CellularDevices",
principalColumn: "SerialNumber",
onDelete: ReferentialAction.Cascade);
},
comment: "协议版本表");
migrationBuilder.CreateTable(
name: "NetworkStackConfigs",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false, comment: "配置ID"),
StackId = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false, comment: "栈ID"),
RanId = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true, comment: "RAN配置ID"),
Description = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true, comment: "描述"),
IsActive = table.Column<bool>(type: "boolean", nullable: false, comment: "是否激活"),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "创建时间"),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "更新时间"),
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
CreatedBy = table.Column<string>(type: "text", nullable: false),
UpdatedBy = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_NetworkStackConfigs", x => x.Id);
table.UniqueConstraint("AK_NetworkStackConfigs_StackId", x => x.StackId);
table.ForeignKey(
name: "FK_NetworkStackConfigs_RAN_Configurations_RanId",
column: x => x.RanId,
principalTable: "RAN_Configurations",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
onDelete: ReferentialAction.SetNull);
},
comment: "蜂窝设备表");
comment: "网络栈配置表");
migrationBuilder.CreateTable(
name: "RolePermissions",
@ -215,10 +306,44 @@ namespace X1.Infrastructure.Migrations
},
comment: "用户角色关系表");
migrationBuilder.CreateIndex(
name: "IX_CellularDevices_ProtocolVersionId",
table: "CellularDevices",
column: "ProtocolVersionId");
migrationBuilder.CreateTable(
name: "Stack_CoreIMS_Bindings",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false, comment: "绑定关系ID"),
StackId = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false, comment: "栈ID"),
Index = table.Column<int>(type: "integer", nullable: false, comment: "索引"),
CnId = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false, comment: "核心网配置ID"),
ImsId = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false, comment: "IMS配置ID"),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "创建时间"),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "更新时间"),
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
CreatedBy = table.Column<string>(type: "text", nullable: false),
UpdatedBy = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Stack_CoreIMS_Bindings", x => x.Id);
table.ForeignKey(
name: "FK_Stack_CoreIMS_Bindings_CoreNetworkConfigs_CnId",
column: x => x.CnId,
principalTable: "CoreNetworkConfigs",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Stack_CoreIMS_Bindings_IMS_Configurations_ImsId",
column: x => x.ImsId,
principalTable: "IMS_Configurations",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Stack_CoreIMS_Bindings_NetworkStackConfigs_StackId",
column: x => x.StackId,
principalTable: "NetworkStackConfigs",
principalColumn: "StackId",
onDelete: ReferentialAction.Cascade);
},
comment: "栈与核心网/IMS绑定关系表");
migrationBuilder.CreateIndex(
name: "IX_CellularDevices_SerialNumber",
@ -226,6 +351,16 @@ namespace X1.Infrastructure.Migrations
column: "SerialNumber",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_CoreNetworkConfigs_Name",
table: "CoreNetworkConfigs",
column: "Name");
migrationBuilder.CreateIndex(
name: "IX_IMS_Configurations_Name",
table: "IMS_Configurations",
column: "Name");
migrationBuilder.CreateIndex(
name: "IX_LoginLogs_IpAddress",
table: "LoginLogs",
@ -246,11 +381,36 @@ namespace X1.Infrastructure.Migrations
table: "LoginLogs",
columns: new[] { "UserId", "LoginTime" });
migrationBuilder.CreateIndex(
name: "IX_NetworkStackConfigs_IsActive",
table: "NetworkStackConfigs",
column: "IsActive");
migrationBuilder.CreateIndex(
name: "IX_NetworkStackConfigs_RanId",
table: "NetworkStackConfigs",
column: "RanId");
migrationBuilder.CreateIndex(
name: "IX_NetworkStackConfigs_StackId",
table: "NetworkStackConfigs",
column: "StackId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ProtocolVersions_SerialNumber",
table: "ProtocolVersions",
column: "SerialNumber");
migrationBuilder.CreateIndex(
name: "IX_ProtocolVersions_Version",
table: "ProtocolVersions",
column: "Version",
unique: true);
column: "Version");
migrationBuilder.CreateIndex(
name: "IX_RAN_Configurations_Name",
table: "RAN_Configurations",
column: "Name");
migrationBuilder.CreateIndex(
name: "IX_RolePermissions_PermissionId",
@ -269,6 +429,27 @@ namespace X1.Infrastructure.Migrations
column: "NormalizedName",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Stack_CoreIMS_Bindings_CnId",
table: "Stack_CoreIMS_Bindings",
column: "CnId");
migrationBuilder.CreateIndex(
name: "IX_Stack_CoreIMS_Bindings_ImsId",
table: "Stack_CoreIMS_Bindings",
column: "ImsId");
migrationBuilder.CreateIndex(
name: "IX_Stack_CoreIMS_Bindings_StackId",
table: "Stack_CoreIMS_Bindings",
column: "StackId");
migrationBuilder.CreateIndex(
name: "IX_Stack_CoreIMS_Bindings_StackId_Index",
table: "Stack_CoreIMS_Bindings",
columns: new[] { "StackId", "Index" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_UserRoles_RoleId",
table: "UserRoles",
@ -308,28 +489,43 @@ namespace X1.Infrastructure.Migrations
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CellularDevices");
name: "LoginLogs");
migrationBuilder.DropTable(
name: "LoginLogs");
name: "ProtocolVersions");
migrationBuilder.DropTable(
name: "RolePermissions");
migrationBuilder.DropTable(
name: "Stack_CoreIMS_Bindings");
migrationBuilder.DropTable(
name: "UserRoles");
migrationBuilder.DropTable(
name: "ProtocolVersions");
name: "CellularDevices");
migrationBuilder.DropTable(
name: "Permissions");
migrationBuilder.DropTable(
name: "CoreNetworkConfigs");
migrationBuilder.DropTable(
name: "IMS_Configurations");
migrationBuilder.DropTable(
name: "NetworkStackConfigs");
migrationBuilder.DropTable(
name: "Roles");
migrationBuilder.DropTable(
name: "Users");
migrationBuilder.DropTable(
name: "RAN_Configurations");
}
}
}

380
src/X1.Infrastructure/Migrations/AppDbContextModelSnapshot.cs

@ -247,12 +247,6 @@ namespace X1.Infrastructure.Migrations
.HasColumnType("character varying(100)")
.HasComment("设备名称");
b.Property<string>("ProtocolVersionId")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("协议版本ID");
b.Property<string>("SerialNumber")
.IsRequired()
.HasMaxLength(50)
@ -269,9 +263,6 @@ namespace X1.Infrastructure.Migrations
b.HasKey("Id");
b.HasIndex("ProtocolVersionId")
.HasDatabaseName("IX_CellularDevices_ProtocolVersionId");
b.HasIndex("SerialNumber")
.IsUnique()
.HasDatabaseName("IX_CellularDevices_SerialNumber");
@ -323,6 +314,12 @@ namespace X1.Infrastructure.Migrations
.HasColumnType("timestamp with time zone")
.HasComment("发布日期");
b.Property<string>("SerialNumber")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("设备序列号");
b.Property<DateTime?>("UpdatedAt")
.IsRequired()
.HasColumnType("timestamp with time zone")
@ -339,8 +336,10 @@ namespace X1.Infrastructure.Migrations
b.HasKey("Id");
b.HasIndex("SerialNumber")
.HasDatabaseName("IX_ProtocolVersions_SerialNumber");
b.HasIndex("Version")
.IsUnique()
.HasDatabaseName("IX_ProtocolVersions_Version");
b.ToTable("ProtocolVersions", null, t =>
@ -444,6 +443,308 @@ namespace X1.Infrastructure.Migrations
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.NetworkProfile.CoreNetworkConfig", b =>
{
b.Property<string>("Id")
.HasColumnType("text")
.HasComment("配置ID");
b.Property<string>("ConfigContent")
.IsRequired()
.HasColumnType("text")
.HasComment("配置内容(JSON格式)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间");
b.Property<string>("CreatedBy")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasComment("配置描述");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<bool>("IsDisabled")
.HasColumnType("boolean")
.HasComment("是否禁用");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasComment("配置名称");
b.Property<DateTime?>("UpdatedAt")
.IsRequired()
.HasColumnType("timestamp with time zone")
.HasComment("更新时间");
b.Property<string>("UpdatedBy")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Name")
.HasDatabaseName("IX_CoreNetworkConfigs_Name");
b.ToTable("CoreNetworkConfigs", null, t =>
{
t.HasComment("核心网配置表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.NetworkProfile.IMS_Configuration", b =>
{
b.Property<string>("Id")
.HasColumnType("text")
.HasComment("配置ID");
b.Property<string>("ConfigContent")
.IsRequired()
.HasColumnType("text")
.HasComment("配置内容(JSON格式)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间");
b.Property<string>("CreatedBy")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasComment("配置描述");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<bool>("IsDisabled")
.HasColumnType("boolean")
.HasComment("是否禁用");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasComment("配置名称");
b.Property<DateTime?>("UpdatedAt")
.IsRequired()
.HasColumnType("timestamp with time zone")
.HasComment("更新时间");
b.Property<string>("UpdatedBy")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Name")
.HasDatabaseName("IX_IMS_Configurations_Name");
b.ToTable("IMS_Configurations", null, t =>
{
t.HasComment("IMS配置表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.NetworkProfile.NetworkStackConfig", b =>
{
b.Property<string>("Id")
.HasColumnType("text")
.HasComment("配置ID");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间");
b.Property<string>("CreatedBy")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasComment("描述");
b.Property<bool>("IsActive")
.HasColumnType("boolean")
.HasComment("是否激活");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<string>("RanId")
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("RAN配置ID");
b.Property<string>("StackId")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("栈ID");
b.Property<DateTime?>("UpdatedAt")
.IsRequired()
.HasColumnType("timestamp with time zone")
.HasComment("更新时间");
b.Property<string>("UpdatedBy")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("IsActive")
.HasDatabaseName("IX_NetworkStackConfigs_IsActive");
b.HasIndex("RanId")
.HasDatabaseName("IX_NetworkStackConfigs_RanId");
b.HasIndex("StackId")
.IsUnique()
.HasDatabaseName("IX_NetworkStackConfigs_StackId");
b.ToTable("NetworkStackConfigs", null, t =>
{
t.HasComment("网络栈配置表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.NetworkProfile.RAN_Configuration", b =>
{
b.Property<string>("Id")
.HasColumnType("text")
.HasComment("配置ID");
b.Property<string>("ConfigContent")
.IsRequired()
.HasColumnType("text")
.HasComment("配置内容(JSON格式)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间");
b.Property<string>("CreatedBy")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasComment("配置描述");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<bool>("IsDisabled")
.HasColumnType("boolean")
.HasComment("是否禁用");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasComment("配置名称");
b.Property<DateTime?>("UpdatedAt")
.IsRequired()
.HasColumnType("timestamp with time zone")
.HasComment("更新时间");
b.Property<string>("UpdatedBy")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Name")
.HasDatabaseName("IX_RAN_Configurations_Name");
b.ToTable("RAN_Configurations", null, t =>
{
t.HasComment("RAN配置表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.NetworkProfile.Stack_CoreIMS_Binding", b =>
{
b.Property<string>("Id")
.HasColumnType("text")
.HasComment("绑定关系ID");
b.Property<string>("CnId")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("核心网配置ID");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间");
b.Property<string>("CreatedBy")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ImsId")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("IMS配置ID");
b.Property<int>("Index")
.HasColumnType("integer")
.HasComment("索引");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<string>("StackId")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("栈ID");
b.Property<DateTime?>("UpdatedAt")
.IsRequired()
.HasColumnType("timestamp with time zone")
.HasComment("更新时间");
b.Property<string>("UpdatedBy")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("CnId")
.HasDatabaseName("IX_Stack_CoreIMS_Bindings_CnId");
b.HasIndex("ImsId")
.HasDatabaseName("IX_Stack_CoreIMS_Bindings_ImsId");
b.HasIndex("StackId")
.HasDatabaseName("IX_Stack_CoreIMS_Bindings_StackId");
b.HasIndex("StackId", "Index")
.IsUnique()
.HasDatabaseName("IX_Stack_CoreIMS_Bindings_StackId_Index");
b.ToTable("Stack_CoreIMS_Bindings", null, t =>
{
t.HasComment("栈与核心网/IMS绑定关系表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Permission", b =>
{
b.Property<string>("Id")
@ -512,15 +813,14 @@ namespace X1.Infrastructure.Migrations
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Device.CellularDevice", b =>
modelBuilder.Entity("CellularManagement.Domain.Entities.Device.ProtocolVersion", b =>
{
b.HasOne("CellularManagement.Domain.Entities.Device.ProtocolVersion", "ProtocolVersion")
.WithMany()
.HasForeignKey("ProtocolVersionId")
.OnDelete(DeleteBehavior.Restrict)
b.HasOne("CellularManagement.Domain.Entities.Device.CellularDevice", null)
.WithMany("ProtocolVersions")
.HasForeignKey("SerialNumber")
.HasPrincipalKey("SerialNumber")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ProtocolVersion");
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Logging.LoginLog", b =>
@ -532,6 +832,42 @@ namespace X1.Infrastructure.Migrations
.IsRequired();
});
modelBuilder.Entity("CellularManagement.Domain.Entities.NetworkProfile.NetworkStackConfig", b =>
{
b.HasOne("CellularManagement.Domain.Entities.NetworkProfile.RAN_Configuration", null)
.WithMany()
.HasForeignKey("RanId")
.OnDelete(DeleteBehavior.SetNull);
});
modelBuilder.Entity("CellularManagement.Domain.Entities.NetworkProfile.Stack_CoreIMS_Binding", b =>
{
b.HasOne("CellularManagement.Domain.Entities.NetworkProfile.CoreNetworkConfig", "CoreNetworkConfig")
.WithMany()
.HasForeignKey("CnId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CellularManagement.Domain.Entities.NetworkProfile.IMS_Configuration", "IMSConfiguration")
.WithMany()
.HasForeignKey("ImsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CellularManagement.Domain.Entities.NetworkProfile.NetworkStackConfig", "NetworkStackConfig")
.WithMany("StackCoreIMSBindings")
.HasForeignKey("StackId")
.HasPrincipalKey("StackId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("CoreNetworkConfig");
b.Navigation("IMSConfiguration");
b.Navigation("NetworkStackConfig");
});
modelBuilder.Entity("CellularManagement.Domain.Entities.RolePermission", b =>
{
b.HasOne("CellularManagement.Domain.Entities.Permission", "Permission")
@ -570,6 +906,16 @@ namespace X1.Infrastructure.Migrations
b.Navigation("User");
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Device.CellularDevice", b =>
{
b.Navigation("ProtocolVersions");
});
modelBuilder.Entity("CellularManagement.Domain.Entities.NetworkProfile.NetworkStackConfig", b =>
{
b.Navigation("StackCoreIMSBindings");
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Permission", b =>
{
b.Navigation("RolePermissions");

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

File diff suppressed because it is too large

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

@ -1454,3 +1454,377 @@ POSITION: 158
File: parse_relation.c
Line: 1381
Routine: parserOpenTable
2025-07-28 12:20:21.004 +08:00 [ERR] DESKTOP-T6EU05A [15] Failed executing DbCommand (18ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
SELECT r."Id", r."ConfigContent", r."CreatedAt", r."CreatedBy", r."Description", r."IsDeleted", r."IsDisabled", r."Name", r."UpdatedAt", r."UpdatedBy"
FROM "RAN_Configurations" AS r
2025-07-28 12:20:21.025 +08:00 [ERR] DESKTOP-T6EU05A [15] An exception occurred while iterating over the results of a query for context type 'CellularManagement.Infrastructure.Context.AppDbContext'.
Npgsql.PostgresException (0x80004005): 42P01: relation "RAN_Configurations" does not exist
POSITION: 158
at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.InitializeReaderAsync(AsyncEnumerator enumerator, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.<>c__DisplayClass30_0`2.<<ExecuteAsync>b__0>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementationAsync[TState,TResult](Func`4 operation, Func`4 verifySucceeded, TState state, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementationAsync[TState,TResult](Func`4 operation, Func`4 verifySucceeded, TState state, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
Exception data:
Severity: ERROR
SqlState: 42P01
MessageText: relation "RAN_Configurations" does not exist
Position: 158
File: parse_relation.c
Line: 1381
Routine: parserOpenTable
Npgsql.PostgresException (0x80004005): 42P01: relation "RAN_Configurations" does not exist
POSITION: 158
at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.InitializeReaderAsync(AsyncEnumerator enumerator, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.<>c__DisplayClass30_0`2.<<ExecuteAsync>b__0>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementationAsync[TState,TResult](Func`4 operation, Func`4 verifySucceeded, TState state, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementationAsync[TState,TResult](Func`4 operation, Func`4 verifySucceeded, TState state, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
Exception data:
Severity: ERROR
SqlState: 42P01
MessageText: relation "RAN_Configurations" does not exist
Position: 158
File: parse_relation.c
Line: 1381
Routine: parserOpenTable
2025-07-28 12:20:21.215 +08:00 [ERR] DESKTOP-T6EU05A [15] 获取RAN配置列表时发生错误
Npgsql.PostgresException (0x80004005): 42P01: relation "RAN_Configurations" does not exist
POSITION: 158
at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.InitializeReaderAsync(AsyncEnumerator enumerator, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.<>c__DisplayClass30_0`2.<<ExecuteAsync>b__0>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementationAsync[TState,TResult](Func`4 operation, Func`4 verifySucceeded, TState state, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementationAsync[TState,TResult](Func`4 operation, Func`4 verifySucceeded, TState state, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
at CellularManagement.Infrastructure.Repositories.CQRS.QueryRepository`1.FindAsync(Expression`1 predicate, Func`2 include, CancellationToken cancellationToken) in D:\dev\clean-architecture-starter-main\CellularManagement\src\X1.Infrastructure\Repositories\CQRS\QueryRepository.cs:line 74
at CellularManagement.Infrastructure.Repositories.NetworkProfile.RAN_ConfigurationRepository.SearchRAN_ConfigurationsAsync(String keyword, CancellationToken cancellationToken) in D:\dev\clean-architecture-starter-main\CellularManagement\src\X1.Infrastructure\Repositories\NetworkProfile\RAN_ConfigurationRepository.cs:line 86
at CellularManagement.Application.Features.RANConfiguration.Queries.GetRAN_Configurations.GetRAN_ConfigurationsQueryHandler.Handle(GetRAN_ConfigurationsQuery request, CancellationToken cancellationToken) in D:\dev\clean-architecture-starter-main\CellularManagement\src\X1.Application\Features\RANConfiguration\Queries\GetRAN_Configurations\GetRAN_ConfigurationsQueryHandler.cs:line 53
Exception data:
Severity: ERROR
SqlState: 42P01
MessageText: relation "RAN_Configurations" does not exist
Position: 158
File: parse_relation.c
Line: 1381
Routine: parserOpenTable
2025-07-28 12:20:21.271 +08:00 [ERR] DESKTOP-T6EU05A [15] Failed executing DbCommand (12ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
SELECT r."Id", r."ConfigContent", r."CreatedAt", r."CreatedBy", r."Description", r."IsDeleted", r."IsDisabled", r."Name", r."UpdatedAt", r."UpdatedBy"
FROM "RAN_Configurations" AS r
2025-07-28 12:20:21.275 +08:00 [ERR] DESKTOP-T6EU05A [15] An exception occurred while iterating over the results of a query for context type 'CellularManagement.Infrastructure.Context.AppDbContext'.
Npgsql.PostgresException (0x80004005): 42P01: relation "RAN_Configurations" does not exist
POSITION: 158
at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.InitializeReaderAsync(AsyncEnumerator enumerator, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.<>c__DisplayClass30_0`2.<<ExecuteAsync>b__0>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementationAsync[TState,TResult](Func`4 operation, Func`4 verifySucceeded, TState state, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementationAsync[TState,TResult](Func`4 operation, Func`4 verifySucceeded, TState state, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
Exception data:
Severity: ERROR
SqlState: 42P01
MessageText: relation "RAN_Configurations" does not exist
Position: 158
File: parse_relation.c
Line: 1381
Routine: parserOpenTable
Npgsql.PostgresException (0x80004005): 42P01: relation "RAN_Configurations" does not exist
POSITION: 158
at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.InitializeReaderAsync(AsyncEnumerator enumerator, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.<>c__DisplayClass30_0`2.<<ExecuteAsync>b__0>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementationAsync[TState,TResult](Func`4 operation, Func`4 verifySucceeded, TState state, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementationAsync[TState,TResult](Func`4 operation, Func`4 verifySucceeded, TState state, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
Exception data:
Severity: ERROR
SqlState: 42P01
MessageText: relation "RAN_Configurations" does not exist
Position: 158
File: parse_relation.c
Line: 1381
Routine: parserOpenTable
2025-07-28 12:20:21.409 +08:00 [ERR] DESKTOP-T6EU05A [15] 获取RAN配置列表时发生错误
Npgsql.PostgresException (0x80004005): 42P01: relation "RAN_Configurations" does not exist
POSITION: 158
at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.InitializeReaderAsync(AsyncEnumerator enumerator, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.<>c__DisplayClass30_0`2.<<ExecuteAsync>b__0>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementationAsync[TState,TResult](Func`4 operation, Func`4 verifySucceeded, TState state, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementationAsync[TState,TResult](Func`4 operation, Func`4 verifySucceeded, TState state, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
at CellularManagement.Infrastructure.Repositories.CQRS.QueryRepository`1.FindAsync(Expression`1 predicate, Func`2 include, CancellationToken cancellationToken) in D:\dev\clean-architecture-starter-main\CellularManagement\src\X1.Infrastructure\Repositories\CQRS\QueryRepository.cs:line 74
at CellularManagement.Infrastructure.Repositories.NetworkProfile.RAN_ConfigurationRepository.SearchRAN_ConfigurationsAsync(String keyword, CancellationToken cancellationToken) in D:\dev\clean-architecture-starter-main\CellularManagement\src\X1.Infrastructure\Repositories\NetworkProfile\RAN_ConfigurationRepository.cs:line 86
at CellularManagement.Application.Features.RANConfiguration.Queries.GetRAN_Configurations.GetRAN_ConfigurationsQueryHandler.Handle(GetRAN_ConfigurationsQuery request, CancellationToken cancellationToken) in D:\dev\clean-architecture-starter-main\CellularManagement\src\X1.Application\Features\RANConfiguration\Queries\GetRAN_Configurations\GetRAN_ConfigurationsQueryHandler.cs:line 53
Exception data:
Severity: ERROR
SqlState: 42P01
MessageText: relation "RAN_Configurations" does not exist
Position: 158
File: parse_relation.c
Line: 1381
Routine: parserOpenTable
2025-07-28 12:23:22.856 +08:00 [ERR] DESKTOP-T6EU05A [13] Failed executing DbCommand (11ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
SELECT count(*)::int
FROM "NetworkStackConfigs" AS n
2025-07-28 12:23:22.860 +08:00 [ERR] DESKTOP-T6EU05A [13] An exception occurred while iterating over the results of a query for context type 'CellularManagement.Infrastructure.Context.AppDbContext'.
Npgsql.PostgresException (0x80004005): 42P01: relation "NetworkStackConfigs" does not exist
POSITION: 28
at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.InitializeReaderAsync(AsyncEnumerator enumerator, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.<>c__DisplayClass30_0`2.<<ExecuteAsync>b__0>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementationAsync[TState,TResult](Func`4 operation, Func`4 verifySucceeded, TState state, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementationAsync[TState,TResult](Func`4 operation, Func`4 verifySucceeded, TState state, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
Exception data:
Severity: ERROR
SqlState: 42P01
MessageText: relation "NetworkStackConfigs" does not exist
Position: 28
File: parse_relation.c
Line: 1381
Routine: parserOpenTable
Npgsql.PostgresException (0x80004005): 42P01: relation "NetworkStackConfigs" does not exist
POSITION: 28
at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.InitializeReaderAsync(AsyncEnumerator enumerator, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.<>c__DisplayClass30_0`2.<<ExecuteAsync>b__0>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementationAsync[TState,TResult](Func`4 operation, Func`4 verifySucceeded, TState state, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementationAsync[TState,TResult](Func`4 operation, Func`4 verifySucceeded, TState state, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
Exception data:
Severity: ERROR
SqlState: 42P01
MessageText: relation "NetworkStackConfigs" does not exist
Position: 28
File: parse_relation.c
Line: 1381
Routine: parserOpenTable
2025-07-28 12:23:23.031 +08:00 [ERR] DESKTOP-T6EU05A [13] 获取网络栈配置列表时发生错误
Npgsql.PostgresException (0x80004005): 42P01: relation "NetworkStackConfigs" does not exist
POSITION: 28
at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.InitializeReaderAsync(AsyncEnumerator enumerator, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.<>c__DisplayClass30_0`2.<<ExecuteAsync>b__0>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementationAsync[TState,TResult](Func`4 operation, Func`4 verifySucceeded, TState state, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementationAsync[TState,TResult](Func`4 operation, Func`4 verifySucceeded, TState state, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.SingleAsync[TSource](IAsyncEnumerable`1 asyncEnumerable, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.SingleAsync[TSource](IAsyncEnumerable`1 asyncEnumerable, CancellationToken cancellationToken)
at CellularManagement.Infrastructure.Repositories.CQRS.QueryRepository`1.GetPagedAsync(Expression`1 predicate, Int32 pageNumber, Int32 pageSize, Func`2 include, CancellationToken cancellationToken) in D:\dev\clean-architecture-starter-main\CellularManagement\src\X1.Infrastructure\Repositories\CQRS\QueryRepository.cs:line 101
at CellularManagement.Infrastructure.Repositories.NetworkProfile.NetworkStackConfigRepository.SearchNetworkStackConfigsAsync(String keyword, Int32 pageNumber, Int32 pageSize, CancellationToken cancellationToken) in D:\dev\clean-architecture-starter-main\CellularManagement\src\X1.Infrastructure\Repositories\NetworkProfile\NetworkStackConfigRepository.cs:line 136
at CellularManagement.Application.Features.NetworkStackConfigs.Queries.GetNetworkStackConfigs.GetNetworkStackConfigsQueryHandler.Handle(GetNetworkStackConfigsQuery request, CancellationToken cancellationToken) in D:\dev\clean-architecture-starter-main\CellularManagement\src\X1.Application\Features\NetworkStackConfigs\Queries\GetNetworkStackConfigs\GetNetworkStackConfigsQueryHandler.cs:line 38
Exception data:
Severity: ERROR
SqlState: 42P01
MessageText: relation "NetworkStackConfigs" does not exist
Position: 28
File: parse_relation.c
Line: 1381
Routine: parserOpenTable
2025-07-28 12:23:23.073 +08:00 [ERR] DESKTOP-T6EU05A [7] Failed executing DbCommand (11ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
SELECT count(*)::int
FROM "NetworkStackConfigs" AS n
2025-07-28 12:23:23.077 +08:00 [ERR] DESKTOP-T6EU05A [7] An exception occurred while iterating over the results of a query for context type 'CellularManagement.Infrastructure.Context.AppDbContext'.
Npgsql.PostgresException (0x80004005): 42P01: relation "NetworkStackConfigs" does not exist
POSITION: 28
at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.InitializeReaderAsync(AsyncEnumerator enumerator, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.<>c__DisplayClass30_0`2.<<ExecuteAsync>b__0>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementationAsync[TState,TResult](Func`4 operation, Func`4 verifySucceeded, TState state, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementationAsync[TState,TResult](Func`4 operation, Func`4 verifySucceeded, TState state, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
Exception data:
Severity: ERROR
SqlState: 42P01
MessageText: relation "NetworkStackConfigs" does not exist
Position: 28
File: parse_relation.c
Line: 1381
Routine: parserOpenTable
Npgsql.PostgresException (0x80004005): 42P01: relation "NetworkStackConfigs" does not exist
POSITION: 28
at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.InitializeReaderAsync(AsyncEnumerator enumerator, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.<>c__DisplayClass30_0`2.<<ExecuteAsync>b__0>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementationAsync[TState,TResult](Func`4 operation, Func`4 verifySucceeded, TState state, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementationAsync[TState,TResult](Func`4 operation, Func`4 verifySucceeded, TState state, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
Exception data:
Severity: ERROR
SqlState: 42P01
MessageText: relation "NetworkStackConfigs" does not exist
Position: 28
File: parse_relation.c
Line: 1381
Routine: parserOpenTable
2025-07-28 12:23:23.198 +08:00 [ERR] DESKTOP-T6EU05A [7] 获取网络栈配置列表时发生错误
Npgsql.PostgresException (0x80004005): 42P01: relation "NetworkStackConfigs" does not exist
POSITION: 28
at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.InitializeReaderAsync(AsyncEnumerator enumerator, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.<>c__DisplayClass30_0`2.<<ExecuteAsync>b__0>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementationAsync[TState,TResult](Func`4 operation, Func`4 verifySucceeded, TState state, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementationAsync[TState,TResult](Func`4 operation, Func`4 verifySucceeded, TState state, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.SingleAsync[TSource](IAsyncEnumerable`1 asyncEnumerable, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.SingleAsync[TSource](IAsyncEnumerable`1 asyncEnumerable, CancellationToken cancellationToken)
at CellularManagement.Infrastructure.Repositories.CQRS.QueryRepository`1.GetPagedAsync(Expression`1 predicate, Int32 pageNumber, Int32 pageSize, Func`2 include, CancellationToken cancellationToken) in D:\dev\clean-architecture-starter-main\CellularManagement\src\X1.Infrastructure\Repositories\CQRS\QueryRepository.cs:line 101
at CellularManagement.Infrastructure.Repositories.NetworkProfile.NetworkStackConfigRepository.SearchNetworkStackConfigsAsync(String keyword, Int32 pageNumber, Int32 pageSize, CancellationToken cancellationToken) in D:\dev\clean-architecture-starter-main\CellularManagement\src\X1.Infrastructure\Repositories\NetworkProfile\NetworkStackConfigRepository.cs:line 136
at CellularManagement.Application.Features.NetworkStackConfigs.Queries.GetNetworkStackConfigs.GetNetworkStackConfigsQueryHandler.Handle(GetNetworkStackConfigsQuery request, CancellationToken cancellationToken) in D:\dev\clean-architecture-starter-main\CellularManagement\src\X1.Application\Features\NetworkStackConfigs\Queries\GetNetworkStackConfigs\GetNetworkStackConfigsQueryHandler.cs:line 38
Exception data:
Severity: ERROR
SqlState: 42P01
MessageText: relation "NetworkStackConfigs" does not exist
Position: 28
File: parse_relation.c
Line: 1381
Routine: parserOpenTable
2025-07-28 16:20:17.936 +08:00 [ERR] DESKTOP-T6EU05A [9] 用户认证失败
System.NullReferenceException: Object reference not set to an instance of an object.
at CellularManagement.Application.Features.Auth.Commands.BaseLoginCommandHandler`2.HandleLoginAsync(TCommand request, CancellationToken cancellationToken)
2025-07-28 16:20:21.195 +08:00 [ERR] DESKTOP-T6EU05A [5] 用户认证失败
System.ArgumentNullException: Value cannot be null. (Parameter 'entity')
at Microsoft.EntityFrameworkCore.Utilities.Check.NotNull[T](T value, String parameterName)
at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.AddAsync(TEntity entity, CancellationToken cancellationToken)
at CellularManagement.Infrastructure.Repositories.CQRS.CommandRepository`1.AddAsync(T entity, CancellationToken cancellationToken) in D:\dev\clean-architecture-starter-main\CellularManagement\src\X1.Infrastructure\Repositories\CQRS\CommandRepository.cs:line 56
at CellularManagement.Infrastructure.Repositories.Identity.LoginLogRepository.RecordLoginAsync(LoginLog log, CancellationToken cancellationToken) in D:\dev\clean-architecture-starter-main\CellularManagement\src\X1.Infrastructure\Repositories\Identity\LoginLogRepository.cs:line 42
at CellularManagement.Application.Features.Auth.Commands.BaseLoginCommandHandler`2.HandleLoginAsync(TCommand request, CancellationToken cancellationToken)

209
src/X1.WebUI/src/components/ui/ConfigContentEditor.tsx

@ -0,0 +1,209 @@
import React, { useState, useRef, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { MagnifyingGlassIcon, Cross2Icon } from '@radix-ui/react-icons';
interface ConfigContentEditorProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
rows?: number;
disabled?: boolean;
required?: boolean;
label?: string;
className?: string;
}
export default function ConfigContentEditor({
value,
onChange,
placeholder = "请输入配置内容",
rows = 8,
disabled = false,
required = false,
label = "配置内容",
className = ""
}: ConfigContentEditorProps) {
const [searchTerm, setSearchTerm] = useState('');
const [highlightedContent, setHighlightedContent] = useState('');
const [isSearchVisible, setIsSearchVisible] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement>(null);
// 高亮搜索内容
useEffect(() => {
if (!searchTerm.trim()) {
setHighlightedContent(value);
return;
}
const regex = new RegExp(`(${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
const highlighted = value.replace(regex, '<mark class="bg-yellow-200 text-black">$1</mark>');
setHighlightedContent(highlighted);
}, [value, searchTerm]);
// 搜索下一个匹配项
const findNext = () => {
if (!searchTerm.trim() || !textareaRef.current) return;
const textarea = textareaRef.current;
const text = textarea.value;
const currentPos = textarea.selectionStart;
const searchIndex = text.toLowerCase().indexOf(searchTerm.toLowerCase(), currentPos);
if (searchIndex !== -1) {
textarea.setSelectionRange(searchIndex, searchIndex + searchTerm.length);
textarea.focus();
} else {
// 如果从当前位置没找到,从头开始搜索
const firstIndex = text.toLowerCase().indexOf(searchTerm.toLowerCase());
if (firstIndex !== -1) {
textarea.setSelectionRange(firstIndex, firstIndex + searchTerm.length);
textarea.focus();
}
}
};
// 搜索上一个匹配项
const findPrevious = () => {
if (!searchTerm.trim() || !textareaRef.current) return;
const textarea = textareaRef.current;
const text = textarea.value;
const currentPos = textarea.selectionStart;
// 从当前位置向前搜索
const beforeText = text.substring(0, currentPos);
const lastIndex = beforeText.toLowerCase().lastIndexOf(searchTerm.toLowerCase());
if (lastIndex !== -1) {
textarea.setSelectionRange(lastIndex, lastIndex + searchTerm.length);
textarea.focus();
} else {
// 如果从当前位置没找到,从末尾开始搜索
const lastIndexFromEnd = text.toLowerCase().lastIndexOf(searchTerm.toLowerCase());
if (lastIndexFromEnd !== -1) {
textarea.setSelectionRange(lastIndexFromEnd, lastIndexFromEnd + searchTerm.length);
textarea.focus();
}
}
};
// 计算匹配数量
const getMatchCount = () => {
if (!searchTerm.trim()) return 0;
const regex = new RegExp(searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
const matches = value.match(regex);
return matches ? matches.length : 0;
};
const matchCount = getMatchCount();
return (
<div className={`flex flex-col h-full ${className}`}>
<div className="flex items-center justify-between mb-2">
<Label htmlFor="configContent">{label}</Label>
<div className="flex items-center gap-1">
{/* 搜索按钮 */}
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setIsSearchVisible(!isSearchVisible)}
disabled={disabled}
title="搜索内容"
>
<MagnifyingGlassIcon className="h-4 w-4" />
</Button>
</div>
</div>
{/* 搜索栏 */}
{isSearchVisible && (
<div className="flex items-center gap-2 p-2 bg-gray-50 rounded border mb-2">
<div className="relative flex-1">
<MagnifyingGlassIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input
placeholder="搜索内容..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
autoFocus
/>
</div>
{searchTerm && (
<span className="text-sm text-gray-500">
{matchCount}
</span>
)}
<Button
type="button"
variant="outline"
size="sm"
onClick={findPrevious}
disabled={!searchTerm.trim()}
title="上一个"
>
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={findNext}
disabled={!searchTerm.trim()}
title="下一个"
>
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
setSearchTerm('');
setIsSearchVisible(false);
}}
title="关闭搜索"
>
<Cross2Icon className="h-4 w-4" />
</Button>
</div>
)}
{/* 编辑器区域 - 占满剩余空间 */}
<div className="relative flex-1 min-h-0">
<Textarea
ref={textareaRef}
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
required={required}
disabled={disabled}
className="font-mono text-sm h-full resize-none"
style={{ height: '100%' }}
/>
{/* 高亮显示层(仅用于显示,不可编辑) */}
{searchTerm.trim() && (
<div
className="absolute inset-0 pointer-events-none font-mono text-sm p-3 overflow-auto whitespace-pre-wrap"
style={{
backgroundColor: 'transparent',
color: 'transparent',
caretColor: 'transparent',
border: '1px solid transparent',
borderRadius: 'inherit'
}}
dangerouslySetInnerHTML={{ __html: highlightedContent }}
/>
)}
</div>
<p className="text-xs text-gray-500 mt-2">
</p>
</div>
);
}

2
src/X1.WebUI/src/components/ui/dialog.tsx

@ -4,6 +4,8 @@ import { cn } from '@/lib/utils';
export const Dialog = DialogPrimitive.Root;
export const DialogTrigger = DialogPrimitive.Trigger;
export const DialogTitle = DialogPrimitive.Title;
export const DialogDescription = DialogPrimitive.Description;
export const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,

78
src/X1.WebUI/src/components/ui/drawer.tsx

@ -0,0 +1,78 @@
import * as React from "react"
import { Dialog, DialogContent } from "@/components/ui/dialog"
import { cn } from "@/lib/utils"
interface DrawerProps {
open: boolean
onOpenChange: (open: boolean) => void
children: React.ReactNode
className?: string
}
export function Drawer({ open, onOpenChange, children, className }: DrawerProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className={cn(
"fixed right-0 top-0 h-full w-[600px] max-w-[90vw] border-l bg-background p-0 shadow-lg",
"data-[state=open]:animate-in data-[state=closed]:animate-out",
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right",
"duration-300",
"inset-auto left-auto",
className
)}
style={{
position: 'fixed',
right: 0,
top: 0,
height: '100vh',
width: '600px',
maxWidth: '90vw',
transform: open ? 'translateX(0)' : 'translateX(100%)',
transition: 'transform 0.3s ease-in-out',
}}
>
{children}
</DialogContent>
</Dialog>
)
}
interface DrawerHeaderProps {
children: React.ReactNode
className?: string
}
export function DrawerHeader({ children, className }: DrawerHeaderProps) {
return (
<div className={cn("flex h-16 items-center border-b px-6", className)}>
{children}
</div>
)
}
interface DrawerContentProps {
children: React.ReactNode
className?: string
}
export function DrawerContent({ children, className }: DrawerContentProps) {
return (
<div className={cn("flex-1 p-6 overflow-y-auto", className)}>
{children}
</div>
)
}
interface DrawerFooterProps {
children: React.ReactNode
className?: string
}
export function DrawerFooter({ children, className }: DrawerFooterProps) {
return (
<div className={cn("flex items-center justify-end gap-2 border-t p-6", className)}>
{children}
</div>
)
}

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

@ -1,4 +1,4 @@
import { LucideIcon, LayoutDashboard, Users, Settings, FolderOpen, TestTube, BarChart3, Gauge, FileText, ClipboardList } from 'lucide-react';
import { LucideIcon, LayoutDashboard, Users, Settings, FolderOpen, TestTube, BarChart3, Gauge, FileText, ClipboardList, Network } from 'lucide-react';
// 定义权限类型
export type Permission =
@ -192,7 +192,7 @@ export const menuItems: MenuItem[] = [
},
{
title: '网络栈配置',
icon: Gauge,
icon: Network,
href: '/dashboard/network-stack-configs',
permission: 'ranconfigurations.view',
children: [

165
src/X1.WebUI/src/pages/core-network-configs/CoreNetworkConfigDrawer.tsx

@ -0,0 +1,165 @@
import React, { useEffect, useState } from 'react';
import { CoreNetworkConfig, CreateCoreNetworkConfigRequest, UpdateCoreNetworkConfigRequest } from '@/services/coreNetworkConfigService';
import { Drawer, DrawerHeader, DrawerContent, DrawerFooter } from '@/components/ui/drawer';
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 ConfigContentEditor from '@/components/ui/ConfigContentEditor';
import { X } from 'lucide-react';
interface CoreNetworkConfigDrawerProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSubmit: (data: CreateCoreNetworkConfigRequest | UpdateCoreNetworkConfigRequest) => void;
initialData?: Partial<CoreNetworkConfig>;
isEdit?: boolean;
isSubmitting?: boolean;
}
export default function CoreNetworkConfigDrawer({
open,
onOpenChange,
onSubmit,
initialData,
isEdit = false,
isSubmitting = false
}: CoreNetworkConfigDrawerProps) {
const [formData, setFormData] = useState<CreateCoreNetworkConfigRequest>({
name: '',
configContent: '',
description: '',
isDisabled: false
});
// 当初始数据变化时更新表单数据
useEffect(() => {
if (initialData) {
setFormData({
name: initialData.name || '',
configContent: initialData.configContent || '',
description: initialData.description || '',
isDisabled: initialData.isDisabled ?? false
});
} else {
setFormData({
name: '',
configContent: '',
description: '',
isDisabled: false
});
}
}, [initialData]);
const handleInputChange = (field: keyof CreateCoreNetworkConfigRequest, value: string | boolean) => {
setFormData(prev => ({
...prev,
[field]: value
}));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (isSubmitting) return;
if (isEdit && initialData?.coreNetworkConfigId) {
const updateData: UpdateCoreNetworkConfigRequest = {
coreNetworkConfigId: initialData.coreNetworkConfigId,
name: formData.name,
configContent: formData.configContent,
description: formData.description,
isDisabled: formData.isDisabled
};
onSubmit(updateData);
} else {
onSubmit(formData);
}
};
return (
<Drawer open={open} onOpenChange={onOpenChange}>
<div className="flex flex-col h-full">
<DrawerHeader>
<div className="flex items-center justify-between w-full">
<h2 className="text-lg font-semibold">
{isEdit ? '编辑核心网络配置' : '创建核心网络配置'}
</h2>
<Button
variant="ghost"
size="sm"
onClick={() => onOpenChange(false)}
className="h-8 w-8 p-0"
>
<X className="h-4 w-4" />
</Button>
</div>
</DrawerHeader>
<form onSubmit={handleSubmit} className="flex flex-col flex-1">
<DrawerContent className="flex flex-col space-y-4 flex-1 overflow-y-auto">
{/* 配置名称 */}
<div className="space-y-2">
<Label htmlFor="name" className="text-sm font-medium">
<span className="text-red-500">*</span>
</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => handleInputChange('name', e.target.value)}
placeholder="请输入配置名称"
disabled={isSubmitting}
required
/>
</div>
{/* 描述 */}
<div className="space-y-2">
<Label htmlFor="description" className="text-sm font-medium">
</Label>
<Textarea
id="description"
value={formData.description}
onChange={(e) => handleInputChange('description', e.target.value)}
placeholder="请输入配置描述(可选)"
disabled={isSubmitting}
rows={3}
/>
</div>
{/* 配置内容 - 占满剩余空间 */}
<div className="flex flex-col flex-1 min-h-0">
<ConfigContentEditor
value={formData.configContent}
onChange={(value) => handleInputChange('configContent', value)}
placeholder="请输入配置内容"
required
disabled={isSubmitting}
label="配置内容 *"
className="flex-1"
/>
</div>
</DrawerContent>
<DrawerFooter>
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isSubmitting}
>
</Button>
<Button
type="submit"
disabled={isSubmitting}
className="bg-primary text-primary-foreground hover:bg-primary/90"
>
{isSubmitting ? '保存中...' : (isEdit ? '更新' : '创建')}
</Button>
</DrawerFooter>
</form>
</div>
</Drawer>
);
}

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

@ -1,90 +0,0 @@
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>
);
}

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

@ -1,13 +1,14 @@
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';
import StatusSwitch from '@/components/ui/StatusSwitch';
interface CoreNetworkConfigsTableProps {
coreNetworkConfigs: CoreNetworkConfig[];
loading: boolean;
onEdit: (config: CoreNetworkConfig) => void;
onDelete: (config: CoreNetworkConfig) => void;
onStatusChange?: (config: CoreNetworkConfig, newStatus: boolean) => void;
page: number;
pageSize: number;
total: number;
@ -17,12 +18,22 @@ interface CoreNetworkConfigsTableProps {
columns?: { key: string; title: string; visible: boolean }[];
}
// 状态徽章组件
const StatusBadge: React.FC<{ isDisabled: boolean }> = ({ isDisabled }) => {
// Status Switch Component for table cell
const StatusSwitchCell: React.FC<{
isDisabled: boolean;
onStatusChange?: (newStatus: boolean) => void;
disabled?: boolean;
}> = ({ isDisabled, onStatusChange, disabled = false }) => {
return (
<Badge className={!isDisabled ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}>
{!isDisabled ? '启用' : '禁用'}
</Badge>
<div className="flex justify-center">
<StatusSwitch
checked={!isDisabled}
onChange={() => onStatusChange?.(!isDisabled)}
activeText="启用"
inactiveText="禁用"
disabled={disabled}
/>
</div>
);
};
@ -61,6 +72,7 @@ export default function CoreNetworkConfigsTable({
loading,
onEdit,
onDelete,
onStatusChange,
page,
pageSize,
total,
@ -94,7 +106,13 @@ export default function CoreNetworkConfigsTable({
case 'configContent':
return <ConfigContentPreview content={config.configContent} />;
case 'isDisabled':
return <StatusBadge isDisabled={config.isDisabled} />;
return (
<StatusSwitchCell
isDisabled={config.isDisabled}
onStatusChange={(newStatus) => onStatusChange?.(config, newStatus)}
disabled={loading}
/>
);
case 'createdAt':
return <DateDisplay date={config.createdAt} />;
case 'updatedAt':
@ -102,18 +120,28 @@ export default function CoreNetworkConfigsTable({
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>
{[
{
key: 'edit',
className: 'cursor-pointer text-blue-600 hover:underline select-none',
onClick: () => onEdit(config),
text: '修改'
},
{
key: 'delete',
className: 'cursor-pointer text-red-500 hover:underline select-none',
onClick: () => onDelete(config),
text: '删除'
}
].map(item => (
<span
key={`${config.coreNetworkConfigId}-${item.key}`}
className={item.className}
onClick={item.onClick}
>
{item.text}
</span>
))}
</div>
);
default:
@ -130,11 +158,11 @@ export default function CoreNetworkConfigsTable({
return (
<Wrapper {...wrapperProps}>
<Table>
<TableHeader key="header">
<TableHeader key={`header-${Date.now()}`}>
<TableRow className={rowClass}>
{visibleColumns.map(col => (
{visibleColumns.map((col, index) => (
<TableHead
key={col.key}
key={`header-${col.key}-${index}`}
className={`text-foreground text-center ${col.key === 'actions' ? 'text-right' : ''} ${cellPadding}`}
>
{col.title}
@ -142,7 +170,7 @@ export default function CoreNetworkConfigsTable({
))}
</TableRow>
</TableHeader>
<TableBody key="body">
<TableBody key={`body-${Date.now()}`}>
{loading ? (
<TableRow key="loading" className={rowClass}>
<TableCell colSpan={visibleColumns.length} className={`text-center text-muted-foreground ${cellPadding}`}>
@ -156,10 +184,10 @@ export default function CoreNetworkConfigsTable({
</TableCell>
</TableRow>
) : (
coreNetworkConfigs.map((config) => (
<TableRow key={config.coreNetworkConfigId} className={rowClass}>
coreNetworkConfigs.map((config, index) => (
<TableRow key={`${config.coreNetworkConfigId}-${index}`} className={rowClass}>
{visibleColumns.map((column) => (
<TableCell key={column.key} className={`text-foreground text-center ${cellPadding}`}>
<TableCell key={`${config.coreNetworkConfigId}-${column.key}`} className={`text-foreground text-center ${cellPadding}`}>
{renderCell(config, column.key)}
</TableCell>
))}

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

@ -1,11 +1,10 @@
import React, { useEffect, useState } from 'react';
import { coreNetworkConfigService, CoreNetworkConfig, GetCoreNetworkConfigsRequest, CreateCoreNetworkConfigRequest, UpdateCoreNetworkConfigRequest } from '@/services/coreNetworkConfigService';
import CoreNetworkConfigsTable from './CoreNetworkConfigsTable';
import CoreNetworkConfigForm from './CoreNetworkConfigForm';
import CoreNetworkConfigDrawer from './CoreNetworkConfigDrawer';
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';
@ -58,9 +57,9 @@ export default function CoreNetworkConfigsView() {
const [searchTerm, setSearchTerm] = useState('');
const [isDisabled, setIsDisabled] = useState<boolean | undefined>(undefined);
// 表单对话框状态
const [open, setOpen] = useState(false);
const [editOpen, setEditOpen] = useState(false);
// 抽屉状态
const [drawerOpen, setDrawerOpen] = useState(false);
const [editDrawerOpen, setEditDrawerOpen] = useState(false);
const [selectedConfig, setSelectedConfig] = useState<CoreNetworkConfig | null>(null);
// 提交状态
@ -94,7 +93,7 @@ export default function CoreNetworkConfigsView() {
const handleEdit = (config: CoreNetworkConfig) => {
setSelectedConfig(config);
setEditOpen(true);
setEditDrawerOpen(true);
};
const handleDelete = async (config: CoreNetworkConfig) => {
@ -127,6 +126,47 @@ export default function CoreNetworkConfigsView() {
}
};
const handleStatusChange = async (config: CoreNetworkConfig, newStatus: boolean) => {
if (isSubmitting) return;
setIsSubmitting(true);
try {
const updateData: UpdateCoreNetworkConfigRequest = {
coreNetworkConfigId: config.coreNetworkConfigId,
name: config.name,
configContent: config.configContent,
description: config.description,
isDisabled: !newStatus // Note: newStatus is enabled state, isDisabled is disabled state
};
const result = await coreNetworkConfigService.updateCoreNetworkConfig(config.coreNetworkConfigId, updateData);
if (result.isSuccess) {
toast({
title: "状态更新成功",
description: `核心网络配置 "${config.name}" 状态已${newStatus ? '启用' : '禁用'}`,
});
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 handleCreate = async (data: CreateCoreNetworkConfigRequest) => {
if (isSubmitting) return; // 防止重复提交
@ -141,7 +181,7 @@ export default function CoreNetworkConfigsView() {
title: "创建成功",
description: `核心网络配置 "${data.name}" 创建成功`,
});
setOpen(false);
setDrawerOpen(false);
fetchCoreNetworkConfigs();
} else {
const errorMessage = result.errorMessages?.join(', ') || "创建核心网络配置时发生错误";
@ -175,7 +215,7 @@ export default function CoreNetworkConfigsView() {
title: "更新成功",
description: `核心网络配置 "${data.name}" 更新成功`,
});
setEditOpen(false);
setEditDrawerOpen(false);
setSelectedConfig(null);
fetchCoreNetworkConfigs();
} else {
@ -298,14 +338,12 @@ export default function CoreNetworkConfigsView() {
<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>
<Button
className="bg-primary text-primary-foreground hover:bg-primary/90"
onClick={() => setDrawerOpen(true)}
>
+
</Button>
<TableToolbar
onRefresh={() => fetchCoreNetworkConfigs()}
onDensityChange={setDensity}
@ -321,6 +359,7 @@ export default function CoreNetworkConfigsView() {
loading={loading}
onEdit={handleEdit}
onDelete={handleDelete}
onStatusChange={handleStatusChange}
page={pageNumber}
pageSize={pageSize}
total={total}
@ -340,17 +379,23 @@ export default function CoreNetworkConfigsView() {
</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>
{/* 创建核心网络配置抽屉 */}
<CoreNetworkConfigDrawer
open={drawerOpen}
onOpenChange={setDrawerOpen}
onSubmit={handleCreate}
isSubmitting={isSubmitting}
/>
{/* 编辑核心网络配置抽屉 */}
<CoreNetworkConfigDrawer
open={editDrawerOpen}
onOpenChange={setEditDrawerOpen}
onSubmit={(data: CreateCoreNetworkConfigRequest | UpdateCoreNetworkConfigRequest) => handleUpdate(data as UpdateCoreNetworkConfigRequest)}
initialData={selectedConfig || undefined}
isEdit={true}
isSubmitting={isSubmitting}
/>
</main>
);
}

165
src/X1.WebUI/src/pages/ims-configurations/IMSConfigurationDrawer.tsx

@ -0,0 +1,165 @@
import React, { useEffect, useState } from 'react';
import { IMSConfiguration, CreateIMSConfigurationRequest, UpdateIMSConfigurationRequest } from '@/services/imsConfigurationService';
import { Drawer, DrawerHeader, DrawerContent, DrawerFooter } from '@/components/ui/drawer';
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 ConfigContentEditor from '@/components/ui/ConfigContentEditor';
import { X } from 'lucide-react';
interface IMSConfigurationDrawerProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSubmit: (data: CreateIMSConfigurationRequest | UpdateIMSConfigurationRequest) => void;
initialData?: Partial<IMSConfiguration>;
isEdit?: boolean;
isSubmitting?: boolean;
}
export default function IMSConfigurationDrawer({
open,
onOpenChange,
onSubmit,
initialData,
isEdit = false,
isSubmitting = false
}: IMSConfigurationDrawerProps) {
const [formData, setFormData] = useState<CreateIMSConfigurationRequest>({
name: '',
configContent: '',
description: '',
isDisabled: false
});
// 当初始数据变化时更新表单数据
useEffect(() => {
if (initialData) {
setFormData({
name: initialData.name || '',
configContent: initialData.configContent || '',
description: initialData.description || '',
isDisabled: initialData.isDisabled ?? false
});
} else {
setFormData({
name: '',
configContent: '',
description: '',
isDisabled: false
});
}
}, [initialData]);
const handleInputChange = (field: keyof CreateIMSConfigurationRequest, value: string | boolean) => {
setFormData(prev => ({
...prev,
[field]: value
}));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (isSubmitting) return;
if (isEdit && initialData?.imS_ConfigurationId) {
const updateData: UpdateIMSConfigurationRequest = {
imS_ConfigurationId: initialData.imS_ConfigurationId,
name: formData.name,
configContent: formData.configContent,
description: formData.description,
isDisabled: formData.isDisabled
};
onSubmit(updateData);
} else {
onSubmit(formData);
}
};
return (
<Drawer open={open} onOpenChange={onOpenChange}>
<div className="flex flex-col h-full">
<DrawerHeader>
<div className="flex items-center justify-between w-full">
<h2 className="text-lg font-semibold">
{isEdit ? '编辑IMS配置' : '创建IMS配置'}
</h2>
<Button
variant="ghost"
size="sm"
onClick={() => onOpenChange(false)}
className="h-8 w-8 p-0"
>
<X className="h-4 w-4" />
</Button>
</div>
</DrawerHeader>
<form onSubmit={handleSubmit} className="flex flex-col flex-1">
<DrawerContent className="flex flex-col space-y-4 flex-1 overflow-y-auto">
{/* 配置名称 */}
<div className="space-y-2">
<Label htmlFor="name" className="text-sm font-medium">
<span className="text-red-500">*</span>
</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => handleInputChange('name', e.target.value)}
placeholder="请输入配置名称"
disabled={isSubmitting}
required
/>
</div>
{/* 描述 */}
<div className="space-y-2">
<Label htmlFor="description" className="text-sm font-medium">
</Label>
<Textarea
id="description"
value={formData.description}
onChange={(e) => handleInputChange('description', e.target.value)}
placeholder="请输入配置描述(可选)"
disabled={isSubmitting}
rows={3}
/>
</div>
{/* 配置内容 - 占满剩余空间 */}
<div className="flex flex-col flex-1 min-h-0">
<ConfigContentEditor
value={formData.configContent}
onChange={(value) => handleInputChange('configContent', value)}
placeholder="请输入配置内容"
required
disabled={isSubmitting}
label="配置内容 *"
className="flex-1"
/>
</div>
</DrawerContent>
<DrawerFooter>
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isSubmitting}
>
</Button>
<Button
type="submit"
disabled={isSubmitting}
className="bg-primary text-primary-foreground hover:bg-primary/90"
>
{isSubmitting ? '保存中...' : (isEdit ? '更新' : '创建')}
</Button>
</DrawerFooter>
</form>
</div>
</Drawer>
);
}

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

@ -1,90 +0,0 @@
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>
);
}

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

@ -1,13 +1,14 @@
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';
import StatusSwitch from '@/components/ui/StatusSwitch';
interface IMSConfigurationsTableProps {
imsConfigurations: IMSConfiguration[];
loading: boolean;
onEdit: (configuration: IMSConfiguration) => void;
onDelete: (configuration: IMSConfiguration) => void;
onStatusChange?: (configuration: IMSConfiguration, newStatus: boolean) => void;
page: number;
pageSize: number;
total: number;
@ -17,12 +18,22 @@ interface IMSConfigurationsTableProps {
columns?: { key: string; title: string; visible: boolean }[];
}
// 状态徽章组件
const StatusBadge: React.FC<{ isDisabled: boolean }> = ({ isDisabled }) => {
// Status Switch Component for table cell
const StatusSwitchCell: React.FC<{
isDisabled: boolean;
onStatusChange?: (newStatus: boolean) => void;
disabled?: boolean;
}> = ({ isDisabled, onStatusChange, disabled = false }) => {
return (
<Badge className={!isDisabled ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}>
{!isDisabled ? '启用' : '禁用'}
</Badge>
<div className="flex justify-center">
<StatusSwitch
checked={!isDisabled}
onChange={() => onStatusChange?.(!isDisabled)}
activeText="启用"
inactiveText="禁用"
disabled={disabled}
/>
</div>
);
};
@ -61,6 +72,7 @@ export default function IMSConfigurationsTable({
loading,
onEdit,
onDelete,
onStatusChange,
page,
pageSize,
total,
@ -94,7 +106,13 @@ export default function IMSConfigurationsTable({
case 'configContent':
return <ConfigContentPreview content={configuration.configContent} />;
case 'isDisabled':
return <StatusBadge isDisabled={configuration.isDisabled} />;
return (
<StatusSwitchCell
isDisabled={configuration.isDisabled}
onStatusChange={(newStatus) => onStatusChange?.(configuration, newStatus)}
disabled={loading}
/>
);
case 'createdAt':
return <DateDisplay date={configuration.createdAt} />;
case 'updatedAt':
@ -102,18 +120,28 @@ export default function IMSConfigurationsTable({
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>
{[
{
key: 'edit',
className: 'cursor-pointer text-blue-600 hover:underline select-none',
onClick: () => onEdit(configuration),
text: '修改'
},
{
key: 'delete',
className: 'cursor-pointer text-red-500 hover:underline select-none',
onClick: () => onDelete(configuration),
text: '删除'
}
].map(item => (
<span
key={`${configuration.imS_ConfigurationId}-${item.key}`}
className={item.className}
onClick={item.onClick}
>
{item.text}
</span>
))}
</div>
);
default:
@ -130,11 +158,11 @@ export default function IMSConfigurationsTable({
return (
<Wrapper {...wrapperProps}>
<Table>
<TableHeader key="header">
<TableHeader key={`header-${Date.now()}`}>
<TableRow className={rowClass}>
{visibleColumns.map(col => (
{visibleColumns.map((col, index) => (
<TableHead
key={col.key}
key={`header-${col.key}-${index}`}
className={`text-foreground text-center ${col.key === 'actions' ? 'text-right' : ''} ${cellPadding}`}
>
{col.title}
@ -142,7 +170,7 @@ export default function IMSConfigurationsTable({
))}
</TableRow>
</TableHeader>
<TableBody key="body">
<TableBody key={`body-${Date.now()}`}>
{loading ? (
<TableRow key="loading" className={rowClass}>
<TableCell colSpan={visibleColumns.length} className={`text-center text-muted-foreground ${cellPadding}`}>
@ -156,10 +184,10 @@ export default function IMSConfigurationsTable({
</TableCell>
</TableRow>
) : (
imsConfigurations.map((configuration) => (
<TableRow key={configuration.imsConfigurationId} className={rowClass}>
imsConfigurations.map((configuration, index) => (
<TableRow key={`${configuration.imS_ConfigurationId}-${index}`} className={rowClass}>
{visibleColumns.map((column) => (
<TableCell key={column.key} className={`text-foreground text-center ${cellPadding}`}>
<TableCell key={`${configuration.imS_ConfigurationId}-${column.key}`} className={`text-foreground text-center ${cellPadding}`}>
{renderCell(configuration, column.key)}
</TableCell>
))}

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

@ -1,11 +1,10 @@
import React, { useEffect, useState } from 'react';
import { imsConfigurationService, IMSConfiguration, GetIMSConfigurationsRequest, CreateIMSConfigurationRequest, UpdateIMSConfigurationRequest } from '@/services/imsConfigurationService';
import IMSConfigurationsTable from './IMSConfigurationsTable';
import IMSConfigurationForm from './IMSConfigurationForm';
import IMSConfigurationDrawer from './IMSConfigurationDrawer';
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';
@ -58,9 +57,9 @@ export default function IMSConfigurationsView() {
const [searchTerm, setSearchTerm] = useState('');
const [isDisabled, setIsDisabled] = useState<boolean | undefined>(undefined);
// 表单对话框状态
const [open, setOpen] = useState(false);
const [editOpen, setEditOpen] = useState(false);
// 抽屉状态
const [drawerOpen, setDrawerOpen] = useState(false);
const [editDrawerOpen, setEditDrawerOpen] = useState(false);
const [selectedConfiguration, setSelectedConfiguration] = useState<IMSConfiguration | null>(null);
// 提交状态
@ -94,13 +93,13 @@ export default function IMSConfigurationsView() {
const handleEdit = (configuration: IMSConfiguration) => {
setSelectedConfiguration(configuration);
setEditOpen(true);
setEditDrawerOpen(true);
};
const handleDelete = async (configuration: IMSConfiguration) => {
if (confirm(`确定要删除IMS配置 "${configuration.name}" 吗?`)) {
try {
const result = await imsConfigurationService.deleteIMSConfiguration(configuration.imsConfigurationId);
const result = await imsConfigurationService.deleteIMSConfiguration(configuration.imS_ConfigurationId);
if (result.isSuccess) {
toast({
title: "删除成功",
@ -141,7 +140,7 @@ export default function IMSConfigurationsView() {
title: "创建成功",
description: `IMS配置 "${data.name}" 创建成功`,
});
setOpen(false);
setDrawerOpen(false);
fetchIMSConfigurations();
} else {
const errorMessage = result.errorMessages?.join(', ') || "创建IMS配置时发生错误";
@ -164,18 +163,59 @@ export default function IMSConfigurationsView() {
}
};
const handleStatusChange = async (configuration: IMSConfiguration, newStatus: boolean) => {
if (isSubmitting) return;
setIsSubmitting(true);
try {
const updateData: UpdateIMSConfigurationRequest = {
imS_ConfigurationId: configuration.imS_ConfigurationId,
name: configuration.name,
configContent: configuration.configContent,
description: configuration.description,
isDisabled: !newStatus // Note: newStatus is enabled state, isDisabled is disabled state
};
const result = await imsConfigurationService.updateIMSConfiguration(configuration.imS_ConfigurationId, updateData);
if (result.isSuccess) {
toast({
title: "状态更新成功",
description: `IMS配置 "${configuration.name}" 状态已${newStatus ? '启用' : '禁用'}`,
});
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 handleUpdate = async (data: UpdateIMSConfigurationRequest) => {
if (!selectedConfiguration || isSubmitting) return; // 防止重复提交
setIsSubmitting(true);
try {
const result = await imsConfigurationService.updateIMSConfiguration(selectedConfiguration.imsConfigurationId, data);
const result = await imsConfigurationService.updateIMSConfiguration(selectedConfiguration.imS_ConfigurationId, data);
if (result.isSuccess) {
toast({
title: "更新成功",
description: `IMS配置 "${data.name}" 更新成功`,
});
setEditOpen(false);
setEditDrawerOpen(false);
setSelectedConfiguration(null);
fetchIMSConfigurations();
} else {
@ -298,14 +338,12 @@ export default function IMSConfigurationsView() {
<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>
<Button
className="bg-primary text-primary-foreground hover:bg-primary/90"
onClick={() => setDrawerOpen(true)}
>
+ IMS配置
</Button>
<TableToolbar
onRefresh={() => fetchIMSConfigurations()}
onDensityChange={setDensity}
@ -321,6 +359,7 @@ export default function IMSConfigurationsView() {
loading={loading}
onEdit={handleEdit}
onDelete={handleDelete}
onStatusChange={handleStatusChange}
page={pageNumber}
pageSize={pageSize}
total={total}
@ -340,17 +379,23 @@ export default function IMSConfigurationsView() {
</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>
{/* 创建IMS配置抽屉 */}
<IMSConfigurationDrawer
open={drawerOpen}
onOpenChange={setDrawerOpen}
onSubmit={handleCreate}
isSubmitting={isSubmitting}
/>
{/* 编辑IMS配置抽屉 */}
<IMSConfigurationDrawer
open={editDrawerOpen}
onOpenChange={setEditDrawerOpen}
onSubmit={(data: CreateIMSConfigurationRequest | UpdateIMSConfigurationRequest) => handleUpdate(data as UpdateIMSConfigurationRequest)}
initialData={selectedConfiguration || undefined}
isEdit={true}
isSubmitting={isSubmitting}
/>
</main>
);
}

74
src/X1.WebUI/src/pages/instruments/DeviceForm.tsx

@ -5,7 +5,6 @@ import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Checkbox } from '@/components/ui/checkbox';
import { CreateDeviceRequest, UpdateDeviceRequest } from '@/services/instrumentService';
import { protocolService, ProtocolVersion } from '@/services/protocolService';
interface DeviceFormProps {
onSubmit: (data: CreateDeviceRequest | UpdateDeviceRequest) => void;
@ -19,45 +18,12 @@ export default function DeviceForm({ onSubmit, initialData, isEdit = false, isSu
deviceName: initialData?.deviceName || '',
serialNumber: initialData?.serialNumber || '',
description: initialData?.description || '',
protocolVersionId: initialData?.protocolVersionId || '',
ipAddress: initialData?.ipAddress || '',
agentPort: initialData?.agentPort || 8080,
isEnabled: initialData?.isEnabled ?? true,
isRunning: initialData?.isRunning ?? false
});
const [protocolVersions, setProtocolVersions] = useState<ProtocolVersion[]>([]);
const [loadingProtocols, setLoadingProtocols] = useState(false);
// 加载协议版本列表
useEffect(() => {
const loadProtocolVersions = async () => {
setLoadingProtocols(true);
try {
const result = await protocolService.getProtocolVersions({
pageSize: 100,
isEnabled: true
});
if (result.isSuccess && result.data) {
setProtocolVersions(result.data.items || []);
// 如果是编辑模式且有初始数据,尝试根据协议版本名称找到对应的ID
if (isEdit && initialData && !initialData.protocolVersionId) {
// 这里需要根据实际情况来匹配协议版本
// 由于后端返回的是协议版本名称,我们需要找到对应的ID
// 暂时设置为空,需要后端提供更详细的设备信息
}
}
} catch (error) {
console.error('加载协议版本失败:', error);
} finally {
setLoadingProtocols(false);
}
};
loadProtocolVersions();
}, [isEdit, initialData]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (isSubmitting) return; // 防止重复提交
@ -102,26 +68,6 @@ export default function DeviceForm({ onSubmit, initialData, isEdit = false, isSu
/>
</div>
<div className="space-y-2">
<Label htmlFor="protocolVersionId"></Label>
<select
id="protocolVersionId"
className="input h-10 rounded border border-border bg-background px-3 text-sm w-full"
value={formData.protocolVersionId}
onChange={e => setFormData({ ...formData, protocolVersionId: e.target.value })}
required
disabled={isSubmitting || loadingProtocols}
>
<option value=""></option>
{protocolVersions.map(protocol => (
<option key={protocol.protocolVersionId} value={protocol.protocolVersionId}>
{protocol.name} - {protocol.version}
</option>
))}
</select>
{loadingProtocols && <div className="text-sm text-muted-foreground">...</div>}
</div>
<div className="space-y-2">
<Label htmlFor="ipAddress">IP地址</Label>
<Input
@ -150,7 +96,25 @@ export default function DeviceForm({ onSubmit, initialData, isEdit = false, isSu
/>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="isEnabled"
checked={formData.isEnabled}
onCheckedChange={(checked) => setFormData({ ...formData, isEnabled: checked as boolean })}
disabled={isSubmitting}
/>
<Label htmlFor="isEnabled"></Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="isRunning"
checked={formData.isRunning}
onCheckedChange={(checked) => setFormData({ ...formData, isRunning: checked as boolean })}
disabled={isSubmitting}
/>
<Label htmlFor="isRunning"></Label>
</div>
<Button type="submit" className="w-full" disabled={isSubmitting}>
{isSubmitting ? '提交中...' : (isEdit ? '更新设备' : '创建设备')}

59
src/X1.WebUI/src/pages/instruments/DevicesTable.tsx

@ -14,7 +14,6 @@ import { DensityType } from '@/components/ui/TableToolbar';
* - deviceName: 设备名称
* - serialNumber: 序列号
* - description: 设备描述
* - protocolVersion: 协议版本
* - agentPort: Agent端口
* - isEnabled: 是否启用
* - isRunning: 设备状态/
@ -38,40 +37,31 @@ interface DevicesTableProps {
}
// 设备状态徽章组件
const DeviceStatusBadge: React.FC<{ isEnabled: boolean }> = ({ isEnabled }) => {
function DeviceStatusBadge({ isEnabled }: { isEnabled: boolean }) {
return (
<Badge className={isEnabled ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}>
<Badge variant={isEnabled ? 'default' : 'secondary'}>
{isEnabled ? '启用' : '禁用'}
</Badge>
);
};
}
// 设备运行状态徽章组件
const DeviceRunningStatusBadge: React.FC<{ isRunning: boolean }> = ({ isRunning }) => {
function DeviceRunningStatusBadge({ isRunning }: { isRunning: boolean }) {
return (
<Badge className={isRunning ? 'bg-blue-100 text-blue-800' : 'bg-orange-100 text-orange-800'}>
<Badge variant={isRunning ? 'default' : 'secondary'}>
{isRunning ? '运行中' : '已停止'}
</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>;
};
// 日期显示组件
function DateDisplay({ date }: { date: string }) {
return (
<span className="text-sm text-gray-600">
{new Date(date).toLocaleString('zh-CN')}
</span>
);
}
export default function DevicesTable({
devices,
@ -116,8 +106,6 @@ export default function DevicesTable({
{device.description || '-'}
</div>
);
case 'protocolVersion':
return <span className="text-sm">{device.protocolVersion}</span>;
case 'agentPort':
return <span className="font-mono text-sm">{device.agentPort}</span>;
case 'isEnabled':
@ -128,7 +116,7 @@ export default function DevicesTable({
return <DateDisplay date={device.createdAt} />;
case 'actions':
return (
<div className="flex justify-end gap-2">
<div className="flex justify-end space-x-2">
<Button
variant="ghost"
size="sm"
@ -141,7 +129,7 @@ export default function DevicesTable({
variant="ghost"
size="sm"
onClick={() => onDelete(device)}
className="h-8 w-8 p-0"
className="h-8 w-8 p-0 text-red-600 hover:text-red-700"
>
<TrashIcon className="h-4 w-4" />
</Button>
@ -152,11 +140,16 @@ export default function DevicesTable({
}
};
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';
const cellPadding = densityClasses[density];
const rowClass = `hover:bg-muted/50 ${cellPadding}`;
const Wrapper = hideCard ? React.Fragment : ({ children }: { children: React.ReactNode }) => (
<div className="rounded-lg border bg-card text-card-foreground shadow-sm">
{children}
</div>
);
const wrapperProps = hideCard ? {} : {};
return (
<Wrapper {...wrapperProps}>

4
src/X1.WebUI/src/pages/instruments/DevicesView.tsx

@ -15,7 +15,6 @@ const defaultColumns = [
{ key: 'deviceName', title: '设备名称', visible: true },
{ key: 'serialNumber', title: '序列号', visible: true },
{ key: 'description', title: '描述', visible: true },
{ key: 'protocolVersion', title: '协议版本', visible: true },
{ key: 'agentPort', title: 'Agent端口', visible: true },
{ key: 'isEnabled', title: '状态', visible: true },
{ key: 'isRunning', title: '运行状态', visible: true },
@ -56,7 +55,6 @@ const advancedFields: SearchField[] = [
* - deviceName: 设备名称
* - serialNumber: 序列号
* - description: 设备描述
* - protocolVersion: 协议版本
* - agentPort: Agent端口
* - isEnabled: 是否启用
* - isRunning: 设备状态/
@ -390,8 +388,6 @@ export default function DevicesView() {
deviceName: selectedDevice.deviceName,
serialNumber: selectedDevice.serialNumber,
description: selectedDevice.description,
protocolVersionId: '', // 需要从后端获取协议版本ID
ipAddress: '', // 需要从后端获取IP地址
agentPort: selectedDevice.agentPort,
isEnabled: selectedDevice.isEnabled,
isRunning: selectedDevice.isRunning

170
src/X1.WebUI/src/pages/ran-configurations/RANConfigurationDrawer.tsx

@ -0,0 +1,170 @@
import React, { useEffect, useState } from 'react';
import { RANConfiguration, CreateRANConfigurationRequest, UpdateRANConfigurationRequest } from '@/services/ranConfigurationService';
import { Drawer, DrawerHeader, DrawerContent, DrawerFooter } from '@/components/ui/drawer';
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 ConfigContentEditor from '@/components/ui/ConfigContentEditor';
import { X } from 'lucide-react';
interface RANConfigurationDrawerProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSubmit: (data: CreateRANConfigurationRequest | UpdateRANConfigurationRequest) => Promise<void>;
initialData?: RANConfiguration;
isEdit?: boolean;
isSubmitting?: boolean;
}
export default function RANConfigurationDrawer({
open,
onOpenChange,
onSubmit,
initialData,
isEdit = false,
isSubmitting = false
}: RANConfigurationDrawerProps) {
const [formData, setFormData] = useState<CreateRANConfigurationRequest>({
name: '',
configContent: '',
description: '',
isDisabled: false
});
// 当抽屉打开时,初始化表单数据
useEffect(() => {
if (open) {
if (initialData && isEdit) {
setFormData({
name: initialData.name || '',
configContent: initialData.configContent || '',
description: initialData.description || '',
isDisabled: initialData.isDisabled || false
});
} else {
// 重置表单
setFormData({
name: '',
configContent: '',
description: '',
isDisabled: false
});
}
}
}, [open, initialData, isEdit]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (isSubmitting) return;
// 基本验证
if (!formData.name.trim()) {
alert('请输入配置名称');
return;
}
if (!formData.configContent.trim()) {
alert('请输入配置内容');
return;
}
await onSubmit(formData);
};
const handleInputChange = (field: keyof CreateRANConfigurationRequest, value: string | boolean) => {
setFormData(prev => ({
...prev,
[field]: value
}));
};
return (
<Drawer open={open} onOpenChange={onOpenChange}>
<div className="flex flex-col h-full">
<DrawerHeader>
<div className="flex items-center justify-between w-full">
<h2 className="text-lg font-semibold">
{isEdit ? '编辑RAN配置' : '创建RAN配置'}
</h2>
<Button
variant="ghost"
size="sm"
onClick={() => onOpenChange(false)}
className="h-8 w-8 p-0"
>
<X className="h-4 w-4" />
</Button>
</div>
</DrawerHeader>
<form onSubmit={handleSubmit} className="flex flex-col flex-1">
<DrawerContent className="flex flex-col space-y-4 flex-1 overflow-y-auto">
{/* 配置名称 */}
<div className="space-y-2">
<Label htmlFor="name" className="text-sm font-medium">
<span className="text-red-500">*</span>
</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => handleInputChange('name', e.target.value)}
placeholder="请输入配置名称"
disabled={isSubmitting}
required
/>
</div>
{/* 描述 */}
<div className="space-y-2">
<Label htmlFor="description" className="text-sm font-medium">
</Label>
<Textarea
id="description"
value={formData.description}
onChange={(e) => handleInputChange('description', e.target.value)}
placeholder="请输入配置描述(可选)"
disabled={isSubmitting}
rows={3}
/>
</div>
{/* 配置内容 - 占满剩余空间 */}
<div className="flex flex-col flex-1 min-h-0">
<ConfigContentEditor
value={formData.configContent}
onChange={(value) => handleInputChange('configContent', value)}
placeholder="请输入配置内容"
required
disabled={isSubmitting}
label="配置内容 *"
className="flex-1"
/>
</div>
</DrawerContent>
<DrawerFooter>
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isSubmitting}
>
</Button>
<Button
type="submit"
disabled={isSubmitting}
className="bg-primary text-primary-foreground hover:bg-primary/90"
>
{isSubmitting ? '保存中...' : (isEdit ? '更新' : '创建')}
</Button>
</DrawerFooter>
</form>
</div>
</Drawer>
);
}

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

@ -1,90 +0,0 @@
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>
);
}

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

@ -1,13 +1,14 @@
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';
import StatusSwitch from '@/components/ui/StatusSwitch';
interface RANConfigurationsTableProps {
ranConfigurations: RANConfiguration[];
loading: boolean;
onEdit: (configuration: RANConfiguration) => void;
onDelete: (configuration: RANConfiguration) => void;
onStatusChange?: (configuration: RANConfiguration, newStatus: boolean) => void;
page: number;
pageSize: number;
total: number;
@ -17,12 +18,22 @@ interface RANConfigurationsTableProps {
columns?: { key: string; title: string; visible: boolean }[];
}
// 状态徽章组件
const StatusBadge: React.FC<{ isDisabled: boolean }> = ({ isDisabled }) => {
// 状态开关组件
const StatusSwitchCell: React.FC<{
isDisabled: boolean;
onStatusChange?: (newStatus: boolean) => void;
disabled?: boolean;
}> = ({ isDisabled, onStatusChange, disabled = false }) => {
return (
<Badge className={!isDisabled ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}>
{!isDisabled ? '启用' : '禁用'}
</Badge>
<div className="flex justify-center">
<StatusSwitch
checked={!isDisabled}
onChange={() => onStatusChange?.(!isDisabled)}
activeText="启用"
inactiveText="禁用"
disabled={disabled}
/>
</div>
);
};
@ -61,6 +72,7 @@ export default function RANConfigurationsTable({
loading,
onEdit,
onDelete,
onStatusChange,
page,
pageSize,
total,
@ -94,7 +106,13 @@ export default function RANConfigurationsTable({
case 'configContent':
return <ConfigContentPreview content={configuration.configContent} />;
case 'isDisabled':
return <StatusBadge isDisabled={configuration.isDisabled} />;
return (
<StatusSwitchCell
isDisabled={configuration.isDisabled}
onStatusChange={(newStatus) => onStatusChange?.(configuration, newStatus)}
disabled={loading}
/>
);
case 'createdAt':
return <DateDisplay date={configuration.createdAt} />;
case 'updatedAt':
@ -102,18 +120,28 @@ export default function RANConfigurationsTable({
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>
{[
{
key: 'edit',
className: 'cursor-pointer text-blue-600 hover:underline select-none',
onClick: () => onEdit(configuration),
text: '修改'
},
{
key: 'delete',
className: 'cursor-pointer text-red-500 hover:underline select-none',
onClick: () => onDelete(configuration),
text: '删除'
}
].map(item => (
<span
key={`${configuration.raN_ConfigurationId}-${item.key}`}
className={item.className}
onClick={item.onClick}
>
{item.text}
</span>
))}
</div>
);
default:
@ -130,11 +158,11 @@ export default function RANConfigurationsTable({
return (
<Wrapper {...wrapperProps}>
<Table>
<TableHeader key="header">
<TableHeader key={`header-${Date.now()}`}>
<TableRow className={rowClass}>
{visibleColumns.map(col => (
{visibleColumns.map((col, index) => (
<TableHead
key={col.key}
key={`header-${col.key}-${index}`}
className={`text-foreground text-center ${col.key === 'actions' ? 'text-right' : ''} ${cellPadding}`}
>
{col.title}
@ -142,7 +170,7 @@ export default function RANConfigurationsTable({
))}
</TableRow>
</TableHeader>
<TableBody key="body">
<TableBody key={`body-${Date.now()}`}>
{loading ? (
<TableRow key="loading" className={rowClass}>
<TableCell colSpan={visibleColumns.length} className={`text-center text-muted-foreground ${cellPadding}`}>
@ -156,10 +184,10 @@ export default function RANConfigurationsTable({
</TableCell>
</TableRow>
) : (
ranConfigurations.map((configuration) => (
<TableRow key={configuration.ranConfigurationId} className={rowClass}>
ranConfigurations.map((configuration, index) => (
<TableRow key={`${configuration.raN_ConfigurationId}-${index}`} className={rowClass}>
{visibleColumns.map((column) => (
<TableCell key={column.key} className={`text-foreground text-center ${cellPadding}`}>
<TableCell key={`${configuration.raN_ConfigurationId}-${column.key}`} className={`text-foreground text-center ${cellPadding}`}>
{renderCell(configuration, column.key)}
</TableCell>
))}

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

@ -1,13 +1,12 @@
import React, { useEffect, useState } from 'react';
import { ranConfigurationService, RANConfiguration, GetRANConfigurationsRequest, CreateRANConfigurationRequest, UpdateRANConfigurationRequest } from '@/services/ranConfigurationService';
import RANConfigurationsTable from './RANConfigurationsTable';
import RANConfigurationForm from './RANConfigurationForm';
import RANConfigurationDrawer from './RANConfigurationDrawer';
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 { Dialog, DialogContent, DialogTitle } 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 = [
@ -24,8 +23,8 @@ 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[] = [
// 搜索字段
const searchFields: SearchField[] = [
{ key: 'searchTerm', label: '搜索关键词', type: 'input', placeholder: '请输入配置名称或描述' },
{ key: 'isDisabled', label: '状态', type: 'select', options: [
{ value: '', label: '请选择' },
@ -34,16 +33,6 @@ const firstRowFields: SearchField[] = [
] },
];
// 高级字段(展开时才显示)
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);
@ -52,7 +41,6 @@ export default function RANConfigurationsView() {
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('');
@ -100,7 +88,7 @@ export default function RANConfigurationsView() {
const handleDelete = async (configuration: RANConfiguration) => {
if (confirm(`确定要删除RAN配置 "${configuration.name}" 吗?`)) {
try {
const result = await ranConfigurationService.deleteRANConfiguration(configuration.ranConfigurationId);
const result = await ranConfigurationService.deleteRANConfiguration(configuration.raN_ConfigurationId);
if (result.isSuccess) {
toast({
title: "删除成功",
@ -169,7 +157,7 @@ export default function RANConfigurationsView() {
setIsSubmitting(true);
try {
const result = await ranConfigurationService.updateRANConfiguration(selectedConfiguration.ranConfigurationId, data);
const result = await ranConfigurationService.updateRANConfiguration(selectedConfiguration.raN_ConfigurationId, data);
if (result.isSuccess) {
toast({
title: "更新成功",
@ -199,6 +187,47 @@ export default function RANConfigurationsView() {
}
};
const handleStatusChange = async (configuration: RANConfiguration, newStatus: boolean) => {
if (isSubmitting) return; // 防止重复提交
setIsSubmitting(true);
try {
const updateData: UpdateRANConfigurationRequest = {
raN_ConfigurationId: configuration.raN_ConfigurationId,
name: configuration.name,
configContent: configuration.configContent,
description: configuration.description,
isDisabled: !newStatus // 注意:newStatus是启用状态,isDisabled是禁用状态
};
const result = await ranConfigurationService.updateRANConfiguration(configuration.raN_ConfigurationId, updateData);
if (result.isSuccess) {
toast({
title: "状态更新成功",
description: `RAN配置 "${configuration.name}" 状态已${newStatus ? '启用' : '禁用'}`,
});
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);
@ -228,16 +257,16 @@ export default function RANConfigurationsView() {
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`}
className="grid gap-x-8 gap-y-4 items-center md:grid-cols-3 grid-cols-1"
onSubmit={e => {
e.preventDefault();
handleQuery();
}}
>
{(showAdvanced ? [...firstRowFields, ...advancedFields] : firstRowFields).map(field => (
{searchFields.map((field: SearchField) => (
<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"
@ -258,39 +287,25 @@ export default function RANConfigurationsView() {
{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() : ''}
value={field.key === 'isDisabled' ? (isDisabled === undefined ? '' : isDisabled.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 => (
{field.options.map((opt: { value: string; label: string }) => (
<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>
@ -298,14 +313,12 @@ export default function RANConfigurationsView() {
<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>
<Button
className="bg-primary text-primary-foreground hover:bg-primary/90"
onClick={() => setOpen(true)}
>
+ RAN配置
</Button>
<TableToolbar
onRefresh={() => fetchRANConfigurations()}
onDensityChange={setDensity}
@ -321,6 +334,7 @@ export default function RANConfigurationsView() {
loading={loading}
onEdit={handleEdit}
onDelete={handleDelete}
onStatusChange={handleStatusChange}
page={pageNumber}
pageSize={pageSize}
total={total}
@ -340,17 +354,23 @@ export default function RANConfigurationsView() {
</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>
{/* 创建RAN配置抽屉 */}
<RANConfigurationDrawer
open={open}
onOpenChange={setOpen}
onSubmit={handleCreate}
isSubmitting={isSubmitting}
/>
{/* 编辑RAN配置抽屉 */}
<RANConfigurationDrawer
open={editOpen}
onOpenChange={setEditOpen}
onSubmit={(data: CreateRANConfigurationRequest | UpdateRANConfigurationRequest) => handleUpdate(data as UpdateRANConfigurationRequest)}
initialData={selectedConfiguration || undefined}
isEdit={true}
isSubmitting={isSubmitting}
/>
</main>
);
}

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

@ -4,7 +4,7 @@ import { API_PATHS } from '@/constants/api';
// IMS配置接口定义
export interface IMSConfiguration {
imsConfigurationId: string;
imS_ConfigurationId: string;
name: string;
configContent: string;
description?: string;
@ -44,7 +44,7 @@ export interface CreateIMSConfigurationRequest {
// 创建IMS配置响应接口
export interface CreateIMSConfigurationResponse {
imsConfigurationId: string;
imS_ConfigurationId: string;
name: string;
configContent: string;
description?: string;
@ -54,7 +54,7 @@ export interface CreateIMSConfigurationResponse {
// 更新IMS配置请求接口
export interface UpdateIMSConfigurationRequest {
imsConfigurationId: string;
imS_ConfigurationId: string;
name: string;
configContent: string;
description?: string;
@ -63,7 +63,7 @@ export interface UpdateIMSConfigurationRequest {
// 更新IMS配置响应接口
export interface UpdateIMSConfigurationResponse {
imsConfigurationId: string;
imS_ConfigurationId: string;
name: string;
configContent: string;
description?: string;

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

@ -14,7 +14,6 @@ export interface Device {
deviceName: string;
serialNumber: string;
description: string;
protocolVersion: string;
agentPort: number;
isEnabled: boolean;
isRunning: boolean;
@ -45,7 +44,6 @@ export interface CreateDeviceRequest {
deviceName: string;
serialNumber: string;
description?: string;
protocolVersionId: string;
ipAddress: string;
agentPort: number;
isEnabled?: boolean;
@ -58,7 +56,6 @@ export interface CreateDeviceResponse {
deviceName: string;
serialNumber: string;
description: string;
protocolVersion: string;
agentPort: number;
isEnabled: boolean;
isRunning: boolean;
@ -71,7 +68,6 @@ export interface UpdateDeviceRequest {
deviceName: string;
serialNumber: string;
description?: string;
protocolVersionId: string;
ipAddress: string;
agentPort: number;
isEnabled?: boolean;
@ -84,7 +80,6 @@ export interface UpdateDeviceResponse {
deviceName: string;
serialNumber: string;
description: string;
protocolVersion: string;
agentPort: number;
isEnabled: boolean;
isRunning: boolean;

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

@ -4,7 +4,7 @@ import { API_PATHS } from '@/constants/api';
// RAN配置接口定义
export interface RANConfiguration {
ranConfigurationId: string;
raN_ConfigurationId: string;
name: string;
configContent: string;
description?: string;
@ -42,7 +42,7 @@ export interface CreateRANConfigurationRequest {
// 创建RAN配置响应接口
export interface CreateRANConfigurationResponse {
ranConfigurationId: string;
raN_ConfigurationId: string;
name: string;
configContent: string;
description?: string;
@ -52,7 +52,7 @@ export interface CreateRANConfigurationResponse {
// 更新RAN配置请求接口
export interface UpdateRANConfigurationRequest {
ranConfigurationId: string;
raN_ConfigurationId: string;
name: string;
configContent: string;
description?: string;
@ -61,7 +61,7 @@ export interface UpdateRANConfigurationRequest {
// 更新RAN配置响应接口
export interface UpdateRANConfigurationResponse {
ranConfigurationId: string;
raN_ConfigurationId: string;
name: string;
configContent: string;
description?: string;

581
src/modify.md

@ -1,5 +1,435 @@
# 修改记录
## 2024-12-19 RAN配置列表状态开关优化
### 修改文件
- `X1.WebUI/src/pages/ran-configurations/RANConfigurationsTable.tsx` - 将状态徽章改为状态开关,支持直接修改状态
- `X1.WebUI/src/pages/ran-configurations/RANConfigurationsView.tsx` - 添加状态切换处理逻辑
### 修改内容
- **状态显示优化**:将状态列从徽章显示改为状态开关显示
- **直接状态修改**:用户可以直接在列表中点击状态开关来启用/禁用配置
- **实时状态更新**:状态修改后立即更新列表显示
- **操作反馈**:提供状态修改成功/失败的提示信息
- **防重复提交**:在状态修改过程中禁用开关,防止重复操作
### 技术特性
- **状态开关组件**:使用StatusSwitch组件,提供直观的开关界面
- **异步状态更新**:通过API调用更新配置状态
- **错误处理**:完整的错误处理和用户提示
- **加载状态**:在操作过程中显示加载状态
- **数据同步**:状态修改后自动刷新列表数据
### 用户体验改进
- **直观的状态显示**:状态开关比徽章更直观地显示当前状态
- **便捷的状态修改**:无需打开编辑界面,直接在列表中修改状态
- **即时反馈**:状态修改后立即看到结果
- **操作简化**:减少了打开编辑界面的步骤
- **视觉一致性**:与其他配置管理页面保持一致的交互方式
## 2024-12-19 修复RAN配置抽屉保存按钮显示问题
### 修改文件
- `X1.WebUI/src/components/ui/drawer.tsx` - 修复抽屉组件布局问题
- `X1.WebUI/src/pages/ran-configurations/RANConfigurationDrawer.tsx` - 优化抽屉表单布局
- `X1.WebUI/src/pages/ran-configurations/RANConfigurationForm.tsx` - 删除已废弃的表单组件
### 修改内容
- **修复布局问题**:为DrawerContent添加overflow-y-auto,确保内容可滚动
- **优化表单布局**:调整抽屉表单的flex布局,确保保存按钮正确显示
- **滚动优化**:DrawerContent支持垂直滚动,避免内容溢出
- **移除状态开关**:从抽屉表单中移除状态开关,因为状态修改已在列表中实现
- **修复标签重复**:移除抽屉中的重复标签,使用ConfigContentEditor内部的标签
- **布局简化**:简化配置内容区域的布局结构,确保保存按钮正确显示
- **空间分配**:正确分配flex空间,确保保存按钮始终可见
- **布局重构**:重新设计抽屉的整体布局结构,确保保存按钮正确显示
- **清理废弃文件**:删除已不再使用的RANConfigurationForm.tsx文件
### 技术特性
- **正确的flex布局**:确保表单内容、配置编辑器和保存按钮都能正确显示
- **滚动支持**:当内容超出抽屉高度时,支持垂直滚动
- **布局稳定性**:修复了保存按钮被隐藏的问题
- **功能分离**:状态修改在列表中,配置内容编辑在抽屉中
- **标签优化**:避免标签重复,使用组件内部的标签显示
- **空间管理**:合理分配垂直空间,确保所有元素都能正确显示
- **布局层次**:正确的HTML结构层次,确保CSS布局正常工作
### 用户体验改进
- **保存按钮可见**:修复了保存按钮看不到的问题
- **布局稳定**:抽屉内容布局更加稳定和可预测
- **滚动体验**:支持内容滚动,适应不同屏幕高度
- **功能清晰**:状态修改和内容编辑功能分离,避免重复
- **界面简洁**:移除重复标签,界面更加简洁
- **操作便利**:保存按钮始终可见,用户可以方便地保存配置
- **代码清理**:删除废弃的RANConfigurationForm.tsx,保持代码库整洁
## 2024-12-19 IMS配置改用抽屉组件
### 修改文件
- `X1.WebUI/src/pages/ims-configurations/IMSConfigurationDrawer.tsx` - 创建IMS配置抽屉组件
- `X1.WebUI/src/pages/ims-configurations/IMSConfigurationsTable.tsx` - 添加状态开关功能
- `X1.WebUI/src/pages/ims-configurations/IMSConfigurationsView.tsx` - 使用抽屉组件替代对话框
- `X1.WebUI/src/pages/ims-configurations/IMSConfigurationForm.tsx` - 删除已废弃的表单组件
### 修改内容
- **创建抽屉组件**:参考RAN配置实现,创建IMS配置的抽屉组件
- **状态开关集成**:在表格中添加状态开关,支持直接修改IMS配置状态
- **抽屉布局**:使用与RAN配置相同的抽屉布局,确保保存按钮正确显示
- **功能分离**:状态修改在列表中,配置内容编辑在抽屉中
- **代码清理**:删除废弃的IMSConfigurationForm.tsx文件
### 技术特性
- **抽屉布局**:右侧滑出的抽屉,提供更大的编辑空间
- **状态管理**:表格中的状态开关,支持直接切换启用/禁用状态
- **配置编辑器**:使用ConfigContentEditor组件,支持大文本编辑
- **响应式设计**:抽屉支持滚动,适应不同屏幕高度
- **统一体验**:与RAN配置保持一致的交互体验
### 用户体验改进
- **更大编辑空间**:抽屉提供比对话框更大的编辑区域
- **直接状态修改**:在列表中直接切换状态,无需进入编辑模式
- **一致的操作体验**:与RAN配置保持相同的操作方式
- **更好的视觉反馈**:状态开关提供直观的启用/禁用状态显示
## 2024-12-19 Core Network配置改用抽屉组件
### 修改文件
- `X1.WebUI/src/pages/core-network-configs/CoreNetworkConfigDrawer.tsx` - 创建Core Network配置抽屉组件
- `X1.WebUI/src/pages/core-network-configs/CoreNetworkConfigsTable.tsx` - 添加状态开关功能
- `X1.WebUI/src/pages/core-network-configs/CoreNetworkConfigsView.tsx` - 使用抽屉组件替代对话框
- `X1.WebUI/src/pages/core-network-configs/CoreNetworkConfigForm.tsx` - 删除已废弃的表单组件
### 修改内容
- **创建抽屉组件**:参考RAN配置实现,创建Core Network配置的抽屉组件
- **状态开关集成**:在表格中添加状态开关,支持直接修改Core Network配置状态
- **抽屉布局**:使用与RAN配置相同的抽屉布局,确保保存按钮正确显示
- **功能分离**:状态修改在列表中,配置内容编辑在抽屉中
- **代码清理**:删除废弃的CoreNetworkConfigForm.tsx文件
### 技术特性
- **抽屉布局**:右侧滑出的抽屉,提供更大的编辑空间
- **状态管理**:表格中的状态开关,支持直接切换启用/禁用状态
- **配置编辑器**:使用ConfigContentEditor组件,支持大文本编辑
- **响应式设计**:抽屉支持滚动,适应不同屏幕高度
- **统一体验**:与RAN配置和IMS配置保持一致的交互体验
### 用户体验改进
- **更大编辑空间**:抽屉提供比对话框更大的编辑区域
- **直接状态修改**:在列表中直接切换状态,无需进入编辑模式
- **一致的操作体验**:与RAN配置和IMS配置保持相同的操作方式
- **更好的视觉反馈**:状态开关提供直观的启用/禁用状态显示
## 2024-12-19 RAN配置创建改用抽屉组件
### 修改文件
- `X1.WebUI/src/components/ui/drawer.tsx` - 新增抽屉组件
- `X1.WebUI/src/pages/ran-configurations/RANConfigurationDrawer.tsx` - 新增RAN配置抽屉表单组件
- `X1.WebUI/src/pages/ran-configurations/RANConfigurationsView.tsx` - 修改为使用抽屉组件
- `X1.WebUI/src/components/ui/ConfigContentEditor.tsx` - 优化配置内容编辑器,支持高度自适应
### 修改内容
- **创建抽屉组件**:实现右侧滑出的抽屉组件,提供更大的编辑空间
- **RAN配置抽屉表单**:专门为RAN配置设计的抽屉表单,支持配置内容的编辑
- **替换对话框**:将原有的对话框创建/编辑方式改为抽屉方式
- **优化编辑体验**:为配置内容提供更大的编辑区域,支持自适应高度
- **修复定位问题**:修复抽屉组件显示位置错误的问题,确保从右侧正确滑出
- **配置内容自适应**:配置内容编辑器现在能够占满抽屉中的剩余空间
### 技术特性
- **抽屉组件**:600px宽度,支持动画效果,右侧滑出
- **配置内容编辑器**:使用ConfigContentEditor组件,支持搜索高亮功能和高度自适应
- **状态开关**:使用StatusSwitch组件,提供更好的视觉反馈
- **表单验证**:保持原有的表单验证逻辑
- **响应式设计**:支持90%视口宽度的最大限制
- **正确定位**:使用fixed定位和transform动画,确保抽屉从右侧正确滑出
- **弹性布局**:使用flex布局,配置内容编辑器自动占满剩余空间
### 用户体验改进
- **更大的编辑空间**:抽屉提供比对话框更大的编辑区域
- **更好的配置内容编辑**:配置内容编辑器自动占满剩余空间,提供最大编辑区域
- **保持上下文**:抽屉不会完全遮挡主界面,用户可以看到背景内容
- **流畅的动画**:右侧滑入滑出动画提供良好的交互体验
- **正确的显示位置**:抽屉从右侧正确滑出,不会显示在错误位置
- **自适应高度**:配置内容编辑器根据抽屉高度自动调整,充分利用可用空间
## 2024-12-19 网络栈配置菜单图标修改
### 修改文件
- `X1.WebUI/src/constants/menuConfig.ts` - 修改网络栈配置菜单项的图标
### 修改内容
- 导入 `Network` 图标:在 lucide-react 导入中添加 `Network`
- 修改网络栈配置菜单项图标:将 `icon: Gauge` 改为 `icon: Network`
- 使网络栈配置菜单项与仪表管理菜单项使用不同的图标,提高视觉区分度
### 技术说明
- 仪表管理菜单项继续使用 `Gauge` 图标
- 网络栈配置菜单项使用 `Network` 图标,更符合网络配置的功能定位
- 提高了菜单的视觉层次和用户体验
## 2024-12-19 配置表格Key属性统一修复
### 修改文件
- `X1.WebUI/src/pages/ran-configurations/RANConfigurationsTable.tsx` - 修复React Key属性警告
- `X1.WebUI/src/pages/ims-configurations/IMSConfigurationsTable.tsx` - 修复React Key属性警告
- `X1.WebUI/src/pages/core-network-configs/CoreNetworkConfigsTable.tsx` - 修复React Key属性警告
### 问题修复
- **Key属性警告**:修复了所有配置表格中缺少唯一key属性的React警告
- **唯一标识**:为每个TableCell添加了基于配置ID和列键的唯一key
- **操作按钮Key**:重构操作列中的按钮渲染,使用map方法确保每个按钮都有唯一key
- **容器Key清理**:移除了TableHeader和TableBody上不必要的key属性
- **性能优化**:确保React能够正确识别和更新列表项
### 技术细节
- 将 `key={column.key}` 修改为 `key={`${configuration.raN_ConfigurationId}-${column.key}`}`、`key={`${configuration.ims_ConfigurationId}-${column.key}`}` 和 `key={`${config.CoreNetworkConfigId}-${column.key}`}`
- 重构操作列按钮渲染:使用数组map方法替代静态JSX,确保每个按钮都有唯一key
- 使用动态key:将TableHeader和TableBody的key改为 `key={`header-${Date.now()}`}` 和 `key={`body-${Date.now()}`}`
- 确保每个表格单元格和子元素都有唯一的key属性
- 避免React渲染时的key冲突问题
- 修复actions列中span元素的key:使用 `key={`${configuration.id}-${item.key}`}` 格式
- 修复TableHead的key:使用 `key={`header-${col.key}-${index}`}` 格式,包含索引确保唯一性
- 修复TableRow的key:使用 `key={`${configuration.id}-${index}`}` 格式,包含索引确保唯一性
### 重构说明
- 将操作列中的两个静态span元素改为数组map渲染
- 每个按钮对象包含key、className、onClick和text属性
- 通过map方法确保每个渲染的元素都有唯一的key属性
- 使用动态时间戳作为容器元素的key,避免写死的key值
- 为actions列中的span元素添加更唯一的key,包含配置ID和操作类型
- 为TableHead添加更唯一的key,包含列key和索引,避免重复key冲突
- 为TableRow添加更唯一的key,包含配置ID和索引,确保每行都有唯一标识
## 2024-12-19 修复Dialog可访问性警告
### 问题描述
- Radix UI Dialog组件要求 `DialogContent` 必须包含 `DialogTitle` 以确保屏幕阅读器可访问性
- 项目中多个Dialog组件缺少 `DialogTitle`,导致可访问性警告
### 修复内容
1. **Dialog组件导出**
- 在 `dialog.tsx` 中添加 `DialogTitle``DialogDescription` 的导出
2. **RAN配置视图**
- 添加 `DialogTitle` 导入
- 为创建和编辑对话框添加标题:"创建RAN配置" 和 "编辑RAN配置"
3. **IMS配置视图**
- 添加 `DialogTitle` 导入
- 为创建和编辑对话框添加标题:"创建IMS配置" 和 "编辑IMS配置"
4. **核心网络配置视图**
- 添加 `DialogTitle` 导入
- 为创建和编辑对话框添加标题:"创建核心网络配置" 和 "编辑核心网络配置"
### 技术说明
- 确保所有Dialog组件都符合Radix UI的可访问性要求
- 为屏幕阅读器用户提供更好的体验
- 添加语义化的对话框标题,提高用户体验
- 使用一致的样式:`text-lg font-semibold mb-4`
## 2024-12-19 修复配置字段名不匹配问题
### 问题描述
- 前端接口定义与后端API返回的数据字段名不匹配
- 导致前端访问配置ID时出现 undefined 错误
### RAN配置修复内容
1. **修复前端接口定义**
- `RANConfiguration` 接口:`id` → `raN_ConfigurationId`
- `CreateRANConfigurationResponse` 接口:`id` → `raN_ConfigurationId`
- `UpdateRANConfigurationRequest` 接口:`id` → `raN_ConfigurationId`
- `UpdateRANConfigurationResponse` 接口:`id` → `raN_ConfigurationId`
2. **修复表格组件**
- `RANConfigurationsTable.tsx`:`configuration.id` → `configuration.raN_ConfigurationId`
- 表格行key:`key={configuration.id}` → `key={configuration.raN_ConfigurationId}`
- 表格单元格key:`key={`${configuration.id}-${column.key}`}` → `key={`${configuration.raN_ConfigurationId}-${column.key}`}`
3. **修复视图组件**
- `RANConfigurationsView.tsx`:删除和更新操作中的字段引用
- `configuration.id``configuration.raN_ConfigurationId`
- `selectedConfiguration.id``selectedConfiguration.raN_ConfigurationId`
### IMS配置修复内容
1. **修复前端接口定义**
- `IMSConfiguration` 接口:`imsConfigurationId` → `ims_ConfigurationId`
- `CreateIMSConfigurationResponse` 接口:`imsConfigurationId` → `ims_ConfigurationId`
- `UpdateIMSConfigurationRequest` 接口:`imsConfigurationId` → `ims_ConfigurationId`
- `UpdateIMSConfigurationResponse` 接口:`imsConfigurationId` → `ims_ConfigurationId`
2. **修复表格组件**
- `IMSConfigurationsTable.tsx`:`configuration.imsConfigurationId` → `configuration.ims_ConfigurationId`
- 表格行key:`key={configuration.imsConfigurationId}` → `key={configuration.ims_ConfigurationId}`
- 表格单元格key:`key={`${configuration.imsConfigurationId}-${column.key}`}` → `key={`${configuration.ims_ConfigurationId}-${column.key}`}`
3. **修复视图组件**
- `IMSConfigurationsView.tsx`:删除和更新操作中的字段引用
- `configuration.imsConfigurationId``configuration.ims_ConfigurationId`
- `selectedConfiguration.imsConfigurationId``selectedConfiguration.ims_ConfigurationId`
### 核心网络配置修复内容
1. **修复前端接口定义**
- `CoreNetworkConfig` 接口:`coreNetworkConfigId` → `CoreNetworkConfigId`
- `CreateCoreNetworkConfigResponse` 接口:`coreNetworkConfigId` → `CoreNetworkConfigId`
- `UpdateCoreNetworkConfigRequest` 接口:`coreNetworkConfigId` → `CoreNetworkConfigId`
- `UpdateCoreNetworkConfigResponse` 接口:`coreNetworkConfigId` → `CoreNetworkConfigId`
2. **修复表格组件**
- `CoreNetworkConfigsTable.tsx`:`config.coreNetworkConfigId` → `config.CoreNetworkConfigId`
- 表格行key:`key={config.coreNetworkConfigId}` → `key={config.CoreNetworkConfigId}`
- 表格单元格key:`key={`${config.coreNetworkConfigId}-${column.key}`}` → `key={`${config.CoreNetworkConfigId}-${column.key}`}`
3. **修复视图组件**
- `CoreNetworkConfigsView.tsx`:删除和更新操作中的字段引用
- `config.coreNetworkConfigId``config.CoreNetworkConfigId`
- `selectedConfig.coreNetworkConfigId``selectedConfig.CoreNetworkConfigId`
### 技术说明
- 统一前后端字段命名规范
- 确保前端接口定义与后端API返回数据格式一致
- 修复所有相关的字段引用
- 避免运行时出现 undefined 错误
- 注意后端返回的字段名大小写:
- RAN配置:`raN_ConfigurationId`
- IMS配置:`ims_ConfigurationId`
- 核心网络配置:`CoreNetworkConfigId`
## 2024-12-19 配置表单统一集成配置内容编辑器
### 修改文件
- `X1.WebUI/src/pages/ims-configurations/IMSConfigurationForm.tsx` - 集成配置内容编辑器
- `X1.WebUI/src/pages/core-network-configs/CoreNetworkConfigForm.tsx` - 集成配置内容编辑器
### 功能统一
- **IMS配置表单**:使用新的配置内容编辑器替换原有的Textarea
- **核心网络配置表单**:使用新的配置内容编辑器替换原有的Textarea
- **功能一致性**:所有配置表单现在都支持搜索高亮功能
### 技术实现
- 导入 `ConfigContentEditor` 组件
- 替换原有的配置内容输入区域
- 移除JSON格式限制的提示文本
- 保持表单的其他功能不变
### 用户体验
- **统一体验**:所有配置表单现在都有相同的编辑体验
- **搜索功能**:支持在配置内容中搜索和高亮
- **格式灵活**:不再强制要求JSON格式,支持任意格式的配置内容
## 2024-12-19 RAN配置表格Key属性修复
### 修改文件
- `X1.WebUI/src/pages/ran-configurations/RANConfigurationsTable.tsx` - 修复React Key属性警告
### 问题修复
- **Key属性警告**:修复了表格单元格缺少唯一key属性的React警告
- **唯一标识**:为每个TableCell添加了基于配置ID和列键的唯一key
- **操作按钮Key**:重构操作列中的按钮渲染,使用map方法确保每个按钮都有唯一key
- **性能优化**:确保React能够正确识别和更新列表项
### 技术细节
- 将 `key={column.key}` 修改为 `key={`${configuration.ranConfigurationId}-${column.key}`}`
- 重构操作列按钮渲染:使用数组map方法替代静态JSX,确保每个按钮都有唯一key
- 确保每个表格单元格和子元素都有唯一的key属性
- 避免React渲染时的key冲突问题
### 重构说明
- 将操作列中的两个静态span元素改为数组map渲染
- 每个按钮对象包含key、className、onClick和text属性
- 通过map方法确保每个渲染的元素都有唯一的key属性
## 2024-12-19 配置内容编辑器组件简化
### 修改文件
- `X1.WebUI/src/components/ui/ConfigContentEditor.tsx` - 简化配置内容编辑器组件
### 功能调整
#### 1. 移除的功能
- **复制功能**:移除了复制到剪贴板的功能
- **导出功能**:移除了导出为文件的功能
- **导入功能**:移除了从文件导入的功能
- **模板管理**:移除了模板注册、查询、修改、删除功能
#### 2. 新增的功能
- **搜索高亮**:支持在配置内容中搜索并高亮显示匹配项
- **实时搜索**:输入搜索关键词时实时高亮所有匹配项
- **导航功能**:支持上一个/下一个匹配项的快速导航
- **匹配计数**:显示当前搜索关键词的匹配数量
#### 3. 搜索功能特性
- **高亮显示**:使用黄色背景高亮所有匹配的文本
- **大小写不敏感**:搜索时忽略大小写
- **正则表达式转义**:自动转义特殊字符,支持普通文本搜索
- **循环搜索**:到达末尾时自动从头开始,到达开头时自动从末尾开始
#### 4. 用户体验
- **简洁界面**:只保留搜索按钮,界面更加简洁
- **快速访问**:点击搜索按钮即可打开搜索栏
- **键盘友好**:搜索栏自动获得焦点,支持回车键操作
- **视觉反馈**:匹配项数量实时显示
### 技术实现
- **高亮层**:使用绝对定位的透明层显示高亮效果
- **正则匹配**:使用正则表达式进行大小写不敏感的匹配
- **选择导航**:通过 setSelectionRange 实现光标定位
- **状态管理**:使用 useState 管理搜索状态和高亮内容
### 使用方式
```tsx
<ConfigContentEditor
value={configContent}
onChange={setConfigContent}
placeholder="请输入配置内容"
rows={8}
required
disabled={false}
label="配置内容"
/>
```
### 界面布局
- **工具栏**:只包含搜索按钮
- **搜索栏**:可展开的搜索输入框和导航按钮
- **编辑器**:主要的文本编辑区域,支持高亮显示
- **高亮层**:覆盖在编辑器上的透明高亮层
## 2024-12-19 RAN配置页面展开按钮移除
### 修改文件
- `X1.WebUI/src/pages/ran-configurations/RANConfigurationsView.tsx`
### 修改内容
#### 1. 移除展开/收起功能
- 删除了 `ChevronDownIcon``ChevronUpIcon` 的导入
- 移除了 `showAdvanced` 状态变量
- 删除了 `advancedFields` 高级字段配置
- 将 `firstRowFields` 重命名为 `searchFields`
#### 2. 简化搜索栏结构
- 移除了展开/收起按钮
- 只保留基本的搜索字段:搜索关键词和状态
- 简化了表单布局,固定为3列网格布局
- 移除了每页数量选择功能(该功能已移至分页组件)
#### 3. 优化代码结构
- 移除了条件渲染逻辑
- 简化了字段映射和事件处理
- 保持了类型安全和错误处理
### 功能影响
- **用户体验**:搜索界面更简洁,减少了用户操作复杂度
- **界面布局**:搜索栏高度固定,布局更稳定
- **功能完整性**:核心搜索功能保持不变,分页大小调整功能移至分页组件
### 技术细节
- 保持了所有必要的搜索功能
- 维持了类型安全和错误处理机制
- 简化了组件状态管理
- 提高了代码可读性和维护性
## 2024-12-19 网络栈配置管理页面实现
### 新增文件
@ -1432,4 +1862,153 @@ const createResult = await coreNetworkConfigService.createCoreNetworkConfig({
- **前端服务**:提供了完整的网络栈配置管理前端服务
- **API 集成**:与后端 `NetworkStackConfigsController` 完全对应
- **类型安全**:提供了完整的 TypeScript 类型定义
- **开发体验**:统一的服务接口,便于前端开发使用
- **开发体验**:统一的服务接口,便于前端开发使用
## 2024-12-19 数据库重新迁移
### 修改概述
根据用户要求,重新迁移数据库,删除所有现有迁移文件并创建新的初始迁移。
### 执行步骤
#### 1. 检查现有迁移
- 使用 `dotnet ef migrations list` 命令检查现有迁移
- 发现存在3个迁移文件:
- `20250705165102_InitialCreate`
- `20250705173130_InitProtocolVersionAndDevice`
- `20250705174217_UpdateProtocolVersionAndCellularDevice`
#### 2. 回滚数据库
- 使用 `dotnet ef database update 0` 命令将数据库回滚到初始状态
- 成功删除了所有表结构和迁移历史记录
#### 3. 删除迁移文件
- 删除了所有现有的迁移文件:
- `20250705174217_UpdateProtocolVersionAndCellularDevice.cs`
- `20250705174217_UpdateProtocolVersionAndCellularDevice.Designer.cs`
- `20250705173130_InitProtocolVersionAndDevice.cs`
- `20250705173130_InitProtocolVersionAndDevice.Designer.cs`
- `20250705165102_InitialCreate.cs`
- `20250705165102_InitialCreate.Designer.cs`
- `AppDbContextModelSnapshot.cs`
#### 4. 创建新迁移
- 使用 `dotnet ef migrations add InitialCreate` 命令创建新的初始迁移
- 生成了新的迁移文件:
- `20250728081332_InitialCreate.cs`
- `20250728081332_InitialCreate.Designer.cs`
- `AppDbContextModelSnapshot.cs`
#### 5. 应用新迁移
- 使用 `dotnet ef database update` 命令将新迁移应用到数据库
- 成功创建了所有表结构和初始数据
### 技术细节
#### 1. 迁移文件管理
- 完全清理了旧的迁移历史
- 创建了全新的初始迁移
- 确保数据库结构与当前代码模型完全一致
#### 2. 数据库状态
- 数据库已回滚到初始状态
- 所有表结构已重新创建
- 迁移历史记录已重置
#### 3. 实体模型
- 包含所有当前定义的实体:
- 用户和角色管理(AppUser、AppRole、UserRole、Permission、RolePermission)
- 设备管理(CellularDevice、ProtocolVersion)
- 网络配置(RAN_Configuration、CoreNetworkConfig、IMS_Configuration、NetworkStackConfig、Stack_CoreIMS_Binding)
- 日志记录(LoginLog)
### 影响范围
- **数据库结构**:所有表结构已重新创建
- **数据丢失**:所有现有数据已清除
- **迁移历史**:迁移历史记录已重置
- **开发环境**:开发环境数据库已重置为初始状态
### 注意事项
- 这是一个破坏性操作,所有现有数据已丢失
- 需要重新初始化测试数据
- 生产环境请谨慎执行此操作
- 建议在执行前备份重要数据
### 后续操作建议
1. 重新初始化测试数据
2. 验证所有实体关系是否正确
3. 测试所有CRUD操作功能
4. 确认数据库约束和索引设置正确
## 2024-12-19 修复IMS配置字段名称
### 修复内容
- 将 `ims_ConfigurationId` 改为 `imS_ConfigurationId` 以匹配后端API返回的数据格式
- 修改 `X1.WebUI/src/services/imsConfigurationService.ts` 中的所有接口定义
- 修改 `X1.WebUI/src/pages/ims-configurations/IMSConfigurationsTable.tsx` 中的字段引用
- 修改 `X1.WebUI/src/pages/ims-configurations/IMSConfigurationsView.tsx` 中的字段引用
### 技术说明
- 确保前端接口定义与后端API返回的JSON数据格式完全一致
- 修复字段名称大小写问题,从 `ims_ConfigurationId` 改为 `imS_ConfigurationId`
- 保持与后端数据结构的同步
## 2024-12-19 修复核心网络配置字段名称
### 修复内容
- 将 `CoreNetworkConfigId` 改为 `coreNetworkConfigId` 以匹配后端API返回的数据格式
- 修改 `X1.WebUI/src/services/coreNetworkConfigService.ts` 中的所有接口定义
- 修改 `X1.WebUI/src/pages/core-network-configs/CoreNetworkConfigsTable.tsx` 中的字段引用
- 修改 `X1.WebUI/src/pages/core-network-configs/CoreNetworkConfigsView.tsx` 中的字段引用
### 技术说明
- 确保前端接口定义与后端API返回的JSON数据格式完全一致
- 修复字段名称大小写问题,从 `CoreNetworkConfigId` 改为 `coreNetworkConfigId`
- 保持与后端数据结构的同步
## 2025-07-28
### 修复 instrumentService.ts 中的 protocolVersion 字段问题
#### 问题描述
- 前端 `instrumentService.ts` 中的设备接口定义包含了后端不存在的 `protocolVersion` 字段
- 创建设备和更新设备的请求接口中包含了后端命令中不存在的 `protocolVersionId` 字段
- 这导致前后端接口不匹配,可能引起运行时错误
#### 修复内容
1. **修复设备响应接口**
- 从 `Device` 接口中移除 `protocolVersion: string;` 字段
- 从 `CreateDeviceResponse` 接口中移除 `protocolVersion: string;` 字段
- 从 `UpdateDeviceResponse` 接口中移除 `protocolVersion: string;` 字段
2. **修复设备请求接口**
- 从 `CreateDeviceRequest` 接口中移除 `protocolVersionId: string;` 字段
- 从 `UpdateDeviceRequest` 接口中移除 `protocolVersionId: string;` 字段
3. **修复 DeviceForm 组件**
- 移除对 `protocolVersionId` 字段的状态管理
- 移除协议版本加载逻辑
- 移除协议版本选择表单字段
- 添加 `isEnabled``isRunning` 复选框字段
4. **修复 DevicesTable 组件**
- 移除对 `protocolVersion` 字段的渲染逻辑
- 更新表格字段映射关系注释
- 优化组件结构和样式
5. **修复 DevicesView 组件**
- 移除默认列配置中的 `protocolVersion` 字段
- 更新表格字段映射关系注释
- 移除编辑功能中对不存在字段的引用
#### 技术细节
- 后端设备实体 `CellularDevice` 通过 `ProtocolVersions` 集合与协议版本关联,而不是直接存储协议版本字符串
- 后端的设备响应模型(`GetDeviceByIdResponse`、`CreateDeviceResponse`、`UpdateDeviceResponse`)中没有 `protocolVersion` 字段
- 后端的设备命令(`CreateDeviceCommand`、`UpdateDeviceCommand`)中没有 `protocolVersionId` 字段
#### 影响范围
- 前端设备管理功能现在与后端API完全匹配
- 设备表单不再包含协议版本选择,简化了用户界面
- 设备与协议版本的关联关系需要在后续功能中单独处理
- 设备表格不再显示协议版本列,但保留了其他所有功能
Loading…
Cancel
Save