Fosi Fossil Service
Small teams. Open source. Built on Fossil.

IaC / SwiftFlow

High-performance WMS with wave picking, smart routing, and real-time inventory visibility.

Public | Owner: IaC | Default branch: trunk | Last update: 2026-10-23 14:37

Artifact 53aea0bea74cb854b2abcc9d84119d3dd0539e421ef69c6510ebb83da1951c63:


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>
  )
}

Clone

You can clone this repository with the following command:

fossil clone https://fosi.dev/IaC/SwiftFlow

Contribute

The best way to contribute is through e-mail. See the project documentation for more details.