refactor: align system admin forms with shadcn fields

This commit is contained in:
hailin
2026-07-02 15:42:43 +08:00
parent 2a63f97f35
commit be7edae94f
4 changed files with 139 additions and 118 deletions

View File

@@ -2,6 +2,7 @@ import * as React from 'react'
import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable' import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable'
import { AppEmptyState } from '@/components/app/AppEmptyState' import { AppEmptyState } from '@/components/app/AppEmptyState'
import { AppFilterBar } from '@/components/app/AppFilterBar'
import { AppPage } from '@/components/app/AppPage' import { AppPage } from '@/components/app/AppPage'
import { AppPageHeader } from '@/components/app/AppPageHeader' import { AppPageHeader } from '@/components/app/AppPageHeader'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
@@ -80,22 +81,22 @@ export function SystemMenusPage() {
return ( return (
<AppPage> <AppPage>
<AppPageHeader title="菜单权限" /> <AppPageHeader title="菜单权限" />
<div className="rounded-lg border border-border bg-surface p-3"> <AppFilterBar>
<Input placeholder="搜索菜单、路径或权限码" value={query} onChange={(event) => setQuery(event.target.value)} /> <Input placeholder="搜索菜单、路径或权限码" value={query} onChange={(event) => setQuery(event.target.value)} />
</div> </AppFilterBar>
{error ? <AppEmptyState title="无法加载菜单" description={error} /> : null} {error ? <AppEmptyState title="无法加载菜单" description={error} /> : null}
{!error ? ( {!error ? (
<AppDataTable <AppDataTable
columns={columns} columns={columns}
rows={filteredMenus} rows={filteredMenus}
getRowKey={(menu) => menu.id} getRowKey={(menu) => menu.id}
emptyTitle="暂无菜单" emptyTitle="暂无菜单"
footer={<p className="text-sm text-muted-foreground">当前显示 {filteredMenus.length} 个菜单项</p>} footer={<p className="text-sm text-muted-foreground">当前显示 {filteredMenus.length} 个菜单项</p>}
/> />
) : null} ) : null}
</AppPage> </AppPage>
) )
} }

View File

@@ -5,10 +5,12 @@ import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataT
import { AppEmptyState } from '@/components/app/AppEmptyState' import { AppEmptyState } from '@/components/app/AppEmptyState'
import { AppPage } from '@/components/app/AppPage' import { AppPage } from '@/components/app/AppPage'
import { AppPageHeader } from '@/components/app/AppPageHeader' import { AppPageHeader } from '@/components/app/AppPageHeader'
import { AppSection } from '@/components/app/AppSection'
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 { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Field, FieldGroup, FieldLabel, FieldLegend, FieldSet } from '@/components/ui/field'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { createRole, listPermissions, listRoles, setRolePermissions } from './api' import { createRole, listPermissions, listRoles, setRolePermissions } from './api'
@@ -73,31 +75,35 @@ export function SystemRolesPage() {
return ( return (
<AppPage> <AppPage>
<AppPageHeader title="角色权限" /> <AppPageHeader title="角色权限" />
<form className="grid gap-3 rounded-lg border border-border bg-surface p-3 md:grid-cols-[minmax(0,360px)_auto] md:items-end" onSubmit={handleCreate}> <AppSection title="新增角色">
<AppField label="角色名称" htmlFor="system-role-name" required> <form onSubmit={handleCreate}>
<Input id="system-role-name" value={roleName} onChange={(event) => setRoleName(event.target.value)} required /> <FieldGroup className="grid gap-3 md:grid-cols-[minmax(0,360px)_auto] md:items-end">
</AppField> <AppField label="角色名称" htmlFor="system-role-name" required>
<Button type="submit" className="w-full md:w-auto">创建角色</Button> <Input id="system-role-name" value={roleName} onChange={(event) => setRoleName(event.target.value)} required />
</AppField>
<Button type="submit" className="w-full md:w-auto">创建角色</Button>
</FieldGroup>
</form> </form>
</AppSection>
{error ? <AppEmptyState title="无法加载角色" description={error} /> : null} {error ? <AppEmptyState title="无法加载角色" description={error} /> : null}
{!error ? ( {!error ? (
<AppDataTable <AppDataTable
columns={columns} columns={columns}
rows={roles} rows={roles}
getRowKey={(role) => role.id} getRowKey={(role) => role.id}
emptyTitle="暂无角色" emptyTitle="暂无角色"
/>
) : null}
<RolePermissionDialog
role={editingRole}
permissions={permissions}
onClose={() => setEditingRole(null)}
onSaved={load}
/> />
) : null}
<RolePermissionDialog
role={editingRole}
permissions={permissions}
onClose={() => setEditingRole(null)}
onSaved={load}
/>
</AppPage> </AppPage>
) )
} }
@@ -142,12 +148,13 @@ function RolePermissionDialog({
</DialogHeader> </DialogHeader>
<div className="grid max-h-[56vh] gap-4 overflow-auto pr-1"> <div className="grid max-h-[56vh] gap-4 overflow-auto pr-1">
{Object.entries(groupedPermissions).map(([module, modulePermissions]) => ( {Object.entries(groupedPermissions).map(([module, modulePermissions]) => (
<section key={module} className="rounded-md border border-border p-3"> <FieldSet key={module} className="rounded-md border border-border p-3">
<p className="mb-3 text-sm font-medium text-foreground">{module}</p> <FieldLegend variant="label">{module}</FieldLegend>
<div className="grid gap-2 md:grid-cols-2"> <FieldGroup data-slot="checkbox-group" className="grid gap-2 md:grid-cols-2">
{modulePermissions.map((permission) => ( {modulePermissions.map((permission) => (
<label key={permission.codename} className="flex items-start gap-2 text-sm"> <Field key={permission.codename} orientation="horizontal">
<Checkbox <Checkbox
id={`role-permission-${permission.codename}`}
checked={selected.includes(permission.codename)} checked={selected.includes(permission.codename)}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
setSelected((current) => setSelected((current) =>
@@ -157,14 +164,14 @@ function RolePermissionDialog({
) )
} }
/> />
<span> <FieldLabel htmlFor={`role-permission-${permission.codename}`} className="flex-col items-start gap-0.5">
<span className="block text-foreground">{permission.name}</span> <span className="block text-foreground">{permission.name}</span>
<span className="text-xs text-muted-foreground">{permission.codename}</span> <span className="text-xs text-muted-foreground">{permission.codename}</span>
</span> </FieldLabel>
</label> </Field>
))} ))}
</div> </FieldGroup>
</section> </FieldSet>
))} ))}
</div> </div>
<DialogFooter> <DialogFooter>

View File

@@ -5,8 +5,10 @@ import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataT
import { AppEmptyState } from '@/components/app/AppEmptyState' import { AppEmptyState } from '@/components/app/AppEmptyState'
import { AppPage } from '@/components/app/AppPage' import { AppPage } from '@/components/app/AppPage'
import { AppPageHeader } from '@/components/app/AppPageHeader' import { AppPageHeader } from '@/components/app/AppPageHeader'
import { AppSection } from '@/components/app/AppSection'
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 { FieldGroup } from '@/components/ui/field'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { formatDateTime } from '@/lib/format' import { formatDateTime } from '@/lib/format'
@@ -81,31 +83,35 @@ export function SystemUserGroupsPage() {
return ( return (
<AppPage> <AppPage>
<AppPageHeader title="用户组管理" /> <AppPageHeader title="用户组管理" />
<form className="grid gap-3 rounded-lg border border-border bg-surface p-3 md:grid-cols-[1fr_1fr_2fr_auto] md:items-end" onSubmit={handleCreate}> <AppSection title="新增用户组">
<AppField label="用户组名称" htmlFor="system-group-name" required> <form onSubmit={handleCreate}>
<Input id="system-group-name" value={form.name} onChange={(event) => setForm({ ...form, name: event.target.value })} required /> <FieldGroup className="grid gap-3 md:grid-cols-[1fr_1fr_2fr_auto] md:items-end">
</AppField> <AppField label="用户组名称" htmlFor="system-group-name" required>
<AppField label="编码" htmlFor="system-group-code" required> <Input id="system-group-name" value={form.name} onChange={(event) => setForm({ ...form, name: event.target.value })} required />
<Input id="system-group-code" value={form.code} onChange={(event) => setForm({ ...form, code: event.target.value })} required /> </AppField>
</AppField> <AppField label="编码" htmlFor="system-group-code" required>
<AppField label="说明" htmlFor="system-group-description"> <Input id="system-group-code" value={form.code} onChange={(event) => setForm({ ...form, code: event.target.value })} required />
<Input id="system-group-description" value={form.description} onChange={(event) => setForm({ ...form, description: event.target.value })} /> </AppField>
</AppField> <AppField label="说明" htmlFor="system-group-description">
<Button type="submit" className="w-full md:w-auto">创建用户组</Button> <Input id="system-group-description" value={form.description} onChange={(event) => setForm({ ...form, description: event.target.value })} />
</AppField>
<Button type="submit" className="w-full md:w-auto">创建用户组</Button>
</FieldGroup>
</form> </form>
</AppSection>
{error ? <AppEmptyState title="无法加载用户组" description={error} /> : null} {error ? <AppEmptyState title="无法加载用户组" description={error} /> : null}
{!error ? ( {!error ? (
<AppDataTable <AppDataTable
columns={columns} columns={columns}
rows={groups} rows={groups}
getRowKey={(group) => group.id} getRowKey={(group) => group.id}
emptyTitle="暂无用户组" emptyTitle="暂无用户组"
footer={<p className="text-sm text-muted-foreground">共 {groups.length} 个用户组</p>} footer={<p className="text-sm text-muted-foreground">共 {groups.length} 个用户组</p>}
/> />
) : null} ) : null}
</AppPage> </AppPage>
) )
} }

View File

@@ -6,12 +6,13 @@ import { AppEmptyState } from '@/components/app/AppEmptyState'
import { AppFilterBar } from '@/components/app/AppFilterBar' import { AppFilterBar } from '@/components/app/AppFilterBar'
import { AppPage } from '@/components/app/AppPage' import { AppPage } from '@/components/app/AppPage'
import { AppPageHeader } from '@/components/app/AppPageHeader' import { AppPageHeader } from '@/components/app/AppPageHeader'
import { AppSection } from '@/components/app/AppSection'
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 { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Field, FieldGroup, FieldLabel, FieldLegend, FieldSet } from '@/components/ui/field'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { createUser, listRoles, listUserGroups, listUsers, setUserGroups, setUserRoles, updateUser } from './api' import { createUser, listRoles, listUserGroups, listUsers, setUserGroups, setUserRoles, updateUser } from './api'
@@ -140,46 +141,50 @@ export function SystemUsersPage() {
return ( return (
<AppPage> <AppPage>
<AppPageHeader title="用户管理" /> <AppPageHeader title="用户管理" />
<form className="grid gap-3 rounded-lg border border-border bg-surface p-3 md:grid-cols-[1fr_1fr_1fr_1fr_auto] md:items-end" onSubmit={handleCreateUser}> <AppSection title="新增用户">
<AppField label="账号" htmlFor="system-user-username" required> <form onSubmit={handleCreateUser}>
<Input id="system-user-username" value={form.username} onChange={(event) => setForm({ ...form, username: event.target.value })} required /> <FieldGroup className="grid gap-3 md:grid-cols-[1fr_1fr_1fr_1fr_auto] md:items-end">
</AppField> <AppField label="账号" htmlFor="system-user-username" required>
<AppField label="显示名称" htmlFor="system-user-display-name"> <Input id="system-user-username" value={form.username} onChange={(event) => setForm({ ...form, username: event.target.value })} required />
<Input id="system-user-display-name" value={form.display_name} onChange={(event) => setForm({ ...form, display_name: event.target.value })} /> </AppField>
</AppField> <AppField label="显示名称" htmlFor="system-user-display-name">
<AppField label="邮箱" htmlFor="system-user-email"> <Input id="system-user-display-name" value={form.display_name} onChange={(event) => setForm({ ...form, display_name: event.target.value })} />
<Input id="system-user-email" value={form.email} onChange={(event) => setForm({ ...form, email: event.target.value })} /> </AppField>
</AppField> <AppField label="邮箱" htmlFor="system-user-email">
<AppField label="初始密码" htmlFor="system-user-password" required> <Input id="system-user-email" value={form.email} onChange={(event) => setForm({ ...form, email: event.target.value })} />
<Input id="system-user-password" type="password" value={form.password} onChange={(event) => setForm({ ...form, password: event.target.value })} required /> </AppField>
</AppField> <AppField label="初始密码" htmlFor="system-user-password" required>
<Button type="submit" className="w-full md:w-auto">创建用户</Button> <Input id="system-user-password" type="password" value={form.password} onChange={(event) => setForm({ ...form, password: event.target.value })} required />
</AppField>
<Button type="submit" className="w-full md:w-auto">创建用户</Button>
</FieldGroup>
</form> </form>
</AppSection>
<AppFilterBar> <AppFilterBar>
<Input placeholder="搜索账号、姓名或邮箱" value={query} onChange={(event) => setQuery(event.target.value)} /> <Input placeholder="搜索账号、姓名或邮箱" value={query} onChange={(event) => setQuery(event.target.value)} />
</AppFilterBar> </AppFilterBar>
{error ? <AppEmptyState title="无法加载用户" description={error} /> : null} {error ? <AppEmptyState title="无法加载用户" description={error} /> : null}
{!error ? ( {!error ? (
<AppDataTable <AppDataTable
columns={columns} columns={columns}
rows={filteredUsers} rows={filteredUsers}
getRowKey={(user) => user.id} getRowKey={(user) => user.id}
emptyTitle="暂无用户" emptyTitle="暂无用户"
footer={<p className="text-sm text-muted-foreground">当前显示 {filteredUsers.length} 个用户</p>} footer={<p className="text-sm text-muted-foreground">当前显示 {filteredUsers.length} 个用户</p>}
/>
) : null}
<UserAccessDialog
user={editingUser}
roles={roles}
groups={groups}
onClose={() => setEditingUser(null)}
onSaved={load}
/> />
) : null}
<UserAccessDialog
user={editingUser}
roles={roles}
groups={groups}
onClose={() => setEditingUser(null)}
onSaved={load}
/>
</AppPage> </AppPage>
) )
} }
@@ -219,38 +224,40 @@ function UserAccessDialog({
<DialogTitle>用户授权{user ? `:${user.display_name || user.username}` : ''}</DialogTitle> <DialogTitle>用户授权{user ? `:${user.display_name || user.username}` : ''}</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<div className="grid gap-2"> <FieldSet>
<Label>角色</Label> <FieldLegend>角色</FieldLegend>
<div className="grid max-h-64 gap-2 overflow-auto rounded-md border border-border p-3"> <FieldGroup data-slot="checkbox-group" className="grid max-h-64 gap-2 overflow-auto rounded-md border border-border p-3">
{roles.map((role) => ( {roles.map((role) => (
<label key={role.id} className="flex items-center gap-2 text-sm"> <Field key={role.id} orientation="horizontal">
<Checkbox <Checkbox
id={`access-role-${role.id}`}
checked={roleIds.includes(role.id)} checked={roleIds.includes(role.id)}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
setRoleIds((current) => checked ? [...current, role.id] : current.filter((id) => id !== role.id)) setRoleIds((current) => checked ? [...current, role.id] : current.filter((id) => id !== role.id))
} }
/> />
<span>{role.name}</span> <FieldLabel htmlFor={`access-role-${role.id}`}>{role.name}</FieldLabel>
</label> </Field>
))} ))}
</div> </FieldGroup>
</div> </FieldSet>
<div className="grid gap-2"> <FieldSet>
<Label>用户组</Label> <FieldLegend>用户组</FieldLegend>
<div className="grid max-h-64 gap-2 overflow-auto rounded-md border border-border p-3"> <FieldGroup data-slot="checkbox-group" className="grid max-h-64 gap-2 overflow-auto rounded-md border border-border p-3">
{groups.map((group) => ( {groups.map((group) => (
<label key={group.id} className="flex items-center gap-2 text-sm"> <Field key={group.id} orientation="horizontal">
<Checkbox <Checkbox
id={`access-group-${group.id}`}
checked={groupIds.includes(group.id)} checked={groupIds.includes(group.id)}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
setGroupIds((current) => checked ? [...current, group.id] : current.filter((id) => id !== group.id)) setGroupIds((current) => checked ? [...current, group.id] : current.filter((id) => id !== group.id))
} }
/> />
<span>{group.name}</span> <FieldLabel htmlFor={`access-group-${group.id}`}>{group.name}</FieldLabel>
</label> </Field>
))} ))}
</div> </FieldGroup>
</div> </FieldSet>
</div> </div>
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={onClose}>取消</Button> <Button variant="outline" onClick={onClose}>取消</Button>