17 changed files with 925 additions and 88 deletions
@ -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
|
|||
} |
|||
} |
|||
} |
|||
@ -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"); |
|||
} |
|||
} |
|||
} |
|||
@ -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, |
|||
}; |
|||
@ -0,0 +1,6 @@ |
|||
export * from './button'; |
|||
export * from './input'; |
|||
export * from './form'; |
|||
export * from './label'; |
|||
export * from './table'; |
|||
export * from './dialog'; |
|||
@ -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 }; |
|||
@ -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 }; |
|||
@ -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, |
|||
}; |
|||
@ -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> |
|||
); |
|||
} |
|||
Loading…
Reference in new issue