Files
ai-ticket-platform/frontend/src/features/system/SystemUsersPage.tsx
2026-07-02 15:39:03 +08:00

263 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as React from 'react'
import { AppField } from '@/components/app/AppField'
import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable'
import { AppEmptyState } from '@/components/app/AppEmptyState'
import { AppFilterBar } from '@/components/app/AppFilterBar'
import { AppPage } from '@/components/app/AppPage'
import { AppPageHeader } from '@/components/app/AppPageHeader'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import { createUser, listRoles, listUserGroups, listUsers, setUserGroups, setUserRoles, updateUser } from './api'
import type { RoleDetail, SystemUser, UserGroup } from './types'
type UserForm = {
username: string
display_name: string
email: string
password: string
}
const emptyForm: UserForm = {
username: '',
display_name: '',
email: '',
password: ''
}
export function SystemUsersPage() {
const [users, setUsers] = React.useState<SystemUser[]>([])
const [roles, setRoles] = React.useState<RoleDetail[]>([])
const [groups, setGroups] = React.useState<UserGroup[]>([])
const [query, setQuery] = React.useState('')
const [form, setForm] = React.useState<UserForm>(emptyForm)
const [editingUser, setEditingUser] = React.useState<SystemUser | null>(null)
const [error, setError] = React.useState('')
const load = React.useCallback(async () => {
try {
const [nextUsers, nextRoles, nextGroups] = await Promise.all([listUsers(), listRoles(), listUserGroups()])
setUsers(nextUsers)
setRoles(nextRoles)
setGroups(nextGroups)
setError('')
} catch (reason) {
setError(reason instanceof Error ? reason.message : '加载失败')
}
}, [])
React.useEffect(() => {
void load()
}, [load])
const filteredUsers = users.filter((user) => {
const keyword = query.trim().toLowerCase()
if (!keyword) return true
return `${user.username} ${user.display_name} ${user.email}`.toLowerCase().includes(keyword)
})
async function handleCreateUser(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
await createUser({ ...form, is_active: true })
setForm(emptyForm)
await load()
}
async function toggleUserActive(user: SystemUser, checked: boolean) {
await updateUser(user.id, { is_active: checked })
await load()
}
const columns: AppDataTableColumn<SystemUser>[] = [
{
key: 'user',
header: '用户',
cell: (user) => (
<div>
<p className="font-medium text-foreground">{user.display_name || user.username}</p>
<p className="mt-1 text-sm text-muted-foreground">{user.username}</p>
</div>
)
},
{
key: 'email',
header: '邮箱',
cell: (user) => <span className="text-muted-foreground">{user.email || '-'}</span>
},
{
key: 'roles',
header: '角色',
cell: (user) => (
<div className="flex max-w-[260px] flex-wrap gap-1">
{user.roles.length ? user.roles.map((role) => <Badge key={role.id} variant="secondary">{role.name}</Badge>) : '-'}
</div>
)
},
{
key: 'groups',
header: '用户组',
cell: (user) => (
<div className="flex max-w-[260px] flex-wrap gap-1">
{user.user_groups.length
? user.user_groups.map((group) => <Badge key={group.id} variant="info">{group.name}</Badge>)
: '-'}
</div>
)
},
{
key: 'active',
header: '状态',
cell: (user) => (
<Switch
checked={user.is_active}
onClick={(event) => event.stopPropagation()}
onCheckedChange={(checked) => void toggleUserActive(user, checked)}
/>
)
},
{
key: 'actions',
header: '操作',
cell: (user) => (
<Button
variant="ghost"
onClick={(event) => {
event.stopPropagation()
setEditingUser(user)
}}
>
授权
</Button>
)
}
]
return (
<AppPage>
<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}>
<AppField label="账号" htmlFor="system-user-username" required>
<Input id="system-user-username" value={form.username} onChange={(event) => setForm({ ...form, username: event.target.value })} required />
</AppField>
<AppField label="显示名称" htmlFor="system-user-display-name">
<Input id="system-user-display-name" value={form.display_name} onChange={(event) => setForm({ ...form, display_name: event.target.value })} />
</AppField>
<AppField label="邮箱" htmlFor="system-user-email">
<Input id="system-user-email" value={form.email} onChange={(event) => setForm({ ...form, email: event.target.value })} />
</AppField>
<AppField label="初始密码" htmlFor="system-user-password" required>
<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>
</form>
<AppFilterBar>
<Input placeholder="搜索账号、姓名或邮箱" value={query} onChange={(event) => setQuery(event.target.value)} />
</AppFilterBar>
{error ? <AppEmptyState title="无法加载用户" description={error} /> : null}
{!error ? (
<AppDataTable
columns={columns}
rows={filteredUsers}
getRowKey={(user) => user.id}
emptyTitle="暂无用户"
footer={<p className="text-sm text-muted-foreground">当前显示 {filteredUsers.length} 个用户</p>}
/>
) : null}
<UserAccessDialog
user={editingUser}
roles={roles}
groups={groups}
onClose={() => setEditingUser(null)}
onSaved={load}
/>
</AppPage>
)
}
function UserAccessDialog({
user,
roles,
groups,
onClose,
onSaved
}: {
user: SystemUser | null
roles: RoleDetail[]
groups: UserGroup[]
onClose: () => void
onSaved: () => Promise<void>
}) {
const [roleIds, setRoleIds] = React.useState<number[]>([])
const [groupIds, setGroupIds] = React.useState<number[]>([])
React.useEffect(() => {
setRoleIds(user?.roles.map((role) => role.id) ?? [])
setGroupIds(user?.user_groups.map((group) => group.id) ?? [])
}, [user])
async function handleSave() {
if (!user) return
await Promise.all([setUserRoles(user.id, roleIds), setUserGroups(user.id, groupIds)])
await onSaved()
onClose()
}
return (
<Dialog open={Boolean(user)} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="sm:max-w-xl">
<DialogHeader>
<DialogTitle>用户授权{user ? `:${user.display_name || user.username}` : ''}</DialogTitle>
</DialogHeader>
<div className="grid gap-4 md:grid-cols-2">
<div className="grid gap-2">
<Label>角色</Label>
<div className="grid max-h-64 gap-2 overflow-auto rounded-md border border-border p-3">
{roles.map((role) => (
<label key={role.id} className="flex items-center gap-2 text-sm">
<Checkbox
checked={roleIds.includes(role.id)}
onCheckedChange={(checked) =>
setRoleIds((current) => checked ? [...current, role.id] : current.filter((id) => id !== role.id))
}
/>
<span>{role.name}</span>
</label>
))}
</div>
</div>
<div className="grid gap-2">
<Label>用户组</Label>
<div className="grid max-h-64 gap-2 overflow-auto rounded-md border border-border p-3">
{groups.map((group) => (
<label key={group.id} className="flex items-center gap-2 text-sm">
<Checkbox
checked={groupIds.includes(group.id)}
onCheckedChange={(checked) =>
setGroupIds((current) => checked ? [...current, group.id] : current.filter((id) => id !== group.id))
}
/>
<span>{group.name}</span>
</label>
))}
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={onClose}>取消</Button>
<Button onClick={() => void handleSave()}>保存授权</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}