'use client'; // CPS 优惠券分发与对账。平台:美团(actId+sid,完整对账) / 淘宝(淘口令) / 京东(链接)。 // 淘宝/京东无 API → 只统计点击,对账字段显示 "-"。单页 Tabs:对账统计/群管理/活动管理/订单明细。 import { useCallback, useEffect, useState } from 'react'; import Link from 'next/link'; import type { ColumnsType } from 'antd/es/table'; import { Button, Card, Checkbox, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Statistic, Table, Tabs, Tag, Typography, Upload, message, } from 'antd'; import { api, errMsg } from '@/lib/api'; import { canDo } from '@/lib/auth'; import { formatUtcTime, yuan } from '@/lib/format'; import { usePagedList } from '@/lib/usePagedList'; import type { CpsActivity, CpsGroup, CpsGroupStat, CpsOrder, CpsReconcileResult, CpsReferralLinks, CpsStats, } from '@/lib/types'; const MT_STATUS: Record = { '2': { text: '已付款', color: 'blue' }, '3': { text: '已完成', color: 'cyan' }, '4': { text: '已取消', color: 'red' }, '5': { text: '风控', color: 'orange' }, '6': { text: '已结算', color: 'green' }, }; const PLATFORM_LABEL: Record = { meituan: '美团', taobao: '淘宝', jd: '京东' }; const PLATFORM_COLOR: Record = { meituan: 'gold', taobao: 'volcano', jd: 'red' }; const PLATFORM_OPTIONS = [ { label: '美团', value: 'meituan' }, { label: '淘宝', value: 'taobao' }, { label: '京东', value: 'jd' }, ]; const fmtRate = (r: string | null) => (r ? `${Number(r) / 100}%` : '-'); const DASH = -; const platformTags = (ps: string[] | undefined) => (ps || []).map((p) => ( {PLATFORM_LABEL[p] || p} )); export default function CpsPage() { return (

CPS 优惠券分发与对账

美团(actId + sid,可完整对账下单/佣金) / 淘宝(淘口令) / 京东(链接)。淘宝、京东无开放 API, 只统计点击,对账字段显示「-」。发群链接都是咱的落地页 /c/xxx。 }, { key: 'groups', label: '群管理', children: }, { key: 'activities', label: '活动管理', children: }, { key: 'orders', label: '订单明细', children: }, ]} />
); } // ───────────── 对账统计 ───────────── function StatsTab() { const [days, setDays] = useState(30); const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [reconciling, setReconciling] = useState(false); const canReconcile = canDo(['finance']); const load = useCallback(async (d: number) => { setLoading(true); try { const resp = await api.get('/admin/api/cps/stats', { params: { days: d } }); setData(resp.data); } catch (e) { message.error(errMsg(e)); } finally { setLoading(false); } }, []); useEffect(() => { load(days); }, [days, load]); const reconcile = async () => { setReconciling(true); try { const resp = await api.post('/admin/api/cps/orders/reconcile', null, { params: { days }, }); const r = resp.data; message.success(`对账完成:拉取 ${r.fetched} 单(新增 ${r.inserted}、更新 ${r.updated})`); load(days); } catch (e) { message.error(errMsg(e)); } finally { setReconciling(false); } }; const columns: ColumnsType = [ { title: '群', dataIndex: 'name', width: 140, ellipsis: true, fixed: 'left', render: (v: string, r) => r.group_id ? {v} : v, }, { title: '平台', key: 'platforms', width: 130, render: (_, r) => platformTags(r.platforms) }, { title: '人数', dataIndex: 'member_count', width: 64, render: (v) => v ?? '-' }, { title: '点击', dataIndex: 'click_pv', width: 64 }, { title: '独立', dataIndex: 'click_uv', width: 64 }, { title: '复制', dataIndex: 'copy_pv', width: 64, render: (v: number) => (v ? v : DASH), }, { title: '有效单', dataIndex: 'order_count', width: 70, render: (v: number | null) => (v == null ? DASH : v), }, { title: '取消/风控', dataIndex: 'canceled_count', width: 80, render: (v: number | null) => (v == null ? DASH : v ? {v} : 0), }, { title: '点击→下单', key: 'rate', width: 88, render: (_, r) => r.order_count == null ? DASH : r.click_uv ? `${((r.order_count / r.click_uv) * 100).toFixed(0)}%` : '-', }, { title: '成交额', dataIndex: 'gmv_cents', width: 96, render: (v: number | null) => (v == null ? DASH : yuan(v)), }, { title: '预估佣金', dataIndex: 'est_commission_cents', width: 100, sorter: (a, b) => (a.est_commission_cents || 0) - (b.est_commission_cents || 0), render: (v: number | null) => (v == null ? DASH : {yuan(v)}), }, { title: '已结算佣金', dataIndex: 'settled_commission_cents', width: 100, render: (v: number | null) => (v == null ? DASH : {yuan(v)}), }, ]; return (
统计区间 setKeyword(e.target.value)} onPressEnter={() => setApplied({ keyword: keyword || undefined })} allowClear style={{ width: 180 }} /> {canManage && } `共 ${t} 个群`, onChange, }} scroll={{ x: 1090 }} /> setCreateOpen(false)} onDone={reload} /> setEditing(null)} onDone={reload} /> setLinkGroup(null)} /> ); } function GroupFormModal({ open, group, onClose, onDone, }: { open: boolean; group: CpsGroup | null; onClose: () => void; onDone: () => void; }) { const [form] = Form.useForm(); const isEdit = !!group; const watchPlatforms = (Form.useWatch('platforms', form) as string[] | undefined) || []; const hasMeituan = watchPlatforms.includes('meituan'); useEffect(() => { if (open) { if (group) { form.setFieldsValue({ name: group.name, platforms: group.platforms, member_count: group.member_count, remark: group.remark, status: group.status, }); } else { form.resetFields(); form.setFieldsValue({ platforms: ['meituan'] }); } } }, [open, group, form]); const submit = async () => { const v = await form.validateFields(); try { if (isEdit && group) { await api.patch(`/admin/api/cps/groups/${group.id}`, v); message.success('已更新群'); } else { await api.post('/admin/api/cps/groups', v); message.success('已新建群'); } onClose(); onDone(); } catch (e) { message.error(errMsg(e)); } }; return (
{hasMeituan && !isEdit && ( )} {hasMeituan && isEdit && ( 当前 sid:{group?.sid || '(无,保存后如缺会在生成美团链接时报错)'} )} {isEdit && (
`共 ${t} 个活动`, onChange, }} scroll={{ x: 1200 }} /> setCreateOpen(false)} onDone={reload} /> setEditing(null)} onDone={reload} /> ); } // 淘宝活动落地页图选择器:上传新图 或 点选已有图。受控组件(value = image_url 绝对 URL)。 function ImagePicker({ value, onChange }: { value?: string | null; onChange?: (v: string) => void }) { const [existing, setExisting] = useState([]); const [uploading, setUploading] = useState(false); useEffect(() => { api .get<{ images: string[] }>('/admin/api/cps/activity-images') .then((r) => setExisting(r.data.images || [])) .catch(() => {}); }, []); const upload = async (file: File) => { setUploading(true); try { const fd = new FormData(); fd.append('file', file); const r = await api.post<{ url: string }>('/admin/api/cps/upload-image', fd); onChange?.(r.data.url); setExisting((prev) => (prev.includes(r.data.url) ? prev : [r.data.url, ...prev])); message.success('图片已上传'); } catch (e) { message.error(errMsg(e, '上传失败')); } finally { setUploading(false); } }; const thumb = (u: string, w: number, selected: boolean) => ( 落地页图 onChange?.(u)} style={{ width: w, height: Math.round(w * 1.27), objectFit: 'cover', borderRadius: 6, cursor: 'pointer', border: selected ? '2px solid #ff4d4f' : '2px solid #f0f0f0', }} /> ); return (
{ upload(file); return false; // 阻止 antd 默认上传,走我们自己的端点 }} > {value && (
当前: {thumb(value, 90, true)}
)} {existing.length > 0 && (
或选择已有图:
{existing.map((u) => thumb(u, 56, u === value))}
)}
); } function ActivityFormModal({ open, activity, onClose, onDone, }: { open: boolean; activity: CpsActivity | null; onClose: () => void; onDone: () => void; }) { const [form] = Form.useForm(); const isEdit = !!activity; const platform = (Form.useWatch('platform', form) as string | undefined) || 'meituan'; useEffect(() => { if (open) { if (activity) { form.setFieldsValue({ name: activity.name, platform: activity.platform, act_id: activity.act_id, product_view_sign: activity.product_view_sign, payload: activity.payload, image_url: activity.image_url, remark: activity.remark, status: activity.status, }); } else { form.resetFields(); form.setFieldsValue({ platform: 'meituan' }); } } }, [open, activity, form]); const submit = async () => { const v = await form.validateFields(); if (v.platform === 'meituan') { if (!v.act_id && !v.product_view_sign) { message.warning('美团活动:actId 与 productViewSign 至少填一个'); return; } } else if (!v.payload) { message.warning(v.platform === 'taobao' ? '请填淘口令' : '请填京东链接'); return; } try { if (isEdit && activity) { await api.patch(`/admin/api/cps/activities/${activity.id}`, v); message.success('已更新活动'); } else { await api.post('/admin/api/cps/activities', v); message.success('已新建活动'); } onClose(); onDone(); } catch (e) { message.error(errMsg(e)); } }; return ( )} {platform === 'taobao' && ( <> )} {platform === 'jd' && ( )} {isEdit && ( setSid(e.target.value)} onPressEnter={search} allowClear style={{ width: 150 }} />
`共 ${t} 单`, onChange, }} scroll={{ x: 1030 }} /> ); }