feat(assoc): v4.0.1 合伙人关联码、分佣账单与 H5 用户管理

订单佣金只认关联用户;合伙人备注写入独立表;H5 增加用户管理与首页统计。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-30 14:35:39 +08:00
parent 3b669f7e38
commit 9c8d5f2cad
125 changed files with 6355 additions and 1436 deletions
@@ -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>
);
}