feat: move settings creation flows into dialogs
This commit is contained in:
@@ -13,6 +13,7 @@ import type { ApiResult } from "@/modules/core/server/api";
|
||||
import type { JoinRequest, Organization, PublicAccount, RoleDefinition } from "@/modules/core/types";
|
||||
|
||||
type UserManagementClientProps = {
|
||||
canManageAccounts?: boolean;
|
||||
organizations: Organization[];
|
||||
roles: RoleDefinition[];
|
||||
joinRequests: JoinRequest[];
|
||||
@@ -27,11 +28,13 @@ function getDefaultOrganizationId(organizations: Organization[]): string {
|
||||
}
|
||||
|
||||
export function UserManagementClient({
|
||||
canManageAccounts = false,
|
||||
organizations,
|
||||
roles,
|
||||
joinRequests,
|
||||
}: UserManagementClientProps): React.ReactElement {
|
||||
const router = useRouter();
|
||||
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
||||
const [selectedOrganizationId, setSelectedOrganizationId] = useState(() => getDefaultOrganizationId(organizations));
|
||||
const [message, setMessage] = useState("");
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
@@ -42,8 +45,26 @@ export function UserManagementClient({
|
||||
);
|
||||
const pendingJoinRequests = joinRequests.filter((request) => request.status === "pending");
|
||||
|
||||
function openCreateDialog(): void {
|
||||
setMessage("");
|
||||
setIsCreateOpen(true);
|
||||
}
|
||||
|
||||
function closeCreateDialog(): void {
|
||||
if (isPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCreateOpen(false);
|
||||
}
|
||||
|
||||
async function handleCreateAccount(event: FormEvent<HTMLFormElement>): Promise<void> {
|
||||
event.preventDefault();
|
||||
if (!canManageAccounts) {
|
||||
setMessage("当前角色无权创建用户");
|
||||
return;
|
||||
}
|
||||
|
||||
setMessage("");
|
||||
setIsPending(true);
|
||||
|
||||
@@ -68,11 +89,17 @@ export function UserManagementClient({
|
||||
}
|
||||
|
||||
setMessage(result.reason);
|
||||
setIsCreateOpen(false);
|
||||
event.currentTarget.reset();
|
||||
router.refresh();
|
||||
}
|
||||
|
||||
async function reviewJoinRequest(requestId: string, decision: "approved" | "rejected", roleId: string): Promise<void> {
|
||||
if (!canManageAccounts) {
|
||||
setMessage("当前角色无权审批加入申请");
|
||||
return;
|
||||
}
|
||||
|
||||
setMessage("");
|
||||
setIsPending(true);
|
||||
const response = await fetch(`/api/settings/join-requests/${requestId}`, {
|
||||
@@ -95,63 +122,37 @@ export function UserManagementClient({
|
||||
return (
|
||||
<section className="grid gap-4 xl:grid-cols-[0.9fr_1.1fr]">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>创建机构用户</CardTitle>
|
||||
<CardDescription>管理员创建账号后立即分配到机构和角色。</CardDescription>
|
||||
<CardHeader className="gap-4 md:flex-row md:items-start md:justify-between">
|
||||
<div>
|
||||
<CardTitle>用户操作</CardTitle>
|
||||
<CardDescription>创建账号和审批入口已收进弹窗,列表区域保持可扫描。</CardDescription>
|
||||
</div>
|
||||
<Button
|
||||
disabled={!canManageAccounts || organizations.length === 0}
|
||||
onClick={openCreateDialog}
|
||||
type="button"
|
||||
>
|
||||
<Plus className="size-4" aria-hidden="true" />
|
||||
创建用户
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="grid gap-3" onSubmit={handleCreateAccount}>
|
||||
<label className="grid gap-2 text-sm">
|
||||
<span className="font-medium">机构</span>
|
||||
<select
|
||||
className="h-11 w-full rounded-md border bg-background px-3 text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring"
|
||||
name="organizationId"
|
||||
onChange={(event) => setSelectedOrganizationId(event.target.value)}
|
||||
value={selectedOrganizationId}
|
||||
>
|
||||
{organizations.map((organization) => (
|
||||
<option key={organization.id} value={organization.id}>
|
||||
{organization.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="grid gap-2 text-sm">
|
||||
<span className="font-medium">角色</span>
|
||||
<select
|
||||
className="h-11 w-full rounded-md border bg-background px-3 text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring"
|
||||
name="roleId"
|
||||
required
|
||||
>
|
||||
{organizationRoles.map((role) => (
|
||||
<option key={role.id} value={role.id}>
|
||||
{role.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="grid gap-2 text-sm">
|
||||
<span className="font-medium">姓名</span>
|
||||
<Input name="name" required />
|
||||
</label>
|
||||
<label className="grid gap-2 text-sm">
|
||||
<span className="font-medium">邮箱</span>
|
||||
<Input name="email" required type="email" />
|
||||
</label>
|
||||
<label className="grid gap-2 text-sm">
|
||||
<span className="font-medium">初始密码</span>
|
||||
<Input minLength={6} name="password" required type="password" />
|
||||
</label>
|
||||
|
||||
<Button disabled={isPending || organizationRoles.length === 0} type="submit">
|
||||
<Plus className="size-4" aria-hidden="true" />
|
||||
创建用户
|
||||
</Button>
|
||||
</form>
|
||||
<CardContent className="grid gap-3">
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
<div className="rounded-md border bg-secondary/20 p-3">
|
||||
<p className="text-xs text-muted-foreground">可选机构</p>
|
||||
<p className="mt-1 text-2xl font-semibold">{organizations.length}</p>
|
||||
</div>
|
||||
<div className="rounded-md border bg-secondary/20 p-3">
|
||||
<p className="text-xs text-muted-foreground">当前机构角色</p>
|
||||
<p className="mt-1 text-2xl font-semibold">{organizationRoles.length}</p>
|
||||
</div>
|
||||
<div className="rounded-md border bg-secondary/20 p-3">
|
||||
<p className="text-xs text-muted-foreground">待审批</p>
|
||||
<p className="mt-1 text-2xl font-semibold">{pendingJoinRequests.length}</p>
|
||||
</div>
|
||||
</div>
|
||||
{message ? (
|
||||
<p className="mt-3 rounded-md bg-secondary px-3 py-2 text-sm text-secondary-foreground" role="status">
|
||||
<p className="rounded-md bg-secondary px-3 py-2 text-sm text-secondary-foreground" role="status">
|
||||
{message}
|
||||
</p>
|
||||
) : null}
|
||||
@@ -191,7 +192,7 @@ export function UserManagementClient({
|
||||
))}
|
||||
</select>
|
||||
<Button
|
||||
disabled={isPending || !defaultRoleId}
|
||||
disabled={isPending || !defaultRoleId || !canManageAccounts}
|
||||
onClick={() => {
|
||||
const element = document.getElementById(`role-${request.id}`);
|
||||
const roleId = element instanceof HTMLSelectElement ? element.value : defaultRoleId;
|
||||
@@ -204,7 +205,7 @@ export function UserManagementClient({
|
||||
通过
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isPending}
|
||||
disabled={isPending || !canManageAccounts}
|
||||
onClick={() => void reviewJoinRequest(request.id, "rejected", "")}
|
||||
size="sm"
|
||||
type="button"
|
||||
@@ -220,6 +221,76 @@ export function UserManagementClient({
|
||||
{pendingJoinRequests.length === 0 ? <p className="text-sm text-muted-foreground">暂无待审批申请</p> : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Dialog
|
||||
description="管理员创建账号后立即分配到机构和角色。"
|
||||
onClose={closeCreateDialog}
|
||||
open={isCreateOpen}
|
||||
title="创建机构用户"
|
||||
>
|
||||
<form className="grid gap-3" onSubmit={handleCreateAccount}>
|
||||
<label className="grid gap-2 text-sm">
|
||||
<span className="font-medium">机构</span>
|
||||
<select
|
||||
className="h-11 w-full rounded-md border bg-background px-3 text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring"
|
||||
name="organizationId"
|
||||
onChange={(event) => setSelectedOrganizationId(event.target.value)}
|
||||
value={selectedOrganizationId}
|
||||
>
|
||||
{organizations.map((organization) => (
|
||||
<option key={organization.id} value={organization.id}>
|
||||
{organization.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="grid gap-2 text-sm">
|
||||
<span className="font-medium">角色</span>
|
||||
<select
|
||||
className="h-11 w-full rounded-md border bg-background px-3 text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring"
|
||||
disabled={organizationRoles.length === 0}
|
||||
key={selectedOrganizationId}
|
||||
name="roleId"
|
||||
required
|
||||
>
|
||||
{organizationRoles.map((role) => (
|
||||
<option key={role.id} value={role.id}>
|
||||
{role.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="grid gap-2 text-sm">
|
||||
<span className="font-medium">姓名</span>
|
||||
<Input name="name" required />
|
||||
</label>
|
||||
<label className="grid gap-2 text-sm">
|
||||
<span className="font-medium">邮箱</span>
|
||||
<Input name="email" required type="email" />
|
||||
</label>
|
||||
<label className="grid gap-2 text-sm">
|
||||
<span className="font-medium">初始密码</span>
|
||||
<Input minLength={6} name="password" required type="password" />
|
||||
</label>
|
||||
|
||||
<div className="flex flex-col-reverse gap-2 pt-1 sm:flex-row sm:justify-end">
|
||||
<Button disabled={isPending} onClick={closeCreateDialog} type="button" variant="outline">
|
||||
取消
|
||||
</Button>
|
||||
<Button disabled={isPending || organizationRoles.length === 0} type="submit">
|
||||
<Plus className="size-4" aria-hidden="true" />
|
||||
创建用户
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
{message ? (
|
||||
<p className="mt-3 rounded-md bg-secondary px-3 py-2 text-sm text-secondary-foreground" role="status">
|
||||
{message}
|
||||
</p>
|
||||
) : null}
|
||||
</Dialog>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user