220 lines
9.9 KiB
TypeScript
220 lines
9.9 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
import { Button, Form, Input, Modal, Popconfirm, Space, Switch, Table, Tabs, Tag, Typography, message } from 'antd'
|
|
import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons'
|
|
import type { ColumnsType } from 'antd/es/table'
|
|
import { platformApi, userApi } from '../api'
|
|
import { PageHeader } from '../components/PageHeader'
|
|
import type { MerchantMember, MerchantMemberGroup, PageResult, User } from '../types'
|
|
import { formatDateTime } from '../utils/time'
|
|
import { useAuth } from '../store/auth'
|
|
import { roleText } from './merchantCenterUtils'
|
|
import { useNavigate } from 'react-router-dom'
|
|
|
|
type AdminFormValues = {
|
|
username: string
|
|
nickname?: string
|
|
password?: string
|
|
status: boolean
|
|
}
|
|
|
|
export default function PlatformUsers() {
|
|
const { user: currentUser } = useAuth()
|
|
const navigate = useNavigate()
|
|
const [tab, setTab] = useState('admins')
|
|
const [admins, setAdmins] = useState<PageResult<User>>({ list: [], total: 0, page: 1, size: 20 })
|
|
const [merchantAccounts, setMerchantAccounts] = useState<PageResult<MerchantMemberGroup>>({ list: [], total: 0, page: 1, size: 20 })
|
|
const [loading, setLoading] = useState(false)
|
|
const [submitting, setSubmitting] = useState(false)
|
|
const [editingAdmin, setEditingAdmin] = useState<User | null>(null)
|
|
const [modalOpen, setModalOpen] = useState(false)
|
|
const [form] = Form.useForm<AdminFormValues>()
|
|
|
|
const loadAdmins = useCallback(async (page = admins.page, size = admins.size) => {
|
|
setLoading(true)
|
|
try {
|
|
setAdmins(await userApi.list({ page, size }))
|
|
} catch (e) {
|
|
message.error(e instanceof Error ? e.message : '加载失败')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [admins.page, admins.size])
|
|
|
|
const loadMerchantAccounts = useCallback(async (page = merchantAccounts.page, size = merchantAccounts.size) => {
|
|
setLoading(true)
|
|
try {
|
|
setMerchantAccounts(await platformApi.merchantAccounts({ page, size }))
|
|
} catch (e) {
|
|
message.error(e instanceof Error ? e.message : '加载失败')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [merchantAccounts.page, merchantAccounts.size])
|
|
|
|
useEffect(() => {
|
|
if (tab === 'admins') void loadAdmins()
|
|
else void loadMerchantAccounts()
|
|
}, [loadAdmins, loadMerchantAccounts, tab])
|
|
|
|
const openCreate = () => {
|
|
setEditingAdmin(null)
|
|
form.setFieldsValue({ username: '', nickname: '', password: '', status: true })
|
|
setModalOpen(true)
|
|
}
|
|
|
|
const openEdit = (record: User) => {
|
|
setEditingAdmin(record)
|
|
form.setFieldsValue({ username: record.username, nickname: record.nickname, password: '', status: record.status === 1 })
|
|
setModalOpen(true)
|
|
}
|
|
|
|
const submit = async () => {
|
|
const values = await form.validateFields()
|
|
setSubmitting(true)
|
|
try {
|
|
if (editingAdmin) {
|
|
await userApi.update(editingAdmin.id, {
|
|
username: values.username,
|
|
nickname: values.nickname || '',
|
|
password: values.password || undefined,
|
|
status: values.status ? 1 : 0,
|
|
})
|
|
message.success('平台管理员已更新')
|
|
} else {
|
|
await userApi.create({ username: values.username, password: values.password || '', nickname: values.nickname || '' })
|
|
message.success('平台管理员已创建')
|
|
}
|
|
setModalOpen(false)
|
|
form.resetFields()
|
|
void loadAdmins(editingAdmin ? admins.page : 1)
|
|
} catch (e) {
|
|
message.error(e instanceof Error ? e.message : editingAdmin ? '更新失败' : '创建失败')
|
|
} finally {
|
|
setSubmitting(false)
|
|
}
|
|
}
|
|
|
|
const remove = async (record: User) => {
|
|
try {
|
|
await userApi.remove(record.id)
|
|
message.success('平台管理员已删除')
|
|
void loadAdmins()
|
|
} catch (e) {
|
|
message.error(e instanceof Error ? e.message : '删除失败')
|
|
}
|
|
}
|
|
|
|
const columns: ColumnsType<User> = [
|
|
{ title: '用户名', dataIndex: 'username', width: 260, render: (value) => <Typography.Text strong>{value}</Typography.Text> },
|
|
{ title: '昵称', dataIndex: 'nickname', width: 220, render: (value) => value || '-' },
|
|
{ title: '状态', dataIndex: 'status', width: 120, render: (value) => value === 1 ? <Tag color="green">启用</Tag> : <Tag>停用</Tag> },
|
|
{ title: '创建时间', dataIndex: 'created_at', width: 190, render: formatDateTime },
|
|
{
|
|
title: '操作',
|
|
width: 190,
|
|
render: (_, record) => record.id === currentUser?.id ? <Typography.Text type="secondary">当前账号</Typography.Text> : (
|
|
<Space size={0}>
|
|
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => openEdit(record)}>编辑</Button>
|
|
<Popconfirm title={`确认删除平台管理员「${record.username}」?`} description="删除后无法登录平台,且不会影响任何商户员工。" onConfirm={() => remove(record)} okButtonProps={{ danger: true }}>
|
|
<Button type="link" danger size="small" icon={<DeleteOutlined />}>删除</Button>
|
|
</Popconfirm>
|
|
</Space>
|
|
),
|
|
},
|
|
]
|
|
|
|
const memberColumns: ColumnsType<MerchantMember> = [
|
|
{ title: '员工账号', dataIndex: ['user', 'username'], width: 260, render: (_, record) => <Typography.Text strong>{record.user?.username || `#${record.user_id}`}</Typography.Text> },
|
|
{ title: '昵称', dataIndex: ['user', 'nickname'], width: 220, render: (_, record) => record.user?.nickname || '-' },
|
|
{ title: '商户角色', dataIndex: 'role', width: 160, render: (role) => <Tag color="blue">{roleText(role)}</Tag> },
|
|
{ title: '成员状态', dataIndex: 'status', width: 130, render: (status) => status === 1 ? <Tag color="green">启用</Tag> : <Tag>停用</Tag> },
|
|
]
|
|
|
|
const merchantColumns: ColumnsType<MerchantMemberGroup> = [
|
|
{
|
|
title: '商户',
|
|
width: 320,
|
|
render: (_, record) => <Space direction="vertical" size={0}><Typography.Text strong>{record.merchant?.name || `商户#${record.merchant_id}`}</Typography.Text><Typography.Text type="secondary" style={{ fontSize: 12 }}>{record.merchant?.code || '-'}</Typography.Text></Space>,
|
|
},
|
|
{ title: '下游账号', width: 130, render: (_, record) => `${record.members.length} 个` },
|
|
{
|
|
title: '角色分布',
|
|
render: (_, record) => {
|
|
const roles = [...new Set(record.members.map((member) => member.role))]
|
|
return <Space size={[4, 4]} wrap>{roles.map((role) => <Tag color="blue" key={role}>{roleText(role)}</Tag>)}</Space>
|
|
},
|
|
},
|
|
{
|
|
title: '操作',
|
|
width: 160,
|
|
render: (_, record) => <Button type="link" size="small" onClick={() => {
|
|
localStorage.setItem('merchant_id', String(record.merchant_id))
|
|
navigate('/merchant-members')
|
|
}}>管理商户成员</Button>,
|
|
},
|
|
]
|
|
|
|
const items = useMemo(() => [
|
|
{
|
|
key: 'admins',
|
|
label: `平台管理员 (${admins.total})`,
|
|
children: <Table
|
|
rowKey="id"
|
|
loading={loading}
|
|
columns={columns}
|
|
dataSource={admins.list}
|
|
pagination={{ current: admins.page, pageSize: admins.size, total: admins.total, showSizeChanger: true, onChange: (page, size) => void loadAdmins(page, size) }}
|
|
/>,
|
|
},
|
|
{
|
|
key: 'merchant-accounts',
|
|
label: `商户账号 (${merchantAccounts.total})`,
|
|
children: <Table
|
|
rowKey="merchant_id"
|
|
loading={loading}
|
|
columns={merchantColumns}
|
|
dataSource={merchantAccounts.list}
|
|
expandable={{
|
|
expandedRowRender: (record) => <Table rowKey="id" size="small" columns={memberColumns} dataSource={record.members} pagination={false} />,
|
|
rowExpandable: (record) => record.members.length > 0,
|
|
}}
|
|
pagination={{ current: merchantAccounts.page, pageSize: merchantAccounts.size, total: merchantAccounts.total, showSizeChanger: true, onChange: (page, size) => void loadMerchantAccounts(page, size) }}
|
|
/>,
|
|
},
|
|
], [admins, columns, loadAdmins, loadMerchantAccounts, loading, memberColumns, merchantAccounts, merchantColumns])
|
|
|
|
return (
|
|
<div>
|
|
<PageHeader
|
|
title="账号管理"
|
|
subtitle="平台管理员独立管理;商户账号按所属商户展示,下游员工由各商户自行维护。"
|
|
breadcrumbs={[{ title: '账号管理' }]}
|
|
extra={<Space><Button icon={<ReloadOutlined />} loading={loading} onClick={() => tab === 'admins' ? void loadAdmins() : void loadMerchantAccounts()}>刷新</Button>{tab === 'admins' && <Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>新建平台管理员</Button>}</Space>}
|
|
/>
|
|
<Tabs activeKey={tab} onChange={setTab} items={items} />
|
|
<Modal
|
|
title={editingAdmin ? '编辑平台管理员' : '新建平台管理员'}
|
|
open={modalOpen}
|
|
onCancel={() => setModalOpen(false)}
|
|
onOk={() => void submit()}
|
|
confirmLoading={submitting}
|
|
okText="保存"
|
|
destroyOnHidden
|
|
>
|
|
<Form form={form} layout="vertical" preserve={false} initialValues={{ status: true }}>
|
|
<Form.Item label="用户名" name="username" rules={[{ required: true, message: '请输入用户名' }, { max: 64, message: '用户名不能超过 64 个字符' }]}>
|
|
<Input autoComplete="off" />
|
|
</Form.Item>
|
|
<Form.Item label="昵称" name="nickname" rules={[{ max: 64, message: '昵称不能超过 64 个字符' }]}>
|
|
<Input autoComplete="off" />
|
|
</Form.Item>
|
|
<Form.Item label={editingAdmin ? '新密码' : '密码'} name="password" rules={editingAdmin ? [{ min: 6, message: '密码至少 6 位' }] : [{ required: true, message: '请输入密码' }, { min: 6, message: '密码至少 6 位' }]} extra={editingAdmin ? '留空表示不修改密码' : undefined}>
|
|
<Input.Password autoComplete="new-password" />
|
|
</Form.Item>
|
|
{editingAdmin && <Form.Item label="账号状态" name="status" valuePropName="checked"><Switch checkedChildren="启用" unCheckedChildren="停用" /></Form.Item>}
|
|
</Form>
|
|
</Modal>
|
|
</div>
|
|
)
|
|
}
|