Files
dukang/apps/admin-web/src/components/PartnerAssocPanel.tsx
T
jacy cdc4b82931 feat(ops): v4.0.7 HQ 活动图快链与勾选导出
HQ 指定一张活动图为勾选主合伙人合成 PNG/zip;城市合伙人页增加快链与单下/导出。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 15:12:10 +08:00

169 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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';
import ActivityPosterDownloadModal from './ActivityPosterDownloadModal';
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 [canDownloadPosters, setCanDownloadPosters] = useState(false);
const [posterDownloadOpen, setPosterDownloadOpen] = 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) => {
const keys = p.permissionKeys ?? [];
setCanEditAssoc(keys.includes('users_partner_assoc'));
setCanDownloadPosters(keys.includes('activity_posters'));
})
.catch(() => {
setCanEditAssoc(false);
setCanDownloadPosters(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>
{canDownloadPosters ? (
<Button onClick={() => setPosterDownloadOpen(true)}>下载活动图</Button>
) : null}
</div>
</Space>
<Table
rowKey="id"
size="small"
loading={loading}
columns={columns}
dataSource={users}
pagination={{
current: page,
pageSize: 20,
total,
onChange: (p) => void loadUsers(p),
}}
/>
<ActivityPosterDownloadModal
open={posterDownloadOpen}
partnerIds={[partnerId]}
onClose={() => setPosterDownloadOpen(false)}
/>
</div>
);
}