Browse Source

feat(role): 角色分页查询与命名规范,参照用户分页实现

refactor/repository-structure
hyh 7 months ago
parent
commit
492eaa169d
  1. 29
      src/CellularManagement.Application/Features/Roles/Queries/GetAllRolesQuery.cs
  2. 12
      src/CellularManagement.Application/Features/Roles/Queries/GetRole/GetRoleResponse.cs
  3. 43
      src/CellularManagement.Application/Features/Roles/Queries/RoleQueryHandler.cs
  4. 32
      src/CellularManagement.Domain/Entities/AppRole.cs
  5. 8
      src/CellularManagement.Infrastructure/Configurations/AppRoleConfiguration.cs
  6. 290
      src/CellularManagement.Infrastructure/Migrations/20250516021320_AddRoleTimestamps.Designer.cs
  7. 43
      src/CellularManagement.Infrastructure/Migrations/20250516021320_AddRoleTimestamps.cs
  8. 8
      src/CellularManagement.Infrastructure/Migrations/AppDbContextModelSnapshot.cs
  9. 28
      src/CellularManagement.Presentation/Controllers/RolesController.cs
  10. 2
      src/CellularManagement.WebUI/package.json
  11. 168
      src/CellularManagement.WebUI/src/components/ui/form.tsx
  12. 6
      src/CellularManagement.WebUI/src/components/ui/index.ts
  13. 24
      src/CellularManagement.WebUI/src/components/ui/input.tsx
  14. 20
      src/CellularManagement.WebUI/src/components/ui/label.tsx
  15. 113
      src/CellularManagement.WebUI/src/components/ui/table.tsx
  16. 79
      src/CellularManagement.WebUI/src/pages/roles/RoleForm.tsx
  17. 108
      src/CellularManagement.WebUI/src/pages/roles/RoleTable.tsx

29
src/CellularManagement.Application/Features/Roles/Queries/GetAllRolesQuery.cs

@ -4,16 +4,35 @@ using CellularManagement.Application.Common;
namespace CellularManagement.Application.Features.Roles.Queries;
/// <summary>
/// 获取所有角色的查询
/// 获取所有角色的查询,支持分页和按名称模糊查询
/// </summary>
public record GetAllRolesQuery : IRequest<OperationResult<GetAllRolesResponse>>;
public sealed record GetAllRolesQuery(
int PageNumber = 1,
int PageSize = 10,
string? RoleName = null
) : IRequest<OperationResult<GetAllRolesResponse>>;
/// <summary>
/// 获取所有角色的响应
/// 获取所有角色响应,包含角色列表和分页信息
/// </summary>
public record GetAllRolesResponse(IEnumerable<RoleDto> Roles);
public sealed record GetAllRolesResponse(
List<RoleDto> Roles,
int TotalCount,
int PageNumber,
int PageSize
)
{
public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize);
public bool HasPreviousPage => PageNumber > 1;
public bool HasNextPage => PageNumber < TotalPages;
}
/// <summary>
/// 角色数据传输对象
/// </summary>
public record RoleDto(string Id, string Name, string Description);
public record RoleDto(
string Id,
string Name,
string Description,
DateTime CreatedAt,
DateTime UpdatedAt);

12
src/CellularManagement.Application/Features/Roles/Queries/GetRole/GetRoleResponse.cs

@ -17,4 +17,14 @@ public sealed record GetRoleResponse(
/// <summary>
/// 角色描述
/// </summary>
string? Description);
string? Description,
/// <summary>
/// 创建时间
/// </summary>
DateTime CreatedAt,
/// <summary>
/// 更新时间
/// </summary>
DateTime UpdatedAt);

43
src/CellularManagement.Application/Features/Roles/Queries/RoleQueryHandler.cs

@ -4,6 +4,7 @@ using MediatR;
using CellularManagement.Domain.Entities;
using CellularManagement.Application.Features.Roles.Queries.GetRole;
using CellularManagement.Application.Features.Roles.Queries;
using Microsoft.EntityFrameworkCore;
namespace CellularManagement.Application.Features.Roles.Queries;
@ -48,7 +49,12 @@ public sealed class RoleQueryHandler :
_logger.LogInformation("获取角色 {RoleId} 成功", request.RoleId);
return OperationResult<GetRoleResponse>.CreateSuccess(
new GetRoleResponse(role.Id, role.Name, role.Description));
new GetRoleResponse(
role.Id,
role.Name,
role.Description,
role.CreatedAt,
role.UpdatedAt));
}
catch (Exception ex)
{
@ -58,7 +64,7 @@ public sealed class RoleQueryHandler :
}
/// <summary>
/// 处理获取所有角色请求
/// 处理获取所有角色请求(分页+模糊查询)
/// </summary>
public async Task<OperationResult<GetAllRolesResponse>> Handle(
GetAllRolesQuery request,
@ -66,17 +72,38 @@ public sealed class RoleQueryHandler :
{
try
{
var roles = _roleManager.Roles.ToList();
var query = _roleManager.Roles.AsQueryable();
if (!string.IsNullOrWhiteSpace(request.RoleName))
{
query = query.Where(r => r.Name.Contains(request.RoleName));
}
var totalCount = await query.CountAsync(cancellationToken);
var roles = await query
.Skip((request.PageNumber - 1) * request.PageSize)
.Take(request.PageSize)
.ToListAsync(cancellationToken);
var roleDtos = roles.Select(role => new RoleDto(
role.Id,
role.Name,
role.Description
));
role.Description,
role.CreatedAt,
role.UpdatedAt
)).ToList();
var response = new GetAllRolesResponse(
roleDtos,
totalCount,
request.PageNumber,
request.PageSize
);
_logger.LogInformation("成功获取所有角色,共 {Count} 个", roles.Count);
_logger.LogInformation("成功获取所有角色,共 {Count} 个", totalCount);
return OperationResult<GetAllRolesResponse>.CreateSuccess(
new GetAllRolesResponse(roleDtos));
return OperationResult<GetAllRolesResponse>.CreateSuccess(response);
}
catch (Exception ex)
{

32
src/CellularManagement.Domain/Entities/AppRole.cs

@ -13,6 +13,16 @@ public sealed class AppRole : IdentityRole
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; private set; }
/// <summary>
/// 更新时间
/// </summary>
public DateTime UpdatedAt { get; private set; }
/// <summary>
/// 初始化角色
/// 使用 GUID 作为 ID
@ -20,6 +30,8 @@ public sealed class AppRole : IdentityRole
public AppRole()
{
Id = Guid.NewGuid().ToString();
CreatedAt = DateTime.UtcNow;
UpdatedAt = DateTime.UtcNow;
}
/// <summary>
@ -28,6 +40,8 @@ public sealed class AppRole : IdentityRole
/// <param name="id">角色 ID</param>
public AppRole(string id) : base(id)
{
CreatedAt = DateTime.UtcNow;
UpdatedAt = DateTime.UtcNow;
}
/// <summary>
@ -37,6 +51,24 @@ public sealed class AppRole : IdentityRole
public AppRole(string roleName, string id) : base(roleName)
{
Id = id;
CreatedAt = DateTime.UtcNow;
UpdatedAt = DateTime.UtcNow;
}
/// <summary>
/// 更新角色信息
/// </summary>
public void Update(string? name = null, string? description = null)
{
if (name != null)
{
Name = name;
}
if (description != null)
{
Description = description;
}
UpdatedAt = DateTime.UtcNow;
}
// Identity 导航属性

8
src/CellularManagement.Infrastructure/Configurations/AppRoleConfiguration.cs

@ -49,6 +49,14 @@ public sealed class AppRoleConfiguration : IEntityTypeConfiguration<AppRole>
.HasMaxLength(500)
.HasComment("角色描述");
builder.Property(r => r.CreatedAt)
.IsRequired()
.HasComment("创建时间");
builder.Property(r => r.UpdatedAt)
.IsRequired()
.HasComment("更新时间");
// 配置关系
builder.HasMany(r => r.RoleClaims)
.WithOne()

290
src/CellularManagement.Infrastructure/Migrations/20250516021320_AddRoleTimestamps.Designer.cs

@ -0,0 +1,290 @@
// <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 CellularManagement.Infrastructure.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20250516021320_AddRoleTimestamps")]
partial class AddRoleTimestamps
{
/// <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<string>("Email")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("电子邮箱");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean")
.HasComment("邮箱是否已验证");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean")
.HasComment("是否启用账户锁定");
b.Property<DateTimeOffset?>("LockoutEnd")
.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>("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.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.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
}
}
}

43
src/CellularManagement.Infrastructure/Migrations/20250516021320_AddRoleTimestamps.cs

@ -0,0 +1,43 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CellularManagement.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddRoleTimestamps : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "CreatedAt",
table: "Roles",
type: "timestamp with time zone",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
comment: "创建时间");
migrationBuilder.AddColumn<DateTime>(
name: "UpdatedAt",
table: "Roles",
type: "timestamp with time zone",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
comment: "更新时间");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "CreatedAt",
table: "Roles");
migrationBuilder.DropColumn(
name: "UpdatedAt",
table: "Roles");
}
}
}

8
src/CellularManagement.Infrastructure/Migrations/AppDbContextModelSnapshot.cs

@ -33,6 +33,10 @@ namespace CellularManagement.Infrastructure.Migrations
.HasColumnType("text")
.HasComment("并发控制戳");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("character varying(500)")
@ -50,6 +54,10 @@ namespace CellularManagement.Infrastructure.Migrations
.HasColumnType("character varying(256)")
.HasComment("标准化角色名称(大写)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("更新时间");
b.HasKey("Id");
b.HasIndex("Name")

28
src/CellularManagement.Presentation/Controllers/RolesController.cs

@ -201,36 +201,40 @@ public class RolesController : ApiController
}
/// <summary>
/// 获取所有角色
/// 获取所有角色(分页+模糊查询)
/// </summary>
/// <remarks>
/// 示例请求:
///
/// GET /api/roles
/// GET /api/roles?pageNumber=1&pageSize=10&roleName=管理员
///
/// </remarks>
/// <param name="query">查询参数,包含分页和过滤条件</param>
/// <returns>
/// 所有角色信息列表
/// 查询结果,包含:
/// - 成功:返回角色列表和分页信息
/// - 失败:返回错误信息
/// </returns>
/// <response code="200">获取成功,返回角色列表</response>
/// <response code="500">服务器错误</response>
/// <response code="200">查询成功,返回角色列表</response>
/// <response code="400">查询失败,返回错误信息</response>
[HttpGet]
[ProducesResponseType(typeof(OperationResult<GetAllRolesResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(OperationResult<GetAllRolesResponse>), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<OperationResult<GetAllRolesResponse>>> GetAllRoles()
[ProducesResponseType(typeof(OperationResult<object>), StatusCodes.Status400BadRequest)]
public async Task<ActionResult<OperationResult<GetAllRolesResponse>>> GetAllRoles([FromQuery] GetAllRolesQuery query)
{
try
{
var query = new GetAllRolesQuery();
var result = await mediator.Send(query);
if (result.IsSuccess)
{
_logger.LogInformation("成功获取所有角色");
_logger.LogInformation("获取角色列表成功,共 {TotalCount} 条记录,当前第 {PageNumber} 页",
result.Data?.TotalCount,
result.Data?.PageNumber);
}
else
{
_logger.LogWarning("获取所有角色失败: {Error}",
_logger.LogWarning("获取角色列表失败: {Error}",
result.ErrorMessages?.FirstOrDefault());
}
@ -238,9 +242,9 @@ public class RolesController : ApiController
}
catch (Exception ex)
{
_logger.LogError(ex, "获取所有角色时发生异常");
_logger.LogError(ex, "获取角色列表时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError,
OperationResult<GetAllRolesResponse>.CreateFailure("系统错误,请稍后重试"));
OperationResult<object>.CreateFailure("系统错误,请稍后重试"));
}
}
}

2
src/CellularManagement.WebUI/package.json

@ -13,6 +13,7 @@
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.1.13",
"@radix-ui/react-slot": "^1.0.2",
"axios": "^1.9.0",
@ -21,6 +22,7 @@
"lucide-react": "^0.323.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.50.0",
"react-icons": "^5.5.0",
"react-router-dom": "^6.22.0",
"recoil": "^0.7.7",

168
src/CellularManagement.WebUI/src/components/ui/form.tsx

@ -0,0 +1,168 @@
import * as React from 'react';
import * as LabelPrimitive from '@radix-ui/react-label';
import { Slot } from '@radix-ui/react-slot';
import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext } from 'react-hook-form';
import { cn } from '@/lib/utils';
import { Label } from '@/components/ui/label';
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext();
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error('useFormField should be used within <FormField>');
}
const { id } = itemContext;
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};
type FormItemContextValue = {
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
);
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn('space-y-2', className)} {...props} />
</FormItemContext.Provider>
);
});
FormItem.displayName = 'FormItem';
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField();
return (
<Label
ref={ref}
className={cn(error && 'text-destructive', className)}
htmlFor={formItemId}
{...props}
/>
);
});
FormLabel.displayName = 'FormLabel';
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
);
});
FormControl.displayName = 'FormControl';
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField();
return (
<p
ref={ref}
id={formDescriptionId}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
);
});
FormDescription.displayName = 'FormDescription';
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;
if (!body) {
return null;
}
return (
<p
ref={ref}
id={formMessageId}
className={cn('text-sm font-medium text-destructive', className)}
{...props}
>
{body}
</p>
);
});
FormMessage.displayName = 'FormMessage';
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
};

6
src/CellularManagement.WebUI/src/components/ui/index.ts

@ -0,0 +1,6 @@
export * from './button';
export * from './input';
export * from './form';
export * from './label';
export * from './table';
export * from './dialog';

24
src/CellularManagement.WebUI/src/components/ui/input.tsx

@ -0,0 +1,24 @@
import * as React from 'react';
import { cn } from '@/lib/utils';
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
{...props}
/>
);
}
);
Input.displayName = 'Input';
export { Input };

20
src/CellularManagement.WebUI/src/components/ui/label.tsx

@ -0,0 +1,20 @@
import * as React from 'react';
import * as LabelPrimitive from '@radix-ui/react-label';
import { cn } from '@/lib/utils';
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
className
)}
{...props}
/>
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label };

113
src/CellularManagement.WebUI/src/components/ui/table.tsx

@ -0,0 +1,113 @@
import * as React from 'react';
import { cn } from '@/lib/utils';
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn('w-full caption-bottom text-sm', className)}
{...props}
/>
</div>
));
Table.displayName = 'Table';
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
));
TableHeader.displayName = 'TableHeader';
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn('[&_tr:last-child]:border-0', className)}
{...props}
/>
));
TableBody.displayName = 'TableBody';
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn('bg-primary font-medium text-primary-foreground', className)}
{...props}
/>
));
TableFooter.displayName = 'TableFooter';
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
className
)}
{...props}
/>
));
TableRow.displayName = 'TableRow';
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
'h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0',
className
)}
{...props}
/>
));
TableHead.displayName = 'TableHead';
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn('p-4 align-middle [&:has([role=checkbox])]:pr-0', className)}
{...props}
/>
));
TableCell.displayName = 'TableCell';
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn('mt-4 text-sm text-muted-foreground', className)}
{...props}
/>
));
TableCaption.displayName = 'TableCaption';
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
};

79
src/CellularManagement.WebUI/src/pages/roles/RoleForm.tsx

@ -1,43 +1,66 @@
import React, { useState } from 'react';
import React from 'react';
import { useForm } from 'react-hook-form';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
interface RoleFormProps {
onSubmit: (data: { name: string; description?: string }) => void;
}
export default function RoleForm({ onSubmit }: RoleFormProps) {
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const form = useForm({
defaultValues: {
name: '',
description: '',
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSubmit({ name, description });
setName('');
setDescription('');
const handleSubmit = (data: { name: string; description?: string }) => {
onSubmit(data);
form.reset();
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1"></label>
<input
className="w-full border rounded px-3 py-2 focus:outline-none focus:ring focus:border-blue-300"
value={name}
onChange={e => setName(e.target.value)}
required
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="请输入角色名" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div>
<label className="block text-sm font-medium mb-1"></label>
<input
className="w-full border rounded px-3 py-2 focus:outline-none focus:ring focus:border-blue-300"
value={description}
onChange={e => setDescription(e.target.value)}
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="请输入描述" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex justify-end">
<Button type="submit"></Button>
</div>
</form>
<div className="flex justify-end">
<Button type="submit"></Button>
</div>
</form>
</Form>
);
}

108
src/CellularManagement.WebUI/src/pages/roles/RoleTable.tsx

@ -1,5 +1,13 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Role } from '@/services/roleService';
import { formatToBeijingTime } from '@/lib/utils';
@ -11,47 +19,79 @@ interface RoleTableProps {
onSetPermissions?: (role: Role) => void;
}
export default function RoleTable({ roles, loading, onDelete, onEdit, onSetPermissions }: RoleTableProps) {
export default function RoleTable({
roles,
loading,
onDelete,
onEdit,
onSetPermissions,
}: RoleTableProps) {
return (
<div className="bg-white rounded shadow">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<tr>
<td colSpan={5} className="text-center py-6">...</td>
</tr>
<TableRow>
<TableCell colSpan={5} className="text-center">
...
</TableCell>
</TableRow>
) : roles.length === 0 ? (
<tr>
<td colSpan={5} className="text-center py-6"></td>
</tr>
<TableRow>
<TableCell colSpan={5} className="text-center">
</TableCell>
</TableRow>
) : (
roles.map((role) => (
<tr key={role.id}>
<td className="px-6 py-4 whitespace-nowrap">{role.name}</td>
<td className="px-6 py-4 whitespace-nowrap">{role.description || '-'}</td>
<td className="px-6 py-4 whitespace-nowrap">-</td>
<td className="px-6 py-4 whitespace-nowrap">-</td>
<td className="px-6 py-4 whitespace-nowrap">{formatToBeijingTime(role.createdAt)}</td>
<td className="px-6 py-4 whitespace-nowrap text-left space-x-2">
<Button variant="outline" size="sm" onClick={() => onEdit?.(role)}></Button>
<Button variant="outline" size="sm" onClick={() => onSetPermissions?.(role)}></Button>
<Button variant="destructive" size="sm" onClick={() => onDelete(role.id)}></Button>
</td>
</tr>
<TableRow key={role.id}>
<TableCell>{role.name}</TableCell>
<TableCell>{role.description}</TableCell>
<TableCell>{formatToBeijingTime(role.createdAt)}</TableCell>
<TableCell>{formatToBeijingTime(role.updatedAt)}</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
{onEdit && (
<Button
variant="outline"
size="sm"
onClick={() => onEdit(role)}
>
</Button>
)}
{onSetPermissions && (
<Button
variant="outline"
size="sm"
onClick={() => onSetPermissions(role)}
>
</Button>
)}
<Button
variant="destructive"
size="sm"
onClick={() => onDelete(role.id)}
>
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</tbody>
</table>
</TableBody>
</Table>
</div>
);
}
Loading…
Cancel
Save