feat(role): add CreateRoleDrawer component and integrate role creation functionality
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user