feat: adopt kumo green ui system

This commit is contained in:
2026-07-02 07:12:31 -07:00
parent 4bb4312b08
commit 0f0bd8813d
32 changed files with 1453 additions and 796 deletions

View File

@@ -7,6 +7,7 @@ import { useRouter } from "next/navigation";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import type { ApiResult } from "@/modules/core/server/api";
import type { SystemSettings } from "@/modules/core/types";
@@ -69,7 +70,7 @@ export function GlobalSettingsClient({ settings }: GlobalSettingsClientProps): R
<span className="block font-medium"></span>
<span className="block text-xs text-muted-foreground">使</span>
</span>
<input defaultChecked={settings.registrationEnabled} name="registrationEnabled" type="checkbox" />
<Checkbox aria-label="开放公开注册" defaultChecked={settings.registrationEnabled} name="registrationEnabled" />
</label>
<label className="flex items-center justify-between gap-4 rounded-md border p-3 text-sm">
@@ -77,7 +78,7 @@ export function GlobalSettingsClient({ settings }: GlobalSettingsClientProps): R
<span className="block font-medium"> OIDC </span>
<span className="block text-xs text-muted-foreground"> OIDC </span>
</span>
<input defaultChecked={settings.oidcEnabled} name="oidcEnabled" type="checkbox" />
<Checkbox aria-label="启用 OIDC 登录" defaultChecked={settings.oidcEnabled} name="oidcEnabled" />
</label>
<label className="flex items-center justify-between gap-4 rounded-md border p-3 text-sm">
@@ -85,7 +86,7 @@ export function GlobalSettingsClient({ settings }: GlobalSettingsClientProps): R
<span className="block font-medium">OIDC </span>
<span className="block text-xs text-muted-foreground"> OIDC </span>
</span>
<input defaultChecked={settings.oidcAutoProvision} name="oidcAutoProvision" type="checkbox" />
<Checkbox aria-label="OIDC 自动开户" defaultChecked={settings.oidcAutoProvision} name="oidcAutoProvision" />
</label>
</CardContent>
</Card>

View File

@@ -7,7 +7,9 @@ import { useRouter } from "next/navigation";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Table } from "@/components/ui/table";
import type { ApiResult } from "@/modules/core/server/api";
import type { Organization, OrganizationInvitation, RoleDefinition } from "@/modules/core/types";
import { getInviteHref, OrganizationInviteDialog } from "@/modules/settings/components/OrganizationInviteDialog";
@@ -74,7 +76,11 @@ export function OrganizationDetailClient({
<span className="block font-medium"></span>
<span className="block text-xs text-muted-foreground">使</span>
</span>
<input defaultChecked={organization.registrationEnabled} name="registrationEnabled" type="checkbox" />
<Checkbox
aria-label="开放公开注册"
defaultChecked={organization.registrationEnabled}
name="registrationEnabled"
/>
</label>
<label className="flex items-center justify-between gap-4 rounded-md border p-3 text-sm">
@@ -82,7 +88,7 @@ export function OrganizationDetailClient({
<span className="block font-medium"> OIDC </span>
<span className="block text-xs text-muted-foreground"> OIDC </span>
</span>
<input defaultChecked={organization.oidcEnabled} name="oidcEnabled" type="checkbox" />
<Checkbox aria-label="启用 OIDC 登录" defaultChecked={organization.oidcEnabled} name="oidcEnabled" />
</label>
<div className="grid gap-3 md:grid-cols-2">
@@ -126,7 +132,11 @@ export function OrganizationDetailClient({
<span className="block font-medium">OIDC </span>
<span className="block text-xs text-muted-foreground"> OIDC </span>
</span>
<input defaultChecked={organization.oidcAutoProvision} name="oidcAutoProvision" type="checkbox" />
<Checkbox
aria-label="OIDC 自动开户"
defaultChecked={organization.oidcAutoProvision}
name="oidcAutoProvision"
/>
</label>
{settingsMessage ? (
@@ -156,28 +166,28 @@ export function OrganizationDetailClient({
</CardHeader>
<CardContent className="grid gap-4">
<div className="overflow-x-auto rounded-md border">
<table className="w-full min-w-[720px] text-sm">
<thead className="bg-secondary text-left text-xs text-muted-foreground">
<tr>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 text-right font-medium"></th>
</tr>
</thead>
<tbody className="divide-y bg-card">
<Table className="w-full min-w-[720px] text-sm">
<Table.Header className="bg-secondary text-left text-xs text-muted-foreground">
<Table.Row>
<Table.Head className="px-4 py-3 font-medium"></Table.Head>
<Table.Head className="px-4 py-3 font-medium"></Table.Head>
<Table.Head className="px-4 py-3 font-medium"></Table.Head>
<Table.Head className="px-4 py-3 font-medium"></Table.Head>
<Table.Head className="px-4 py-3 text-right font-medium"></Table.Head>
</Table.Row>
</Table.Header>
<Table.Body className="divide-y bg-card">
{invitations.map((invitation) => (
<tr className="hover:bg-secondary/45" key={invitation.id}>
<td className="px-4 py-3">{invitation.email || "通用邀请"}</td>
<td className="px-4 py-3">{invitation.roleLabel}</td>
<td className="px-4 py-3">
<Table.Row className="hover:bg-secondary/45" key={invitation.id}>
<Table.Cell className="px-4 py-3">{invitation.email || "通用邀请"}</Table.Cell>
<Table.Cell className="px-4 py-3">{invitation.roleLabel}</Table.Cell>
<Table.Cell className="px-4 py-3">
<Badge variant={invitation.status === "active" ? "success" : "secondary"}>{invitation.status}</Badge>
</td>
<td className="px-4 py-3 text-muted-foreground">
</Table.Cell>
<Table.Cell className="px-4 py-3 text-muted-foreground">
{new Date(invitation.expiresAt).toLocaleString("zh-CN")}
</td>
<td className="px-4 py-3 text-right">
</Table.Cell>
<Table.Cell className="px-4 py-3 text-right">
<Button
onClick={() => void navigator.clipboard.writeText(getInviteHref(invitation.token))}
size="sm"
@@ -187,18 +197,18 @@ export function OrganizationDetailClient({
<Copy className="size-4" aria-hidden="true" />
</Button>
</td>
</tr>
</Table.Cell>
</Table.Row>
))}
{invitations.length === 0 ? (
<tr>
<td className="px-4 py-8 text-center text-muted-foreground" colSpan={5}>
<Table.Row>
<Table.Cell className="px-4 py-8 text-center text-muted-foreground" colSpan={5}>
</td>
</tr>
</Table.Cell>
</Table.Row>
) : null}
</tbody>
</table>
</Table.Body>
</Table>
</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<LinkIcon className="size-4" aria-hidden="true" />

View File

@@ -7,6 +7,7 @@ import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Dialog } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Select } from "@/components/ui/select";
import type { ApiResult } from "@/modules/core/server/api";
import type { Organization, OrganizationInvitation, RoleDefinition } from "@/modules/core/types";
@@ -91,20 +92,15 @@ export function OrganizationInviteDialog({
</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"
<Select
aria-label="邀请角色"
defaultValue={defaultRoleId}
disabled={organizationRoles.length === 0}
key={organization.id}
name="roleId"
options={organizationRoles.map((role) => ({ value: role.id, label: role.label }))}
required
>
{organizationRoles.map((role) => (
<option key={role.id} value={role.id}>
{role.label}
</option>
))}
</select>
/>
</label>
{message ? (
<p className="rounded-md bg-secondary px-3 py-2 text-sm text-secondary-foreground" role="status">

View File

@@ -7,6 +7,7 @@ import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Dialog } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Select } from "@/components/ui/select";
import type { ApiResult } from "@/modules/core/server/api";
import type { Organization, RoleDefinition } from "@/modules/core/types";
import { OrganizationInviteDialog } from "@/modules/settings/components/OrganizationInviteDialog";
@@ -204,14 +205,15 @@ export function OrganizationRowActions({
</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"
<Select
aria-label="机构状态"
defaultValue={organization.status}
name="status"
>
<option value="active"></option>
<option value="disabled"></option>
</select>
options={[
{ value: "active", label: "启用" },
{ value: "disabled", label: "停用" },
]}
/>
</label>
{message ? (
<p className="rounded-md bg-secondary px-3 py-2 text-sm text-secondary-foreground" role="status">

View File

@@ -6,8 +6,10 @@ import { useRouter } from "next/navigation";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Dialog } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Select } from "@/components/ui/select";
import type { ApiResult } from "@/modules/core/server/api";
import type { Permission, RoleDefinition } from "@/modules/core/types";
@@ -101,11 +103,10 @@ function PermissionTree({
<div className="rounded-md border bg-card" key={group.category}>
<label className="flex items-center justify-between gap-3 border-b px-3 py-3 text-sm">
<span className="flex min-w-0 items-center gap-3">
<input
<Checkbox
aria-label={`切换 ${group.category} 权限`}
checked={isAllSelected}
className="size-4 rounded border-input"
onChange={(event) => toggleCategory(group.permissions, event.target.checked)}
type="checkbox"
onCheckedChange={(checked) => toggleCategory(group.permissions, checked)}
/>
<span>
<span className="block font-medium">{group.category}</span>
@@ -122,11 +123,11 @@ function PermissionTree({
className="flex min-h-16 items-start gap-3 rounded-md border bg-background p-3 text-sm transition-colors hover:bg-secondary/50"
key={permission.id}
>
<input
<Checkbox
aria-label={`切换 ${permission.label}`}
checked={selectedSet.has(permission.id)}
className="mt-1 size-4 rounded border-input"
onChange={(event) => togglePermission(permission.id, event.target.checked)}
type="checkbox"
className="mt-1"
onCheckedChange={(checked) => togglePermission(permission.id, checked)}
/>
<span className="min-w-0">
<span className="block font-medium">{permission.label}</span>
@@ -366,14 +367,15 @@ export function RoleRowActions({ permissions, role }: RoleRowActionsProps): Reac
</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"
<Select
aria-label="角色状态"
defaultValue={String(role.isEnabled)}
name="isEnabled"
>
<option value="true"></option>
<option value="false"></option>
</select>
options={[
{ value: "true", label: "启用" },
{ value: "false", label: "停用" },
]}
/>
</label>
<PermissionTree
onChange={setSelectedPermissions}

View File

@@ -2,6 +2,7 @@ import { Activity, Building2, Server, ShieldCheck, Users } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Table } from "@/components/ui/table";
import type {
AuditLog,
JoinRequest,
@@ -191,39 +192,39 @@ export function SettingsOverview({
</CardHeader>
<CardContent>
<div className="overflow-x-auto rounded-md border">
<table className="w-full min-w-[560px] text-sm">
<thead className="bg-secondary text-left text-xs text-muted-foreground">
<tr>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium">Slug</th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 text-right font-medium"></th>
</tr>
</thead>
<tbody className="divide-y bg-card">
<Table className="w-full min-w-[560px] text-sm">
<Table.Header className="bg-secondary text-left text-xs text-muted-foreground">
<Table.Row>
<Table.Head className="px-4 py-3 font-medium"></Table.Head>
<Table.Head className="px-4 py-3 font-medium">Slug</Table.Head>
<Table.Head className="px-4 py-3 font-medium"></Table.Head>
<Table.Head className="px-4 py-3 text-right font-medium"></Table.Head>
</Table.Row>
</Table.Header>
<Table.Body className="divide-y bg-card">
{organizations.map((organization) => (
<tr key={organization.id}>
<td className="px-4 py-3 font-medium">{organization.name}</td>
<td className="px-4 py-3 text-muted-foreground">{organization.slug}</td>
<td className="px-4 py-3">
<Table.Row key={organization.id}>
<Table.Cell className="px-4 py-3 font-medium">{organization.name}</Table.Cell>
<Table.Cell className="px-4 py-3 text-muted-foreground">{organization.slug}</Table.Cell>
<Table.Cell className="px-4 py-3">
<Badge variant={organization.status === "active" ? "success" : "danger"}>
{organization.status === "active" ? "启用" : "停用"}
</Badge>
</td>
<td className="px-4 py-3 text-right text-muted-foreground">
</Table.Cell>
<Table.Cell className="px-4 py-3 text-right text-muted-foreground">
{new Date(organization.createdAt).toLocaleString("zh-CN")}
</td>
</tr>
</Table.Cell>
</Table.Row>
))}
{organizations.length === 0 ? (
<tr>
<td className="px-4 py-8 text-center text-muted-foreground" colSpan={4}>
<Table.Row>
<Table.Cell className="px-4 py-8 text-center text-muted-foreground" colSpan={4}>
</td>
</tr>
</Table.Cell>
</Table.Row>
) : null}
</tbody>
</table>
</Table.Body>
</Table>
</div>
</CardContent>
</Card>
@@ -285,35 +286,35 @@ export function SettingsOverview({
</CardHeader>
<CardContent>
<div className="overflow-x-auto rounded-md border">
<table className="w-full min-w-[620px] text-sm">
<thead className="bg-secondary text-left text-xs text-muted-foreground">
<tr>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 text-right font-medium"></th>
</tr>
</thead>
<tbody className="divide-y bg-card">
<Table className="w-full min-w-[620px] text-sm">
<Table.Header className="bg-secondary text-left text-xs text-muted-foreground">
<Table.Row>
<Table.Head className="px-4 py-3 font-medium"></Table.Head>
<Table.Head className="px-4 py-3 font-medium"></Table.Head>
<Table.Head className="px-4 py-3 font-medium"></Table.Head>
<Table.Head className="px-4 py-3 text-right font-medium"></Table.Head>
</Table.Row>
</Table.Header>
<Table.Body className="divide-y bg-card">
{accounts.map((account) => (
<tr key={account.id}>
<td className="px-4 py-3">
<Table.Row key={account.id}>
<Table.Cell className="px-4 py-3">
<p className="font-medium">{account.name}</p>
<p className="text-xs text-muted-foreground">{account.email}</p>
</td>
<td className="px-4 py-3">{formatRoleLabel(account.role)}</td>
<td className="px-4 py-3">
</Table.Cell>
<Table.Cell className="px-4 py-3">{formatRoleLabel(account.role)}</Table.Cell>
<Table.Cell className="px-4 py-3">
<Badge variant={account.status === "active" ? "success" : account.status === "pending" ? "warning" : "danger"}>
{account.status === "active" ? "启用" : account.status === "pending" ? "待审批" : "停用"}
</Badge>
</td>
<td className="px-4 py-3 text-right text-muted-foreground">
</Table.Cell>
<Table.Cell className="px-4 py-3 text-right text-muted-foreground">
{new Date(account.createdAt).toLocaleString("zh-CN")}
</td>
</tr>
</Table.Cell>
</Table.Row>
))}
</tbody>
</table>
</Table.Body>
</Table>
</div>
</CardContent>
</Card>
@@ -325,41 +326,41 @@ export function SettingsOverview({
</CardHeader>
<CardContent>
<div className="overflow-x-auto rounded-md border">
<table className="w-full min-w-[720px] text-sm">
<thead className="bg-secondary text-left text-xs text-muted-foreground">
<tr>
<th className="px-4 py-3 font-medium"> ID</th>
<th className="px-4 py-3 font-medium"> ID</th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 text-right font-medium"></th>
</tr>
</thead>
<tbody className="divide-y bg-card">
<Table className="w-full min-w-[720px] text-sm">
<Table.Header className="bg-secondary text-left text-xs text-muted-foreground">
<Table.Row>
<Table.Head className="px-4 py-3 font-medium"> ID</Table.Head>
<Table.Head className="px-4 py-3 font-medium"> ID</Table.Head>
<Table.Head className="px-4 py-3 font-medium"></Table.Head>
<Table.Head className="px-4 py-3 font-medium"></Table.Head>
<Table.Head className="px-4 py-3 text-right font-medium"></Table.Head>
</Table.Row>
</Table.Header>
<Table.Body className="divide-y bg-card">
{memberships.map((membership) => (
<tr key={membership.id}>
<td className="px-4 py-3 text-muted-foreground">{membership.accountId.slice(0, 8)}</td>
<td className="px-4 py-3 text-muted-foreground">{membership.organizationId.slice(0, 8)}</td>
<td className="px-4 py-3">{membership.roleLabel}</td>
<td className="px-4 py-3">
<Table.Row key={membership.id}>
<Table.Cell className="px-4 py-3 text-muted-foreground">{membership.accountId.slice(0, 8)}</Table.Cell>
<Table.Cell className="px-4 py-3 text-muted-foreground">{membership.organizationId.slice(0, 8)}</Table.Cell>
<Table.Cell className="px-4 py-3">{membership.roleLabel}</Table.Cell>
<Table.Cell className="px-4 py-3">
<Badge variant={membership.status === "active" ? "success" : membership.status === "pending" ? "warning" : "danger"}>
{membership.status}
</Badge>
</td>
<td className="px-4 py-3 text-right text-muted-foreground">
</Table.Cell>
<Table.Cell className="px-4 py-3 text-right text-muted-foreground">
{new Date(membership.createdAt).toLocaleString("zh-CN")}
</td>
</tr>
</Table.Cell>
</Table.Row>
))}
{memberships.length === 0 ? (
<tr>
<td className="px-4 py-8 text-center text-muted-foreground" colSpan={5}>
<Table.Row>
<Table.Cell className="px-4 py-8 text-center text-muted-foreground" colSpan={5}>
</td>
</tr>
</Table.Cell>
</Table.Row>
) : null}
</tbody>
</table>
</Table.Body>
</Table>
</div>
</CardContent>
</Card>
@@ -370,30 +371,30 @@ export function SettingsOverview({
</CardHeader>
<CardContent>
<div className="overflow-x-auto rounded-md border">
<table className="w-full min-w-[860px] text-sm">
<thead className="bg-secondary text-left text-xs text-muted-foreground">
<tr>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium"></th>
</tr>
</thead>
<tbody className="divide-y bg-card">
<Table className="w-full min-w-[860px] text-sm">
<Table.Header className="bg-secondary text-left text-xs text-muted-foreground">
<Table.Row>
<Table.Head className="px-4 py-3 font-medium"></Table.Head>
<Table.Head className="px-4 py-3 font-medium"></Table.Head>
<Table.Head className="px-4 py-3 font-medium"></Table.Head>
<Table.Head className="px-4 py-3 font-medium"></Table.Head>
<Table.Head className="px-4 py-3 font-medium"></Table.Head>
<Table.Head className="px-4 py-3 font-medium"></Table.Head>
</Table.Row>
</Table.Header>
<Table.Body className="divide-y bg-card">
{auditLogs.map((log) => (
<tr key={log.id}>
<td className="px-4 py-3 text-muted-foreground">
<Table.Row key={log.id}>
<Table.Cell className="px-4 py-3 text-muted-foreground">
{new Date(log.timestamp).toLocaleString("zh-CN")}
</td>
<td className="px-4 py-3">{log.actorEmail ?? "系统"}</td>
<td className="px-4 py-3">{log.action}</td>
<td className="px-4 py-3 text-muted-foreground">
</Table.Cell>
<Table.Cell className="px-4 py-3">{log.actorEmail ?? "系统"}</Table.Cell>
<Table.Cell className="px-4 py-3">{log.action}</Table.Cell>
<Table.Cell className="px-4 py-3 text-muted-foreground">
{log.targetType}
{log.targetId ? ` / ${log.targetId.slice(0, 8)}` : ""}
</td>
<td className="px-4 py-3">
</Table.Cell>
<Table.Cell className="px-4 py-3">
<Badge
variant={
log.result === "success" ? "success" : log.result === "denied" ? "warning" : "danger"
@@ -401,19 +402,19 @@ export function SettingsOverview({
>
{log.result}
</Badge>
</td>
<td className="px-4 py-3 text-muted-foreground">{log.reason}</td>
</tr>
</Table.Cell>
<Table.Cell className="px-4 py-3 text-muted-foreground">{log.reason}</Table.Cell>
</Table.Row>
))}
{auditLogs.length === 0 ? (
<tr>
<td className="px-4 py-8 text-center text-muted-foreground" colSpan={6}>
<Table.Row>
<Table.Cell className="px-4 py-8 text-center text-muted-foreground" colSpan={6}>
</td>
</tr>
</Table.Cell>
</Table.Row>
) : null}
</tbody>
</table>
</Table.Body>
</Table>
</div>
</CardContent>
</Card>

View File

@@ -1,6 +1,4 @@
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Button, LinkButton } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { buildPageHref } from "@/modules/settings/lib/pagination";
@@ -39,9 +37,9 @@ export function TableToolbar({
</Button>
) : (
<Button asChild size="sm" variant="outline">
<Link href={buildPageHref(pathname, query, page - 1)}></Link>
</Button>
<LinkButton href={buildPageHref(pathname, query, page - 1)} size="sm" variant="outline">
</LinkButton>
)}
<span className="min-w-16 text-center">
{page}/{pageCount}
@@ -51,9 +49,9 @@ export function TableToolbar({
</Button>
) : (
<Button asChild size="sm" variant="outline">
<Link href={buildPageHref(pathname, query, page + 1)}></Link>
</Button>
<LinkButton href={buildPageHref(pathname, query, page + 1)} size="sm" variant="outline">
</LinkButton>
)}
</div>
</div>

View File

@@ -9,6 +9,7 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Dialog } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Select } from "@/components/ui/select";
import type { ApiResult } from "@/modules/core/server/api";
import type { JoinRequest, Organization, PublicAccount, RoleDefinition } from "@/modules/core/types";
@@ -36,6 +37,7 @@ export function UserManagementClient({
const router = useRouter();
const [isCreateOpen, setIsCreateOpen] = useState(false);
const [selectedOrganizationId, setSelectedOrganizationId] = useState(() => getDefaultOrganizationId(organizations));
const [reviewRoleByRequestId, setReviewRoleByRequestId] = useState<Record<string, string>>({});
const [message, setMessage] = useState("");
const [isPending, setIsPending] = useState(false);
@@ -169,6 +171,7 @@ export function UserManagementClient({
{pendingJoinRequests.map((request) => {
const requestRoles = getOrganizationRoles(roles, request.organizationId);
const defaultRoleId = requestRoles.find((role) => role.key === "caregiver")?.id ?? requestRoles[0]?.id ?? "";
const selectedRoleId = reviewRoleByRequestId[request.id] ?? defaultRoleId;
return (
<div key={request.id} className="rounded-md border p-3">
@@ -181,24 +184,18 @@ export function UserManagementClient({
<Badge variant="warning"></Badge>
</div>
<div className="mt-3 grid gap-2 sm:grid-cols-[1fr_auto_auto]">
<select
className="h-10 w-full rounded-md border bg-background px-3 text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring"
defaultValue={defaultRoleId}
id={`role-${request.id}`}
>
{requestRoles.map((role) => (
<option key={role.id} value={role.id}>
{role.label}
</option>
))}
</select>
<Select
aria-label={`${request.accountName} 审批角色`}
disabled={requestRoles.length === 0}
onValueChange={(roleId) =>
setReviewRoleByRequestId((current) => ({ ...current, [request.id]: roleId }))
}
options={requestRoles.map((role) => ({ value: role.id, label: role.label }))}
value={selectedRoleId}
/>
<Button
disabled={isPending || !defaultRoleId || !canManageAccounts}
onClick={() => {
const element = document.getElementById(`role-${request.id}`);
const roleId = element instanceof HTMLSelectElement ? element.value : defaultRoleId;
void reviewJoinRequest(request.id, "approved", roleId);
}}
disabled={isPending || !selectedRoleId || !canManageAccounts}
onClick={() => void reviewJoinRequest(request.id, "approved", selectedRoleId)}
size="sm"
type="button"
>
@@ -232,35 +229,26 @@ export function UserManagementClient({
<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"
<Select
aria-label="机构"
name="organizationId"
onChange={(event) => setSelectedOrganizationId(event.target.value)}
onValueChange={setSelectedOrganizationId}
options={organizations.map((organization) => ({ value: organization.id, label: organization.name }))}
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"
<Select
aria-label="角色"
defaultValue={organizationRoles[0]?.id ?? ""}
disabled={organizationRoles.length === 0}
key={selectedOrganizationId}
name="roleId"
options={organizationRoles.map((role) => ({ value: role.id, label: role.label }))}
required
>
{organizationRoles.map((role) => (
<option key={role.id} value={role.id}>
{role.label}
</option>
))}
</select>
/>
</label>
<label className="grid gap-2 text-sm">
@@ -424,47 +412,38 @@ export function UserAccountActions({
</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"
<Select
aria-label="账号状态"
defaultValue={account.status}
disabled={isCurrentAccount}
name="status"
>
<option value="active"></option>
<option value="disabled"></option>
</select>
options={[
{ value: "active", label: "启用" },
{ value: "disabled", label: "停用" },
]}
/>
</label>
<div className="grid gap-3 md:grid-cols-2">
<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"
<Select
aria-label="账号机构"
name="organizationId"
onChange={(event) => setSelectedOrganizationId(event.target.value)}
onValueChange={setSelectedOrganizationId}
options={organizations.map((organization) => ({ value: organization.id, label: organization.name }))}
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"
<Select
aria-label="账号角色"
defaultValue={defaultRoleId}
disabled={organizationRoles.length === 0}
key={selectedOrganizationId}
name="roleId"
>
{organizationRoles.map((role) => (
<option key={role.id} value={role.id}>
{role.label}
</option>
))}
</select>
options={organizationRoles.map((role) => ({ value: role.id, label: role.label }))}
/>
</label>
</div>
{message ? (