feat(role): add CreateRoleDrawer component and integrate role creation functionality

This commit is contained in:
mlogclub
2026-04-23 14:05:13 +08:00
parent a769df5d44
commit e616169c41
4 changed files with 274 additions and 3 deletions
+3 -3
View File
@@ -1,5 +1,5 @@
.PHONY: help install run run-server run-dashboard run-widget build build-all build-assets build-dashboard build-widget \
package-current package-platform build-server-linux-amd64 clean-dist clean-temp generator enums migration testdata
package-current package-platform build-linux-amd64 clean-dist clean-temp generator enums migration testdata
DIST_DIR ?= dist
TMP_DIR := $(DIST_DIR)/.tmp
@@ -28,7 +28,7 @@ help:
@echo " make build-widget Build widget sdk and app"
@echo " make package-current Package current platform"
@echo " make package-platform Package specified PLATFORM"
@echo " make build-server-linux-amd64 Build linux amd64 server binary"
@echo " make build-linux-amd64 Build and package linux amd64 release"
@echo " make clean-dist Remove dist directory"
@echo " make clean-temp Remove temporary packaging files"
@echo " make generator Run code generator"
@@ -103,7 +103,7 @@ package-current:
build-linux-amd64: clean-dist build-assets
@$(MAKE) package-platform PLATFORM=linux-amd64
@$(MAKE) clean-temp
@echo "[build-server-linux-amd64] done"
@echo "[build-linux-amd64] done"
package-platform:
@platform="$(PLATFORM)"; \
@@ -0,0 +1,208 @@
"use client"
import { Resolver, useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod/v4"
import { type CreateAdminRolePayload } from "@/lib/api/admin"
import { Button } from "@/components/ui/button"
import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
} from "@/components/ui/drawer"
import {
Field,
FieldContent,
FieldError,
FieldLabel,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
type CreateRoleDrawerProps = {
open: boolean
saving: boolean
defaultSortNo: number
onOpenChange: (open: boolean) => void
onSubmit: (payload: CreateAdminRolePayload) => Promise<void>
}
const createFormSchema = z.object({
name: z.string().trim().min(1, "角色名称不能为空"),
code: z
.string()
.trim()
.min(1, "角色编码不能为空")
.regex(/^[A-Za-z][A-Za-z0-9:_-]*$/, "角色编码需以字母开头,仅支持字母、数字、冒号、下划线和短横线"),
sortNo: z
.string()
.trim()
.min(1, "排序不能为空")
.regex(/^\d+$/, "排序值必须是大于等于 0 的整数"),
remark: z.string().trim(),
})
type CreateForm = z.infer<typeof createFormSchema>
const createFormResolver = zodResolver(createFormSchema as never) as Resolver<
z.input<typeof createFormSchema>,
undefined,
z.output<typeof createFormSchema>
>
function buildEmptyForm(defaultSortNo: number): CreateForm {
return {
name: "",
code: "",
sortNo: String(defaultSortNo),
remark: "",
}
}
function buildPayload(form: CreateForm): CreateAdminRolePayload {
return {
name: form.name.trim(),
code: form.code.trim(),
sortNo: Number(form.sortNo),
remark: form.remark.trim(),
}
}
export function CreateRoleDrawer({
open,
saving,
defaultSortNo,
onOpenChange,
onSubmit,
}: CreateRoleDrawerProps) {
return (
<Drawer open={open} onOpenChange={onOpenChange} direction="right">
{open ? (
<CreateRoleDrawerBody
key={`create-role-${defaultSortNo}`}
saving={saving}
defaultSortNo={defaultSortNo}
onOpenChange={onOpenChange}
onSubmit={onSubmit}
/>
) : null}
</Drawer>
)
}
type CreateRoleDrawerBodyProps = {
saving: boolean
defaultSortNo: number
onOpenChange: (open: boolean) => void
onSubmit: (payload: CreateAdminRolePayload) => Promise<void>
}
function CreateRoleDrawerBody({
saving,
defaultSortNo,
onOpenChange,
onSubmit,
}: CreateRoleDrawerBodyProps) {
const form = useForm<
z.input<typeof createFormSchema>,
undefined,
z.output<typeof createFormSchema>
>({
resolver: createFormResolver,
defaultValues: buildEmptyForm(defaultSortNo),
})
const {
handleSubmit,
register,
reset,
formState: { errors },
} = form
async function onFormSubmit(values: CreateForm) {
await onSubmit(buildPayload(values))
reset(buildEmptyForm(defaultSortNo))
}
return (
<DrawerContent className="min-w-2xl">
<DrawerHeader>
<DrawerTitle></DrawerTitle>
<DrawerDescription></DrawerDescription>
</DrawerHeader>
<form
className="flex h-full flex-col"
onSubmit={handleSubmit(onFormSubmit)}
>
<div className="space-y-4 overflow-y-auto px-4 pb-4">
<Field data-invalid={!!errors.name}>
<FieldLabel htmlFor="create-role-name"></FieldLabel>
<FieldContent>
<Input
id="create-role-name"
placeholder="例如:客服主管"
autoComplete="off"
aria-invalid={!!errors.name}
{...register("name")}
/>
<FieldError errors={[errors.name]} />
</FieldContent>
</Field>
<Field data-invalid={!!errors.code}>
<FieldLabel htmlFor="create-role-code"></FieldLabel>
<FieldContent>
<Input
id="create-role-code"
placeholder="例如:support_manager"
autoComplete="off"
aria-invalid={!!errors.code}
{...register("code")}
/>
<FieldError errors={[errors.code]} />
</FieldContent>
</Field>
<Field data-invalid={!!errors.sortNo}>
<FieldLabel htmlFor="create-role-sort-no"></FieldLabel>
<FieldContent>
<Input
id="create-role-sort-no"
inputMode="numeric"
aria-invalid={!!errors.sortNo}
{...register("sortNo")}
/>
<FieldError errors={[errors.sortNo]} />
</FieldContent>
</Field>
<Field data-invalid={!!errors.remark}>
<FieldLabel htmlFor="create-role-remark"></FieldLabel>
<FieldContent>
<Textarea
id="create-role-remark"
placeholder="可选"
aria-invalid={!!errors.remark}
{...register("remark")}
/>
<FieldError errors={[errors.remark]} />
</FieldContent>
</Field>
</div>
<DrawerFooter className="border-t">
<Button type="submit" disabled={saving}>
{saving ? "创建中..." : "创建角色"}
</Button>
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={saving}
>
</Button>
</DrawerFooter>
</form>
</DrawerContent>
)
}
+49
View File
@@ -22,6 +22,7 @@ import {
import { CSS } from "@dnd-kit/utilities"
import {
GripVerticalIcon,
PlusIcon,
RefreshCwIcon,
ShieldCheckIcon,
ShieldIcon,
@@ -30,16 +31,19 @@ import { toast } from "sonner"
import {
assignRolePermissions,
createRole,
fetchPermissions,
fetchRoleDetail,
fetchRoles,
type AdminPermission,
type AdminRole,
type CreateAdminRolePayload,
type PageResult,
updateRoleSort,
} from "@/lib/api/admin"
import { cn } from "@/lib/utils"
import { AssignPermissionsDrawer } from "./_components/assign-permissions"
import { CreateRoleDrawer } from "./_components/create"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
@@ -148,6 +152,8 @@ function SortableRoleRow({
export default function DashboardRolesPage() {
const [loading, setLoading] = useState(true)
const [sorting, setSorting] = useState(false)
const [creatingOpen, setCreatingOpen] = useState(false)
const [savingCreate, setSavingCreate] = useState(false)
const [savingPermissions, setSavingPermissions] = useState(false)
const [assignPermissionsLoading, setAssignPermissionsLoading] = useState(false)
const [assigningRole, setAssigningRole] = useState<AdminRole | null>(null)
@@ -184,6 +190,31 @@ export default function DashboardRolesPage() {
}
}
function handleCreateDrawerOpenChange(open: boolean) {
if (savingCreate) {
return
}
setCreatingOpen(open)
}
async function handleCreateRole(payload: CreateAdminRolePayload) {
if (savingCreate) {
return
}
setSavingCreate(true)
try {
const role = await createRole(payload)
toast.success(`已创建角色 ${role.name}`)
setCreatingOpen(false)
await loadRoles()
} catch (error) {
toast.error(error instanceof Error ? error.message : "创建角色失败")
} finally {
setSavingCreate(false)
}
}
async function openAssignPermissionsDrawer(role: AdminRole) {
setActionLoadingId(role.id)
setAssigningRole(role)
@@ -279,9 +310,20 @@ export default function DashboardRolesPage() {
void loadRoles()
}, [])
const defaultSortNo =
result.results.reduce((max, item) => Math.max(max, item.sortNo), -1) + 1
return (
<div className="flex flex-1 flex-col gap-6 p-4 lg:p-6">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-end">
<Button
type="button"
onClick={() => setCreatingOpen(true)}
disabled={loading || sorting}
>
<PlusIcon className="size-4" />
</Button>
<Button onClick={() => void loadRoles()} disabled={loading || sorting}>
<RefreshCwIcon className={cn((loading || sorting) && "animate-spin")} />
@@ -353,6 +395,13 @@ export default function DashboardRolesPage() {
onOpenChange={handleAssignPermissionsOpenChange}
onSubmit={handleAssignPermissions}
/>
<CreateRoleDrawer
open={creatingOpen}
saving={savingCreate}
defaultSortNo={defaultSortNo}
onOpenChange={handleCreateDrawerOpenChange}
onSubmit={handleCreateRole}
/>
</div>
)
}
+14
View File
@@ -72,6 +72,13 @@ export type AdminRole = {
permissions?: string[]
}
export type CreateAdminRolePayload = {
name: string
code: string
sortNo: number
remark: string
}
export type AdminPermission = {
id: number
name: string
@@ -712,6 +719,13 @@ export function fetchRoleDetail(id: number) {
return request<AdminRole>(`/api/dashboard/role/${id}`)
}
export function createRole(payload: CreateAdminRolePayload) {
return request<AdminRole>("/api/dashboard/role/create", {
method: "POST",
body: JSON.stringify(payload),
})
}
export function assignRolePermissions(roleId: number, permissionIds: number[]) {
return request<void>("/api/dashboard/role/assign_permission", {
method: "POST",