Files
affiliate_dash/frontend/src/pages/PlatformUsers.tsx
T

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