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 \
|
.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
|
DIST_DIR ?= dist
|
||||||
TMP_DIR := $(DIST_DIR)/.tmp
|
TMP_DIR := $(DIST_DIR)/.tmp
|
||||||
@@ -28,7 +28,7 @@ help:
|
|||||||
@echo " make build-widget Build widget sdk and app"
|
@echo " make build-widget Build widget sdk and app"
|
||||||
@echo " make package-current Package current platform"
|
@echo " make package-current Package current platform"
|
||||||
@echo " make package-platform Package specified 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-dist Remove dist directory"
|
||||||
@echo " make clean-temp Remove temporary packaging files"
|
@echo " make clean-temp Remove temporary packaging files"
|
||||||
@echo " make generator Run code generator"
|
@echo " make generator Run code generator"
|
||||||
@@ -103,7 +103,7 @@ package-current:
|
|||||||
build-linux-amd64: clean-dist build-assets
|
build-linux-amd64: clean-dist build-assets
|
||||||
@$(MAKE) package-platform PLATFORM=linux-amd64
|
@$(MAKE) package-platform PLATFORM=linux-amd64
|
||||||
@$(MAKE) clean-temp
|
@$(MAKE) clean-temp
|
||||||
@echo "[build-server-linux-amd64] done"
|
@echo "[build-linux-amd64] done"
|
||||||
|
|
||||||
package-platform:
|
package-platform:
|
||||||
@platform="$(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 { CSS } from "@dnd-kit/utilities"
|
||||||
import {
|
import {
|
||||||
GripVerticalIcon,
|
GripVerticalIcon,
|
||||||
|
PlusIcon,
|
||||||
RefreshCwIcon,
|
RefreshCwIcon,
|
||||||
ShieldCheckIcon,
|
ShieldCheckIcon,
|
||||||
ShieldIcon,
|
ShieldIcon,
|
||||||
@@ -30,16 +31,19 @@ import { toast } from "sonner"
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
assignRolePermissions,
|
assignRolePermissions,
|
||||||
|
createRole,
|
||||||
fetchPermissions,
|
fetchPermissions,
|
||||||
fetchRoleDetail,
|
fetchRoleDetail,
|
||||||
fetchRoles,
|
fetchRoles,
|
||||||
type AdminPermission,
|
type AdminPermission,
|
||||||
type AdminRole,
|
type AdminRole,
|
||||||
|
type CreateAdminRolePayload,
|
||||||
type PageResult,
|
type PageResult,
|
||||||
updateRoleSort,
|
updateRoleSort,
|
||||||
} from "@/lib/api/admin"
|
} from "@/lib/api/admin"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { AssignPermissionsDrawer } from "./_components/assign-permissions"
|
import { AssignPermissionsDrawer } from "./_components/assign-permissions"
|
||||||
|
import { CreateRoleDrawer } from "./_components/create"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
@@ -148,6 +152,8 @@ function SortableRoleRow({
|
|||||||
export default function DashboardRolesPage() {
|
export default function DashboardRolesPage() {
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [sorting, setSorting] = useState(false)
|
const [sorting, setSorting] = useState(false)
|
||||||
|
const [creatingOpen, setCreatingOpen] = useState(false)
|
||||||
|
const [savingCreate, setSavingCreate] = useState(false)
|
||||||
const [savingPermissions, setSavingPermissions] = useState(false)
|
const [savingPermissions, setSavingPermissions] = useState(false)
|
||||||
const [assignPermissionsLoading, setAssignPermissionsLoading] = useState(false)
|
const [assignPermissionsLoading, setAssignPermissionsLoading] = useState(false)
|
||||||
const [assigningRole, setAssigningRole] = useState<AdminRole | null>(null)
|
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) {
|
async function openAssignPermissionsDrawer(role: AdminRole) {
|
||||||
setActionLoadingId(role.id)
|
setActionLoadingId(role.id)
|
||||||
setAssigningRole(role)
|
setAssigningRole(role)
|
||||||
@@ -279,9 +310,20 @@ export default function DashboardRolesPage() {
|
|||||||
void loadRoles()
|
void loadRoles()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const defaultSortNo =
|
||||||
|
result.results.reduce((max, item) => Math.max(max, item.sortNo), -1) + 1
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 flex-col gap-6 p-4 lg:p-6">
|
<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">
|
<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}>
|
<Button onClick={() => void loadRoles()} disabled={loading || sorting}>
|
||||||
<RefreshCwIcon className={cn((loading || sorting) && "animate-spin")} />
|
<RefreshCwIcon className={cn((loading || sorting) && "animate-spin")} />
|
||||||
刷新列表
|
刷新列表
|
||||||
@@ -353,6 +395,13 @@ export default function DashboardRolesPage() {
|
|||||||
onOpenChange={handleAssignPermissionsOpenChange}
|
onOpenChange={handleAssignPermissionsOpenChange}
|
||||||
onSubmit={handleAssignPermissions}
|
onSubmit={handleAssignPermissions}
|
||||||
/>
|
/>
|
||||||
|
<CreateRoleDrawer
|
||||||
|
open={creatingOpen}
|
||||||
|
saving={savingCreate}
|
||||||
|
defaultSortNo={defaultSortNo}
|
||||||
|
onOpenChange={handleCreateDrawerOpenChange}
|
||||||
|
onSubmit={handleCreateRole}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,13 @@ export type AdminRole = {
|
|||||||
permissions?: string[]
|
permissions?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CreateAdminRolePayload = {
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
sortNo: number
|
||||||
|
remark: string
|
||||||
|
}
|
||||||
|
|
||||||
export type AdminPermission = {
|
export type AdminPermission = {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
@@ -712,6 +719,13 @@ export function fetchRoleDetail(id: number) {
|
|||||||
return request<AdminRole>(`/api/dashboard/role/${id}`)
|
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[]) {
|
export function assignRolePermissions(roleId: number, permissionIds: number[]) {
|
||||||
return request<void>("/api/dashboard/role/assign_permission", {
|
return request<void>("/api/dashboard/role/assign_permission", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
Reference in New Issue
Block a user