9c8d5f2cad
订单佣金只认关联用户;合伙人备注写入独立表;H5 增加用户管理与首页统计。 Co-authored-by: Cursor <cursoragent@cursor.com>
151 lines
5.0 KiB
TypeScript
151 lines
5.0 KiB
TypeScript
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>
|
||
);
|
||
}
|