v4.0.20版本迭代推广码增加渠道负责人
This commit is contained in:
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user