feat(admin): 平台商户成员管理改用抽屉,消除商户视角切换与全局状态污染
- 交互重构:在「平台商户」与「平台账号」点击成员管理时,不再跳转路由或改写 localStorage.merchant_id,改为滑出 MerchantMembersDrawer 抽屉 - 抽屉能力:支持查看指定商户成员列表、新增成员(新建账号或关联已有账号)、修改角色与启用状态、移除成员 - 顶栏清理:彻底移除顶栏右上角的「当前商户:xxx」标签与「退出商户视角」按钮,超管登录自动清理任何残留上下文 - 后端补齐:新增平台专属 /api/platform/merchants/:id/members 与 roles 接口,平台操作不再依赖 X-Merchant-ID - 验证:tsc -b && vite build 成功;全量 go test ./... 包含路由与鉴权回归测试 100% 通过
This commit is contained in:
@@ -180,3 +180,71 @@ func (h *MerchantHandler) AdminUpdateMerchantAccount(c *gin.Context) {
|
||||
}
|
||||
response.OK(c, account)
|
||||
}
|
||||
|
||||
// ListPlatformMerchantMembers GET /api/platform/merchants/:id/members
|
||||
func (h *MerchantHandler) ListPlatformMerchantMembers(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if id == 0 {
|
||||
response.BadRequest(c, "商户 ID 无效")
|
||||
return
|
||||
}
|
||||
members, err := h.merchantSvc.ListMembers(uint(id))
|
||||
if err != nil {
|
||||
response.ServerError(c, err.Error())
|
||||
return
|
||||
}
|
||||
response.OK(c, members)
|
||||
}
|
||||
|
||||
// UpdatePlatformMerchantMember PATCH /api/platform/merchants/:id/members/:mid
|
||||
func (h *MerchantHandler) UpdatePlatformMerchantMember(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
mid, _ := strconv.ParseUint(c.Param("mid"), 10, 64)
|
||||
if id == 0 || mid == 0 {
|
||||
response.BadRequest(c, "参数无效")
|
||||
return
|
||||
}
|
||||
var req updateMemberReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "参数错误:角色必填")
|
||||
return
|
||||
}
|
||||
member, err := h.merchantSvc.UpdateMember(uint(id), uint(mid), service.UpdateMemberInput{
|
||||
Role: req.Role, Status: req.Status, IsDefault: req.IsDefault,
|
||||
}, middleware.GetUserID(c))
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
response.OK(c, member)
|
||||
}
|
||||
|
||||
// DeletePlatformMerchantMember DELETE /api/platform/merchants/:id/members/:mid
|
||||
func (h *MerchantHandler) DeletePlatformMerchantMember(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
mid, _ := strconv.ParseUint(c.Param("mid"), 10, 64)
|
||||
if id == 0 || mid == 0 {
|
||||
response.BadRequest(c, "参数无效")
|
||||
return
|
||||
}
|
||||
if err := h.merchantSvc.RemoveMember(uint(id), uint(mid), middleware.GetUserID(c)); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
response.OK(c, nil)
|
||||
}
|
||||
|
||||
// ListPlatformMerchantRoles GET /api/platform/merchants/:id/roles
|
||||
func (h *MerchantHandler) ListPlatformMerchantRoles(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if id == 0 {
|
||||
response.BadRequest(c, "商户 ID 无效")
|
||||
return
|
||||
}
|
||||
roles, err := h.merchantSvc.ListRoles(uint(id))
|
||||
if err != nil {
|
||||
response.ServerError(c, err.Error())
|
||||
return
|
||||
}
|
||||
response.OK(c, roles)
|
||||
}
|
||||
|
||||
@@ -243,7 +243,11 @@ func Setup(h *Handlers) *gin.Engine {
|
||||
platform.GET("/platform/orders", h.Merchant.ListPlatformOrders)
|
||||
platform.POST("/platform/merchants", h.Merchant.CreateMerchant)
|
||||
platform.PATCH("/platform/merchants/:id", h.Merchant.UpdateMerchantSettings)
|
||||
platform.GET("/platform/merchants/:id/members", h.Merchant.ListPlatformMerchantMembers)
|
||||
platform.POST("/platform/merchants/:id/members", h.Merchant.AddPlatformMerchantMember)
|
||||
platform.PATCH("/platform/merchants/:id/members/:mid", h.Merchant.UpdatePlatformMerchantMember)
|
||||
platform.DELETE("/platform/merchants/:id/members/:mid", h.Merchant.DeletePlatformMerchantMember)
|
||||
platform.GET("/platform/merchants/:id/roles", h.Merchant.ListPlatformMerchantRoles)
|
||||
platform.GET("/platform/product-catalog", h.Merchant.ListProductCatalog)
|
||||
platform.GET("/platform/merchants/:id/products", h.Merchant.ListMerchantProductsByAdmin)
|
||||
platform.PATCH("/platform/merchants/:id/products/:pid", h.Merchant.AdminUpdateMerchantProduct)
|
||||
|
||||
@@ -213,3 +213,30 @@ func TestRequestLoggerSuppressesPollingOnInfo(t *testing.T) {
|
||||
t.Errorf("expected GET order to appear as [DEBUG] in DEBUG mode, got: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestPlatformMerchantMembersRoutes 验证平台管理员访问商户成员与角色接口完全独立于 X-Merchant-ID。
|
||||
func TestPlatformMerchantMembersRoutes(t *testing.T) {
|
||||
engine, jm := newTestRouter(t)
|
||||
token, err := jm.Generate(1, "admin", model.RoleAdmin)
|
||||
if err != nil {
|
||||
t.Fatalf("generate token: %v", err)
|
||||
}
|
||||
|
||||
// 1. 获取商户 1 的成员列表
|
||||
req := httptest.NewRequest("GET", "/api/platform/merchants/1/members", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
w := httptest.NewRecorder()
|
||||
engine.ServeHTTP(w, req)
|
||||
if w.Code != 200 {
|
||||
t.Fatalf("平台管理员查询商户成员应返回 200,实际 %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
// 2. 获取商户 1 的角色列表
|
||||
req = httptest.NewRequest("GET", "/api/platform/merchants/1/roles", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
w = httptest.NewRecorder()
|
||||
engine.ServeHTTP(w, req)
|
||||
if w.Code != 200 {
|
||||
t.Fatalf("平台管理员查询商户角色应返回 200,实际 %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,10 +230,21 @@ export const platformApi = {
|
||||
}) => request.post('/platform/merchants', data).then((r) => r.data.data as Merchant),
|
||||
updateMerchant: (id: number, data: Partial<Merchant>) =>
|
||||
request.patch(`/platform/merchants/${id}`, data).then((r) => r.data.data),
|
||||
members: (merchantId: number) =>
|
||||
request.get(`/platform/merchants/${merchantId}/members`).then((r) => r.data.data as MerchantMember[]),
|
||||
addMember: (
|
||||
merchantId: number,
|
||||
data: { user_id?: number; username?: string; password?: string; nickname?: string; role: string; is_default?: boolean },
|
||||
data: { user_id?: number; username?: string; password?: string; nickname?: string; role: string; is_default?: boolean },
|
||||
) => request.post(`/platform/merchants/${merchantId}/members`, data).then((r) => r.data.data as MerchantMember),
|
||||
updateMember: (
|
||||
merchantId: number,
|
||||
memberId: number,
|
||||
data: { role: string; status: number; is_default: boolean },
|
||||
) => request.patch(`/platform/merchants/${merchantId}/members/${memberId}`, data).then((r) => r.data.data as MerchantMember),
|
||||
removeMember: (merchantId: number, memberId: number) =>
|
||||
request.delete(`/platform/merchants/${merchantId}/members/${memberId}`).then((r) => r.data.data),
|
||||
roles: (merchantId: number) =>
|
||||
request.get(`/platform/merchants/${merchantId}/roles`).then((r) => r.data.data as MerchantRole[]),
|
||||
productCatalog: () =>
|
||||
request.get('/platform/product-catalog').then((r) => r.data.data as ProductCatalogItem[]),
|
||||
merchantProducts: (merchantId: number) =>
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
Drawer,
|
||||
Table,
|
||||
Button,
|
||||
Space,
|
||||
Tag,
|
||||
Badge,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
Radio,
|
||||
Switch,
|
||||
Popconfirm,
|
||||
Typography,
|
||||
message,
|
||||
} from 'antd'
|
||||
import { PlusOutlined, ReloadOutlined, UserOutlined } from '@ant-design/icons'
|
||||
import { platformApi } from '../api'
|
||||
import type { MerchantMember, MerchantRole } from '../types'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
|
||||
interface MerchantMembersDrawerProps {
|
||||
open: boolean
|
||||
merchantId: number | null
|
||||
merchantName?: string
|
||||
merchantCode?: string
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function MerchantMembersDrawer({
|
||||
open,
|
||||
merchantId,
|
||||
merchantName,
|
||||
merchantCode,
|
||||
onClose,
|
||||
}: MerchantMembersDrawerProps) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [members, setMembers] = useState<MerchantMember[]>([])
|
||||
const [roles, setRoles] = useState<MerchantRole[]>([])
|
||||
|
||||
// 添加成员弹窗状态
|
||||
const [addModalOpen, setAddModalOpen] = useState(false)
|
||||
const [addMode, setAddMode] = useState<'create' | 'link'>('create')
|
||||
const [addLoading, setAddLoading] = useState(false)
|
||||
const [addForm] = Form.useForm()
|
||||
|
||||
// 编辑成员弹窗状态
|
||||
const [editModalOpen, setEditModalOpen] = useState(false)
|
||||
const [editingMember, setEditingMember] = useState<MerchantMember | null>(null)
|
||||
const [editLoading, setEditLoading] = useState(false)
|
||||
const [editForm] = Form.useForm()
|
||||
|
||||
const loadData = async (mid: number) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const [membersData, rolesData] = await Promise.all([
|
||||
platformApi.members(mid),
|
||||
platformApi.roles(mid),
|
||||
])
|
||||
setMembers(membersData || [])
|
||||
setRoles(rolesData || [])
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '获取成员列表失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (open && merchantId) {
|
||||
void loadData(merchantId)
|
||||
} else {
|
||||
setMembers([])
|
||||
setRoles([])
|
||||
}
|
||||
}, [open, merchantId])
|
||||
|
||||
const roleNameMap = new Map<string, string>()
|
||||
roles.forEach((r) => roleNameMap.set(r.code, r.name))
|
||||
|
||||
const handleOpenAdd = () => {
|
||||
addForm.resetFields()
|
||||
setAddMode('create')
|
||||
setAddModalOpen(true)
|
||||
}
|
||||
|
||||
const handleAddSubmit = async () => {
|
||||
if (!merchantId) return
|
||||
try {
|
||||
const values = await addForm.validateFields()
|
||||
setAddLoading(true)
|
||||
const payload: {
|
||||
role: string
|
||||
is_default?: boolean
|
||||
user_id?: number
|
||||
username?: string
|
||||
password?: string
|
||||
nickname?: string
|
||||
} = {
|
||||
role: values.role,
|
||||
is_default: !!values.is_default,
|
||||
}
|
||||
if (addMode === 'link') {
|
||||
payload.user_id = Number(values.user_id)
|
||||
} else {
|
||||
payload.username = values.username
|
||||
payload.password = values.password
|
||||
payload.nickname = values.nickname
|
||||
}
|
||||
await platformApi.addMember(merchantId, payload)
|
||||
message.success('成员添加成功')
|
||||
setAddModalOpen(false)
|
||||
void loadData(merchantId)
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '添加成员失败')
|
||||
} finally {
|
||||
setAddLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleOpenEdit = (record: MerchantMember) => {
|
||||
setEditingMember(record)
|
||||
editForm.setFieldsValue({
|
||||
role: record.role,
|
||||
status: record.status === 1,
|
||||
is_default: record.is_default,
|
||||
})
|
||||
setEditModalOpen(true)
|
||||
}
|
||||
|
||||
const handleEditSubmit = async () => {
|
||||
if (!merchantId || !editingMember) return
|
||||
try {
|
||||
const values = await editForm.validateFields()
|
||||
setEditLoading(true)
|
||||
await platformApi.updateMember(merchantId, editingMember.id, {
|
||||
role: values.role,
|
||||
status: values.status ? 1 : 0,
|
||||
is_default: !!values.is_default,
|
||||
})
|
||||
message.success('成员更新成功')
|
||||
setEditModalOpen(false)
|
||||
void loadData(merchantId)
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '更新成员失败')
|
||||
} finally {
|
||||
setEditLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemove = async (memberId: number) => {
|
||||
if (!merchantId) return
|
||||
try {
|
||||
await platformApi.removeMember(merchantId, memberId)
|
||||
message.success('已移除成员')
|
||||
void loadData(merchantId)
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '移除失败')
|
||||
}
|
||||
}
|
||||
|
||||
const columns: ColumnsType<MerchantMember> = [
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: ['user', 'username'],
|
||||
key: 'username',
|
||||
render: (text) => (
|
||||
<Space size="small">
|
||||
<UserOutlined style={{ color: '#94a3b8' }} />
|
||||
<Typography.Text strong>{text}</Typography.Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '姓名 / 昵称',
|
||||
dataIndex: ['user', 'nickname'],
|
||||
key: 'nickname',
|
||||
render: (text) => text || '-',
|
||||
},
|
||||
{
|
||||
title: '角色',
|
||||
dataIndex: 'role',
|
||||
key: 'role',
|
||||
render: (role: string) => {
|
||||
const name = roleNameMap.get(role) || (role === 'owner' ? '负责人' : role)
|
||||
const color = role === 'owner' ? 'gold' : 'blue'
|
||||
return <Tag color={color}>{name}</Tag>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: number) =>
|
||||
status === 1 ? (
|
||||
<Badge status="success" text="启用" />
|
||||
) : (
|
||||
<Badge status="error" text="禁用" />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '默认商户',
|
||||
dataIndex: 'is_default',
|
||||
key: 'is_default',
|
||||
render: (val: boolean) =>
|
||||
val ? <Tag color="green">默认</Tag> : <Typography.Text type="secondary">-</Typography.Text>,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 140,
|
||||
render: (_, record) => {
|
||||
const isOwner = record.role === 'owner'
|
||||
return (
|
||||
<Space size="small">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
disabled={isOwner}
|
||||
onClick={() => handleOpenEdit(record)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
{isOwner ? (
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
负责人
|
||||
</Typography.Text>
|
||||
) : (
|
||||
<Popconfirm
|
||||
title="确定从该商户移除此成员?"
|
||||
description="移除后该账号将无法再访问该商户后台"
|
||||
onConfirm={() => handleRemove(record.id)}
|
||||
okText="确认"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button type="link" size="small" danger>
|
||||
移除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const titleText = merchantName
|
||||
? `成员管理 · ${merchantName}${merchantCode ? ` (${merchantCode})` : ''}`
|
||||
: '商户成员管理'
|
||||
|
||||
return (
|
||||
<>
|
||||
<Drawer
|
||||
title={titleText}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
width={760}
|
||||
destroyOnClose
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => merchantId && void loadData(merchantId)}
|
||||
loading={loading}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleOpenAdd}>
|
||||
添加成员
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={members}
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
size="middle"
|
||||
/>
|
||||
</Drawer>
|
||||
|
||||
{/* 添加成员弹窗 */}
|
||||
<Modal
|
||||
title="添加商户成员"
|
||||
open={addModalOpen}
|
||||
onCancel={() => setAddModalOpen(false)}
|
||||
onOk={handleAddSubmit}
|
||||
confirmLoading={addLoading}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={addForm} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<Form.Item label="添加方式">
|
||||
<Radio.Group
|
||||
value={addMode}
|
||||
onChange={(e) => setAddMode(e.target.value)}
|
||||
buttonStyle="solid"
|
||||
>
|
||||
<Radio.Button value="create">新建账号并绑定</Radio.Button>
|
||||
<Radio.Button value="link">关联已有平台账号</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
{addMode === 'create' ? (
|
||||
<>
|
||||
<Form.Item
|
||||
name="username"
|
||||
label="用户名"
|
||||
rules={[
|
||||
{ required: true, message: '请输入用户名' },
|
||||
{ min: 3, message: '用户名至少 3 个字符' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="员工登录用户名" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="password"
|
||||
label="初始密码"
|
||||
rules={[
|
||||
{ required: true, message: '请输入密码' },
|
||||
{ min: 6, message: '密码至少 6 个字符' },
|
||||
]}
|
||||
>
|
||||
<Input.Password placeholder="员工登录密码" />
|
||||
</Form.Item>
|
||||
<Form.Item name="nickname" label="姓名 / 昵称">
|
||||
<Input placeholder="员工真实姓名或备注昵称" />
|
||||
</Form.Item>
|
||||
</>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="user_id"
|
||||
label="平台账号 UserID"
|
||||
rules={[{ required: true, message: '请输入平台账号 UserID' }]}
|
||||
>
|
||||
<Input placeholder="输入已存在的平台账号 ID" type="number" />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
name="role"
|
||||
label="所属角色"
|
||||
rules={[{ required: true, message: '请选择角色' }]}
|
||||
>
|
||||
<Select placeholder="请选择角色">
|
||||
{roles.map((r) => (
|
||||
<Select.Option key={r.code} value={r.code}>
|
||||
{r.name} ({r.code})
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="is_default" label="设为该账号的默认商户" valuePropName="checked">
|
||||
<Switch defaultChecked={addMode === 'create'} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 编辑成员角色与状态弹窗 */}
|
||||
<Modal
|
||||
title={`编辑成员:${editingMember?.user?.username || ''}`}
|
||||
open={editModalOpen}
|
||||
onCancel={() => setEditModalOpen(false)}
|
||||
onOk={handleEditSubmit}
|
||||
confirmLoading={editLoading}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={editForm} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<Form.Item
|
||||
name="role"
|
||||
label="角色"
|
||||
rules={[{ required: true, message: '请选择角色' }]}
|
||||
>
|
||||
<Select placeholder="请选择角色">
|
||||
{roles
|
||||
.filter((r) => r.code !== 'owner')
|
||||
.map((r) => (
|
||||
<Select.Option key={r.code} value={r.code}>
|
||||
{r.name} ({r.code})
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="status" label="账号状态" valuePropName="checked">
|
||||
<Switch checkedChildren="启用" unCheckedChildren="禁用" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="is_default" label="设为该账号的默认商户" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -229,16 +229,12 @@ export default function MainLayout() {
|
||||
return localStorage.getItem(SIDEBAR_COLLAPSED_KEY) === 'true'
|
||||
})
|
||||
const { user, logout, refreshProfile, isAdmin, merchant, merchantPermissions } = useAuth()
|
||||
// 管理员的商户上下文来自 localStorage(即请求头 X-Merchant-ID 的来源),
|
||||
// 之前只会在「平台商户」页点「成员」时被写入,界面上没有任何提示——
|
||||
// 管理员很容易以为自己看的是全平台数据,实际被静默框定在某个商户里。
|
||||
const [activeMerchantId] = useState<string | null>(() => localStorage.getItem('merchant_id'))
|
||||
const merchantContextLabel = merchant?.name || (activeMerchantId ? `商户 #${activeMerchantId}` : '默认商户')
|
||||
const exitMerchantContext = () => {
|
||||
localStorage.removeItem('merchant_id')
|
||||
// 整页刷新:上下文变了,当前页已经加载的数据也必须按新上下文重新取一遍。
|
||||
window.location.reload()
|
||||
}
|
||||
useEffect(() => {
|
||||
// 管理员始终保持平台全局视角,清除任何残留的商户上下文
|
||||
if (isAdmin && localStorage.getItem('merchant_id')) {
|
||||
localStorage.removeItem('merchant_id')
|
||||
}
|
||||
}, [isAdmin])
|
||||
const [passwordOpen, setPasswordOpen] = useState(false)
|
||||
const [passwordSaving, setPasswordSaving] = useState(false)
|
||||
const [passwordForm] = Form.useForm()
|
||||
@@ -431,22 +427,6 @@ export default function MainLayout() {
|
||||
<div className="app-header__right">
|
||||
{isAdmin && (
|
||||
<Space size="small">
|
||||
<Tooltip title="商户后台页面(发货订单 / 商品 / 钱包 / 成员等)都按这个商户隔离;首页看板与「下游订单」是平台口径,不受它影响">
|
||||
<Tag
|
||||
color={activeMerchantId ? 'orange' : 'default'}
|
||||
bordered={false}
|
||||
style={{ margin: 0, fontSize: 11.5, fontWeight: 600 }}
|
||||
>
|
||||
当前商户:{merchantContextLabel}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
{activeMerchantId && (
|
||||
<Tooltip title="回到你的默认商户上下文(自营商户)">
|
||||
<Button type="text" size="small" onClick={exitMerchantContext}>
|
||||
退出商户视角
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title="开放 API 接口文档">
|
||||
<Button
|
||||
type="text"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
@@ -23,6 +22,7 @@ import { platformApi } from '../api'
|
||||
import type { Merchant, MerchantOwnerAccount, PageResult, ProductCatalogItem, WalletAccount } from '../types'
|
||||
import { formatDateTime } from '../utils/time'
|
||||
import { PageHeader } from '../components/PageHeader'
|
||||
import { MerchantMembersDrawer } from '../components/MerchantMembersDrawer'
|
||||
|
||||
const featureOptions = [
|
||||
{ value: 'products', label: '商品' },
|
||||
@@ -33,9 +33,10 @@ const featureOptions = [
|
||||
]
|
||||
|
||||
export default function PlatformMerchants() {
|
||||
const navigate = useNavigate()
|
||||
const [data, setData] = useState<PageResult<Merchant>>({ list: [], total: 0, page: 1, size: 10 })
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [membersDrawerOpen, setMembersDrawerOpen] = useState(false)
|
||||
const [membersDrawerMerchant, setMembersDrawerMerchant] = useState<Merchant | null>(null)
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
const [settingsOpen, setSettingsOpen] = useState(false)
|
||||
const [assignOpen, setAssignOpen] = useState(false)
|
||||
@@ -344,8 +345,8 @@ export default function PlatformMerchants() {
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
localStorage.setItem('merchant_id', String(record.id))
|
||||
navigate('/merchant-members')
|
||||
setMembersDrawerMerchant(record)
|
||||
setMembersDrawerOpen(true)
|
||||
}}
|
||||
>
|
||||
成员
|
||||
@@ -752,6 +753,17 @@ export default function PlatformMerchants() {
|
||||
</Typography.Text>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<MerchantMembersDrawer
|
||||
open={membersDrawerOpen}
|
||||
merchantId={membersDrawerMerchant?.id ?? null}
|
||||
merchantName={membersDrawerMerchant?.name}
|
||||
merchantCode={membersDrawerMerchant?.code}
|
||||
onClose={() => {
|
||||
setMembersDrawerOpen(false)
|
||||
setMembersDrawerMerchant(null)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { MerchantMember, MerchantMemberGroup, PageResult, User } from '../t
|
||||
import { formatDateTime } from '../utils/time'
|
||||
import { useAuth } from '../store/authContext'
|
||||
import { roleText } from './merchantCenterUtils'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { MerchantMembersDrawer } from '../components/MerchantMembersDrawer'
|
||||
|
||||
type AdminFormValues = {
|
||||
username: string
|
||||
@@ -19,7 +19,9 @@ type AdminFormValues = {
|
||||
|
||||
export default function PlatformUsers() {
|
||||
const { user: currentUser } = useAuth()
|
||||
const navigate = useNavigate()
|
||||
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 })
|
||||
@@ -169,12 +171,21 @@ export default function PlatformUsers() {
|
||||
{
|
||||
title: '操作',
|
||||
width: 160,
|
||||
render: (_, record) => <Button type="link" size="small" onClick={() => {
|
||||
localStorage.setItem('merchant_id', String(record.merchant_id))
|
||||
navigate('/merchant-members')
|
||||
}}>管理商户成员</Button>,
|
||||
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>
|
||||
),
|
||||
},
|
||||
], [navigate])
|
||||
], [])
|
||||
|
||||
const items = useMemo(() => [
|
||||
{
|
||||
@@ -236,6 +247,17 @@ export default function PlatformUsers() {
|
||||
{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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user