v4.0.20版本迭代推广码增加渠道负责人

This commit is contained in:
2026-09-17 09:06:29 +08:00
parent 1867e7ea55
commit b9921b1a75
36 changed files with 12138 additions and 112 deletions
@@ -1,4 +1,4 @@
import { useState, type CSSProperties } from 'react';
import { useEffect, useMemo, useState, type CSSProperties } from 'react';
import { useNavigate, useOutletContext } from 'react-router-dom';
import { QuestionCircleOutlined } from '@ant-design/icons';
import {
@@ -23,8 +23,8 @@ import {
PROMO_CODE_STATUS_LABELS,
promoConversion,
} from '@dukang/shared-types';
import { request } from '../../lib/api';
import { fmtTime } from '../../lib/constants';
import { request, type Paginated } from '../../lib/api';
import { ADMIN_OPTIONS_PAGE_SIZE, fmtTime } from '../../lib/constants';
import type { PromoCodeDetailContext } from './PromoCodeDetailLayout';
import PromoCodeMetricsPanel from './PromoCodeMetricsPanel';
@@ -51,6 +51,19 @@ const ORDER_HINT = '下单时绑定当时的归因推广码,仅统计已完成
const CONVERSION_HINT =
'已完成订单数 ÷ 扫码进入次数(人次)。同一人多次扫码会放大分母,未登录扫码也计入。';
type PartnerOption = { id: string; companyName?: string | null; name?: string | null; phone?: string | null };
function formatPartnerLabel(p?: { companyName?: string | null; name?: string | null; phone?: string | null; id: string } | null) {
if (!p) return '—';
const title = p.companyName || p.name || p.id;
return p.phone ? `${title} · ${p.phone}` : title;
}
function formatPartnerNames(partners?: Array<{ companyName?: string | null; name?: string | null; id: string }> | null) {
if (!partners?.length) return '—';
return partners.map((p) => p.companyName || p.name || p.id).join('、');
}
function StatHint({ label, hint }: { label: string; hint: string }) {
return (
<span>
@@ -86,10 +99,30 @@ export default function PromoCodeDetailPage() {
const [editForm] = Form.useForm();
const [editOpen, setEditOpen] = useState(false);
const [saving, setSaving] = useState(false);
const [partners, setPartners] = useState<PartnerOption[]>([]);
const stats = detail.stats;
async function handleEdit(values: Record<string, string>) {
const partnerSelectOptions = useMemo(() => {
const map = new Map(partners.map((p) => [p.id, p]));
for (const p of detail.channelOwners ?? []) map.set(p.id, p);
if (detail.assocPartner) map.set(detail.assocPartner.id, detail.assocPartner);
return [...map.values()].map((p) => ({ value: p.id, label: formatPartnerLabel(p) }));
}, [partners, detail.channelOwners, detail.assocPartner]);
useEffect(() => {
void request<Paginated<PartnerOption>>(`/admin/partners?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`)
.then((res) => setPartners(res.items ?? []))
.catch(() => setPartners([]));
}, []);
async function handleEdit(values: {
name: string;
scene: string;
remark?: string;
channelOwnerPartnerIds?: string[];
assocPartnerAccountId?: string;
}) {
setSaving(true);
try {
await request(`/admin/promo-codes/${detail.id}`, {
@@ -97,7 +130,8 @@ export default function PromoCodeDetailPage() {
body: JSON.stringify({
name: values.name,
scene: values.scene,
ownerUserId: values.ownerUserId?.trim() || null,
channelOwnerPartnerIds: values.channelOwnerPartnerIds ?? [],
assocPartnerAccountId: values.assocPartnerAccountId || null,
remark: values.remark?.trim() || null,
}),
});
@@ -128,7 +162,8 @@ export default function PromoCodeDetailPage() {
name: detail.name,
scene: detail.scene,
remark: detail.remark,
ownerUserId: detail.ownerUser?.id,
channelOwnerPartnerIds: (detail.channelOwners ?? []).map((p) => p.id),
assocPartnerAccountId: detail.assocPartner?.id,
});
setEditOpen(true);
}}
@@ -194,7 +229,12 @@ export default function PromoCodeDetailPage() {
</Descriptions.Item>
<Descriptions.Item label="渠道负责人">
<span style={{ whiteSpace: 'nowrap' }}>
{detail.ownerUser?.userNo || detail.ownerUser?.phone || '—'}
{formatPartnerNames(detail.channelOwners)}
</span>
</Descriptions.Item>
<Descriptions.Item label="关联合伙人">
<span style={{ whiteSpace: 'nowrap' }}>
{formatPartnerLabel(detail.assocPartner)}
</span>
</Descriptions.Item>
<Descriptions.Item label="备注">{detail.remark || '—'}</Descriptions.Item>
@@ -309,8 +349,32 @@ export default function PromoCodeDetailPage() {
options={Object.entries(PROMO_CODE_SCENE_LABELS).map(([value, label]) => ({ value, label }))}
/>
</Form.Item>
<Form.Item name="ownerUserId" label="关联用户 ID">
<Input placeholder="留空表示解除关联" />
<Form.Item
name="channelOwnerPartnerIds"
label="渠道负责人"
extra="可空、可多选。被指定的主合伙人可在 H5 查看扫码/归因/订单数量。"
>
<Select
mode="multiple"
allowClear
showSearch
optionFilterProp="label"
placeholder="选择主合伙人"
options={partnerSelectOptions}
/>
</Form.Item>
<Form.Item
name="assocPartnerAccountId"
label="关联合伙人"
extra="可空。扫该码且尚未关联的用户将绑定该合伙人;已关联他人不换绑,不回刷历史。"
>
<Select
allowClear
showSearch
optionFilterProp="label"
placeholder="选择主合伙人"
options={partnerSelectOptions}
/>
</Form.Item>
<Form.Item name="remark" label="备注">
<Input.TextArea rows={2} />