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 >