import { useEffect, useMemo, useState, type CSSProperties } from 'react';
import { useNavigate, useOutletContext } from 'react-router-dom';
import { QuestionCircleOutlined } from '@ant-design/icons';
import {
Button,
Card,
Col,
Descriptions,
Form,
Input,
Modal,
Popconfirm,
Row,
Select,
Space,
Statistic,
Tooltip,
Typography,
message,
} from 'antd';
import {
PROMO_CODE_SCENE_LABELS,
PROMO_CODE_STATUS_LABELS,
promoConversion,
} from '@dukang/shared-types';
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';
const descLabelStyle: CSSProperties = {
whiteSpace: 'nowrap',
width: 108,
};
const descContentStyle: CSSProperties = {
wordBreak: 'break-all',
};
const SCAN_HINT =
'按人次累计:每次扫码或带参进入记一次,未登录也计入;同一用户多次进入记多次。同一次打开内的重复上报、以及登录后补归因不重复计。不是按人去重。';
const ATTRIBUTION_HINT =
'首次触达本推广码的用户,按人计:每人只归因一次、只归一个码。已登录扫码时,若还没有归因记录则记到本码;未登录先扫码、登录后再补记。之后再扫其他码不会改。未登录连续扫多个码时,登录后归到最后一次缓存的码。与「扫码注册」不同:已有分享等其他来源的用户仍可计入归因,但不计入扫码注册。';
const REGISTER_HINT =
'用户来源仍是自然量时,才标记为本推广码。已有分享等其他来源的用户不计入,因此通常小于或等于归因用户数。';
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 (
{label}
e.stopPropagation()}
/>
);
}
async function downloadQrcode(url: string, filename: string) {
try {
const res = await fetch(url);
const blob = await res.blob();
const objectUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = objectUrl;
a.download = filename;
a.click();
URL.revokeObjectURL(objectUrl);
} catch {
window.open(url, '_blank');
}
}
export default function PromoCodeDetailPage() {
const navigate = useNavigate();
const { detail, reload } = useOutletContext();
const [editForm] = Form.useForm();
const [editOpen, setEditOpen] = useState(false);
const [saving, setSaving] = useState(false);
const [partners, setPartners] = useState([]);
const stats = detail.stats;
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>(`/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}`, {
method: 'PUT',
body: JSON.stringify({
name: values.name,
scene: values.scene,
channelOwnerPartnerIds: values.channelOwnerPartnerIds ?? [],
assocPartnerAccountId: values.assocPartnerAccountId || null,
remark: values.remark?.trim() || null,
}),
});
message.success('已保存');
setEditOpen(false);
await reload();
} catch (e) {
message.error(e instanceof Error ? e.message : '保存失败');
} finally {
setSaving(false);
}
}
return (
<>
{
await request(`/admin/promo-codes/${detail.id}/status`, {
method: 'PUT',
body: JSON.stringify({
status: detail.status === 'ACTIVE' ? 'DISABLED' : 'ACTIVE',
}),
});
message.success('状态已更新');
await reload();
}}
>
)}
>
{detail.name}
{detail.code}
{PROMO_CODE_SCENE_LABELS[detail.scene] || detail.scene}
{PROMO_CODE_STATUS_LABELS[detail.status] || detail.status}
{detail.id}
{detail.qrcodeId}
{detail.qrcodeUrl ? (
{detail.qrcodeUrl}
) : '—'}
{formatPartnerNames(detail.channelOwners)}
{formatPartnerLabel(detail.assocPartner)}
{detail.remark || '—'}
{fmtTime(detail.createdAt)}
{fmtTime(detail.updatedAt)}
{detail.qrcodeUrl ? (
<>
scene={detail.id}
>
) : (
暂无小程序码
)}
navigate(`/promo-codes/${detail.id}?eventType=SCAN`)}
>
}
value={stats?.scanCount ?? detail.scanCount}
/>
}
value={stats?.attributionCount ?? 0}
/>
}
value={stats?.registerCount ?? stats?.sourceMarkedCount ?? 0}
/>
navigate(`/orders?promoCodeId=${detail.id}&status=COMPLETED`)}
>
}
value={stats?.orderCount ?? detail.orderCount}
/>
}
value={promoConversion(stats?.scanCount ?? detail.scanCount, stats?.orderCount ?? detail.orderCount)}
/>
setEditOpen(false)}
footer={null}
destroyOnClose
>
>
);
}