feat(assoc): v4.0.1 合伙人关联码、分佣账单与 H5 用户管理
订单佣金只认关联用户;合伙人备注写入独立表;H5 增加用户管理与首页统计。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button, Popconfirm, Space, Table, Typography, message } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import type { PartnerAssocSummary, PartnerAssocUserItem } from '@dukang/shared-types';
|
||||
import { request, type HqProfile, type Paginated } from '../lib/api';
|
||||
import { fmtTime } from '../lib/constants';
|
||||
|
||||
function formatNicknameWithRemark(row: PartnerAssocUserItem) {
|
||||
const name = row.nickname?.trim() || '—';
|
||||
const remark = row.hqRemark?.trim();
|
||||
return remark ? `${name}(${remark})` : name;
|
||||
}
|
||||
|
||||
type PartnerAssocPanelProps = {
|
||||
partnerId: string;
|
||||
};
|
||||
|
||||
export default function PartnerAssocPanel({ partnerId }: PartnerAssocPanelProps) {
|
||||
const [summary, setSummary] = useState<PartnerAssocSummary | null>(null);
|
||||
const [users, setUsers] = useState<PartnerAssocUserItem[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [issuing, setIssuing] = useState(false);
|
||||
const [canEditAssoc, setCanEditAssoc] = useState(false);
|
||||
|
||||
const loadSummary = useCallback(async () => {
|
||||
const data = await request<PartnerAssocSummary>(`/admin/partners/${partnerId}/assoc`);
|
||||
setSummary(data);
|
||||
}, [partnerId]);
|
||||
|
||||
const loadUsers = useCallback(async (p = 1) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await request<Paginated<PartnerAssocUserItem>>(
|
||||
`/admin/partners/${partnerId}/assoc/users?page=${p}&pageSize=20`,
|
||||
);
|
||||
setUsers(res.items ?? []);
|
||||
setTotal(res.total ?? 0);
|
||||
setPage(p);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [partnerId]);
|
||||
|
||||
useEffect(() => {
|
||||
void request<HqProfile>('/admin/auth/me')
|
||||
.then((p) => setCanEditAssoc((p.permissionKeys ?? []).includes('users_partner_assoc')))
|
||||
.catch(() => setCanEditAssoc(false));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void loadSummary().catch((e) => message.error(e instanceof Error ? e.message : '加载关联码失败'));
|
||||
void loadUsers(1).catch(() => undefined);
|
||||
}, [loadSummary, loadUsers]);
|
||||
|
||||
async function reissue() {
|
||||
setIssuing(true);
|
||||
try {
|
||||
await request(`/admin/partners/${partnerId}/assoc/qrcode`, { method: 'POST' });
|
||||
message.success('已重新生成关联码');
|
||||
await loadSummary();
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '生成失败');
|
||||
} finally {
|
||||
setIssuing(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function unbind(userId: string) {
|
||||
try {
|
||||
await request(`/admin/partners/${partnerId}/assoc/users/${userId}/unbind`, { method: 'POST' });
|
||||
message.success('已解绑');
|
||||
await Promise.all([loadSummary(), loadUsers(page)]);
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '解绑失败');
|
||||
}
|
||||
}
|
||||
|
||||
const columns: ColumnsType<PartnerAssocUserItem> = [
|
||||
{ title: '用户编号', dataIndex: 'userNo', width: 140 },
|
||||
{
|
||||
title: '用户',
|
||||
dataIndex: 'nickname',
|
||||
render: (_, row) => (
|
||||
<Link
|
||||
className="admin-primary-link"
|
||||
to={`/users?userId=${encodeURIComponent(row.id)}`}
|
||||
title="在用户列表中查看"
|
||||
>
|
||||
{formatNicknameWithRemark(row)}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{ title: '手机', dataIndex: 'phone', render: (v) => v || '—' },
|
||||
{ title: '关联时间', dataIndex: 'boundAt', render: (v) => (v ? fmtTime(v) : '—') },
|
||||
{ title: '已付订单', dataIndex: 'orderCount', width: 90 },
|
||||
...(canEditAssoc
|
||||
? [
|
||||
{
|
||||
title: '操作',
|
||||
width: 90,
|
||||
render: (_: unknown, row: PartnerAssocUserItem) => (
|
||||
<Popconfirm title="解绑后该用户新单不再计订单佣金" onConfirm={() => void unbind(row.id)}>
|
||||
<Button type="link" size="small" danger>
|
||||
解绑
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
),
|
||||
} satisfies ColumnsType<PartnerAssocUserItem>[number],
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Space align="start" style={{ marginBottom: 16 }} wrap>
|
||||
{summary?.qrcodeUrl ? (
|
||||
<img src={summary.qrcodeUrl} alt="关联码" style={{ width: 160, height: 160, background: '#fff' }} />
|
||||
) : (
|
||||
<Typography.Text type="secondary">尚未生成关联码</Typography.Text>
|
||||
)}
|
||||
<div>
|
||||
<Typography.Paragraph style={{ marginBottom: 8 }}>
|
||||
<Link to={`/users?assocPartnerAccountId=${encodeURIComponent(partnerId)}`}>
|
||||
关联用户 {summary?.userCount ?? 0} 人
|
||||
</Link>
|
||||
</Typography.Paragraph>
|
||||
<Button loading={issuing} onClick={() => void reissue()}>
|
||||
{summary?.qrcodeUrl ? '补发关联码' : '生成关联码'}
|
||||
</Button>
|
||||
</div>
|
||||
</Space>
|
||||
<Table
|
||||
rowKey="id"
|
||||
size="small"
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={users}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: 20,
|
||||
total,
|
||||
onChange: (p) => void loadUsers(p),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user