- 交互重构:在「平台商户」与「平台账号」点击成员管理时,不再跳转路由或改写 localStorage.merchant_id,改为滑出 MerchantMembersDrawer 抽屉 - 抽屉能力:支持查看指定商户成员列表、新增成员(新建账号或关联已有账号)、修改角色与启用状态、移除成员 - 顶栏清理:彻底移除顶栏右上角的「当前商户:xxx」标签与「退出商户视角」按钮,超管登录自动清理任何残留上下文 - 后端补齐:新增平台专属 /api/platform/merchants/:id/members 与 roles 接口,平台操作不再依赖 X-Merchant-ID - 验证:tsc -b && vite build 成功;全量 go test ./... 包含路由与鉴权回归测试 100% 通过
264 lines
12 KiB
TypeScript
264 lines
12 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/authContext'
|
|
import { roleText } from './merchantCenterUtils'
|
|
import { MerchantMembersDrawer } from '../components/MerchantMembersDrawer'
|
|
|
|
type AdminFormValues = {
|
|
username: string
|
|
nickname?: string
|
|
password?: string
|
|
status: boolean
|
|
}
|
|
|
|
export default function PlatformUsers() {
|
|
const { user: currentUser } = useAuth()
|
|
const [drawerOpen, setDrawerOpen] = useState(false)
|
|
const [selectedMerchantId, setSelectedMerchantId] = useState<number | null>(null)
|
|
const [selectedMerchantName, setSelectedMerchantName] = useState<string>('')
|
|
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 = useCallback((record: User) => {
|
|
setEditingAdmin(record)
|
|
form.setFieldsValue({ username: record.username, nickname: record.nickname, password: '', status: record.status === 1 })
|
|
setModalOpen(true)
|
|
}, [form])
|
|
|
|
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 = useCallback(async (record: User) => {
|
|
try {
|
|
await userApi.remove(record.id)
|
|
message.success('平台管理员已删除')
|
|
void loadAdmins()
|
|
} catch (e) {
|
|
message.error(e instanceof Error ? e.message : '删除失败')
|
|
}
|
|
}, [loadAdmins])
|
|
|
|
const removeMerchantAccount = useCallback(async (record: MerchantMember) => {
|
|
try {
|
|
await platformApi.deleteMerchantAccount(record.user_id)
|
|
message.success('商户账号已删除')
|
|
void loadMerchantAccounts()
|
|
} catch (e) {
|
|
message.error(e instanceof Error ? e.message : '删除失败')
|
|
}
|
|
}, [loadMerchantAccounts])
|
|
|
|
const columns: ColumnsType<User> = useMemo(() => [
|
|
{ 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>
|
|
),
|
|
},
|
|
], [currentUser?.id, openEdit, remove])
|
|
|
|
const memberColumns: ColumnsType<MerchantMember> = useMemo(() => [
|
|
{ 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> },
|
|
{
|
|
title: '操作',
|
|
width: 120,
|
|
render: (_, record) => <Popconfirm
|
|
title={`确认删除商户账号「${record.user?.username || `#${record.user_id}`}」?`}
|
|
description="该账号会被永久删除,并移除其在所有商户下的成员关系。"
|
|
onConfirm={() => void removeMerchantAccount(record)}
|
|
okButtonProps={{ danger: true }}
|
|
>
|
|
<Button type="link" danger size="small" icon={<DeleteOutlined />}>删除账号</Button>
|
|
</Popconfirm>,
|
|
},
|
|
], [removeMerchantAccount])
|
|
|
|
const merchantColumns: ColumnsType<MerchantMemberGroup> = useMemo(() => [
|
|
{
|
|
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={() => {
|
|
setSelectedMerchantId(record.merchant_id)
|
|
setSelectedMerchantName(record.merchant?.name || record.merchant?.code || `商户 #${record.merchant_id}`)
|
|
setDrawerOpen(true)
|
|
}}
|
|
>
|
|
管理商户成员
|
|
</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>
|
|
|
|
<MerchantMembersDrawer
|
|
open={drawerOpen}
|
|
merchantId={selectedMerchantId}
|
|
merchantName={selectedMerchantName}
|
|
onClose={() => {
|
|
setDrawerOpen(false)
|
|
setSelectedMerchantId(null)
|
|
setSelectedMerchantName('')
|
|
}}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|