Artifact 53aea0bea74cb854b2abcc9d84119d3dd0539e421ef69c6510ebb83da1951c63:
- File
web/src/pages/Users.tsx
— part of check-in
[dfa747c1ad]
at
2026-10-23 06:08:28
on branch trunk
— Web 管理控制台
- 用 Vite 和 React 做完整的 Web 端:登录、按权限显示菜单、切换当前作业仓库 - 覆盖来肯同步、仓库、库位规划、商品、销售订单、采购入库与批次码、采购订单、库存、用户授权和审计日志 - 开发时由 Vite 把 /api 代理到本机后端 - SQLite 写入和页面读取冲突时自动重试,避免请求进程因数据库忙碌直接崩溃 (user: pluto size: 9956)
import { useEffect, useState, type FormEvent } from 'react' import { api, type Warehouse } from '../api' import { Banner, Empty, Field, Page } from '../ui' type UserRow = { id: string username: string display_name: string enabled: boolean roles: string[] warehouse_ids: number[] } type Role = { code: string; name: string; terminals: string[] } const blank = { username: '', password: '', display_name: '', enabled: true, roles: [] as string[], warehouse_ids: [] as number[], } export function Users() { const [users, setUsers] = useState<UserRow[]>([]) const [roles, setRoles] = useState<Role[]>([]) const [warehouses, setWarehouses] = useState<Warehouse[]>([]) const [draft, setDraft] = useState(blank) const [editing, setEditing] = useState<UserRow | null>(null) const [resetId, setResetId] = useState('') const [resetPassword, setResetPassword] = useState('') const [error, setError] = useState('') const [ok, setOk] = useState('') function load() { Promise.all([ api<{ users: UserRow[] }>('/api/users'), api<{ roles: Role[] }>('/api/roles'), api<{ warehouses: Warehouse[] }>('/api/warehouses'), ]) .then(([userData, roleData, warehouseData]) => { setUsers(userData.users) setRoles(roleData.roles) setWarehouses(warehouseData.warehouses) }) .catch((err: unknown) => setError(err instanceof Error ? err.message : '加载失败')) } useEffect(load, []) function toggle(list: string[], value: string) { return list.includes(value) ? list.filter((item) => item !== value) : [...list, value] } async function create(event: FormEvent) { event.preventDefault() setError('') setOk('') try { await api('/api/users', { method: 'POST', body: JSON.stringify(draft), }) setDraft(blank) setOk('用户已创建') load() } catch (err) { setError(err instanceof Error ? err.message : '创建失败') } } async function save(event: FormEvent) { event.preventDefault() if (!editing) return setError('') setOk('') try { await api(`/api/users/${editing.id}`, { method: 'PUT', body: JSON.stringify({ display_name: editing.display_name, enabled: editing.enabled, roles: editing.roles, warehouse_ids: editing.warehouse_ids, }), }) setEditing(null) setOk('用户已更新,对方需要重新登录') load() } catch (err) { setError(err instanceof Error ? err.message : '保存失败') } } async function reset(event: FormEvent) { event.preventDefault() setError('') setOk('') try { await api(`/api/users/${resetId}/password`, { method: 'POST', body: JSON.stringify({ password: resetPassword }), }) setResetId('') setResetPassword('') setOk('密码已重置') } catch (err) { setError(err instanceof Error ? err.message : '重置失败') } } return ( <Page title="用户与权限"> <Banner kind="error" text={error} /> <Banner kind="ok" text={ok} /> {users.length === 0 ? ( <Empty text="还没有用户" /> ) : ( <div className="table-wrap"> <table> <thead> <tr> <th>账号</th> <th>姓名</th> <th>角色</th> <th>仓库</th> <th>状态</th> <th></th> </tr> </thead> <tbody> {users.map((row) => ( <tr key={row.id}> <td className="mono">{row.username}</td> <td>{row.display_name}</td> <td>{row.roles.map((code) => roles.find((role) => role.code === code)?.name ?? code).join('、')}</td> <td> {row.roles.includes('ROLE_SYS_ADMIN') ? '全部' : warehouses .filter((warehouse) => row.warehouse_ids.includes(warehouse.id)) .map((warehouse) => warehouse.name) .join('、')} </td> <td>{row.enabled ? '启用' : '停用'}</td> <td className="row-actions"> <button type="button" className="ghost" onClick={() => setEditing({ ...row })}> 编辑 </button> <button type="button" className="ghost" onClick={() => setResetId(row.id)}> 重置密码 </button> </td> </tr> ))} </tbody> </table> </div> )} {editing && ( <form className="panel" onSubmit={save}> <h2>编辑 {editing.username}</h2> <Field label="姓名"> <input value={editing.display_name} onChange={(event) => setEditing({ ...editing, display_name: event.target.value })} /> </Field> <label className="field"> <span>状态</span> <input type="checkbox" checked={editing.enabled} onChange={(event) => setEditing({ ...editing, enabled: event.target.checked })} /> 启用 </label> <Checks title="角色" options={roles.map((role) => ({ id: role.code, label: `${role.name}(${role.terminals.join('、')})` }))} selected={editing.roles} onToggle={(code) => setEditing({ ...editing, roles: toggle(editing.roles, code) })} /> <Checks title="授权仓库" options={warehouses.map((row) => ({ id: String(row.id), label: `${row.name}(${row.code})` }))} selected={editing.warehouse_ids.map(String)} onToggle={(id) => { const ids = editing.warehouse_ids.includes(Number(id)) ? editing.warehouse_ids.filter((value) => value !== Number(id)) : [...editing.warehouse_ids, Number(id)] setEditing({ ...editing, warehouse_ids: ids }) }} /> <div className="actions"> <button type="submit">保存</button> <button type="button" className="ghost" onClick={() => setEditing(null)}>取消</button> </div> </form> )} {resetId && ( <form className="panel" onSubmit={reset}> <h2>重置密码</h2> <Field label="新密码(至少 8 位)"> <input type="password" value={resetPassword} onChange={(event) => setResetPassword(event.target.value)} /> </Field> <div className="actions"> <button type="submit" disabled={resetPassword.length < 8}>重置</button> <button type="button" className="ghost" onClick={() => setResetId('')}>取消</button> </div> </form> )} <form className="panel" onSubmit={create}> <h2>新建用户</h2> <div className="filters"> <Field label="账号"> <input value={draft.username} onChange={(event) => setDraft({ ...draft, username: event.target.value })} /> </Field> <Field label="初始密码"> <input type="password" value={draft.password} onChange={(event) => setDraft({ ...draft, password: event.target.value })} /> </Field> <Field label="姓名"> <input value={draft.display_name} onChange={(event) => setDraft({ ...draft, display_name: event.target.value })} /> </Field> </div> <Checks title="角色" options={roles.map((role) => ({ id: role.code, label: role.name }))} selected={draft.roles} onToggle={(code) => setDraft({ ...draft, roles: toggle(draft.roles, code) })} /> <Checks title="授权仓库(超级管理员不用选)" options={warehouses.map((row) => ({ id: String(row.id), label: row.name }))} selected={draft.warehouse_ids.map(String)} onToggle={(id) => { const numeric = Number(id) const ids = draft.warehouse_ids.includes(numeric) ? draft.warehouse_ids.filter((value) => value !== numeric) : [...draft.warehouse_ids, numeric] setDraft({ ...draft, warehouse_ids: ids }) }} /> <button type="submit" disabled={!draft.username || draft.password.length < 8 || !draft.display_name || draft.roles.length === 0}> 创建 </button> </form> <h2>角色</h2> <div className="table-wrap"> <table> <thead> <tr> <th>编码</th> <th>名称</th> <th>终端</th> </tr> </thead> <tbody> {roles.map((role) => ( <tr key={role.code}> <td className="mono">{role.code}</td> <td>{role.name}</td> <td>{role.terminals.join('、')}</td> </tr> ))} </tbody> </table> </div> </Page> ) } function Checks({ title, options, selected, onToggle, }: { title: string options: { id: string; label: string }[] selected: string[] onToggle: (id: string) => void }) { return ( <div> <span className="muted">{title}</span> <div className="check-grid"> {options.map((option) => ( <label key={option.id}> <input type="checkbox" checked={selected.includes(option.id)} onChange={() => onToggle(option.id)} /> {option.label} </label> ))} </div> </div> ) }