= { 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 优惠券分发与对账
- 当前仅接美团联盟。每群一个 sid:在「群管理」给群生成带 sid 的券链接发群,「对账统计」按 sid
- 拉回订单与佣金归群。点击数需独立跳转服务(第二期)。
+ 美团(actId + sid,可完整对账下单/佣金) / 淘宝(淘口令) / 京东(链接)。淘宝、京东无开放 API,
+ 只统计点击,对账字段显示「-」。发群链接都是咱的落地页 /c/xxx。
= [
- { title: '群', dataIndex: 'name', width: 160, ellipsis: true },
+ { title: '群', dataIndex: 'name', width: 140, ellipsis: true, fixed: 'left' },
+ { 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: 'sid',
- dataIndex: 'sid',
- width: 130,
- render: (v: string | null) => (v ? {v} : '-'),
+ 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: 'member_count', width: 70, render: (v) => v ?? '-' },
- { title: '点击', dataIndex: 'click_pv', width: 70 },
- { title: '独立访客', dataIndex: 'click_uv', width: 80 },
- { title: '有效单', dataIndex: 'order_count', width: 70 },
{
title: '取消/风控',
dataIndex: 'canceled_count',
- width: 85,
- render: (v: number) => (v ? {v} : 0),
+ width: 80,
+ render: (v: number | null) => (v == null ? DASH : v ? {v} : 0),
},
{
title: '点击→下单',
key: 'rate',
- width: 90,
+ width: 88,
render: (_, r) =>
- r.click_uv ? `${((r.order_count / r.click_uv) * 100).toFixed(0)}%` : '-',
+ 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: 'gmv_cents', width: 110, render: (v: number) => yuan(v) },
{
title: '预估佣金',
dataIndex: 'est_commission_cents',
- width: 110,
- sorter: (a, b) => a.est_commission_cents - b.est_commission_cents,
- render: (v: number) => {yuan(v)},
+ 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: 110,
- render: (v: number) => {yuan(v)},
+ width: 100,
+ render: (v: number | null) => (v == null ? DASH : {yuan(v)}),
},
];
@@ -172,14 +193,12 @@ function StatsTab() {
刷新对账(拉美团订单)
)}
-
- 佣金为美团预估口径,实际以「已结算」为准
-
+ 淘宝/京东无对账,「-」即无法对账;佣金以「已结算」为准
-
+
);
@@ -222,33 +241,33 @@ function GroupsTab() {
);
const canManage = canDo(['operator']);
- const [editing, setEditing] = useState(null); // null=不开,{} 新建用单独标志
+ const [editing, setEditing] = useState(null);
const [createOpen, setCreateOpen] = useState(false);
const [linkGroup, setLinkGroup] = useState(null);
const columns: ColumnsType = [
- { title: 'ID', dataIndex: 'id', width: 70 },
- { title: '群名', dataIndex: 'name', width: 180, ellipsis: true },
+ { title: 'ID', dataIndex: 'id', width: 60 },
+ { title: '群名', dataIndex: 'name', width: 160, ellipsis: true },
+ { title: '平台', key: 'platforms', width: 140, render: (_, g) => platformTags(g.platforms) },
{
title: 'sid',
dataIndex: 'sid',
- width: 150,
- render: (v: string) => {v},
+ width: 130,
+ render: (v: string | null) => (v ? {v} : DASH),
},
- { title: '人数', dataIndex: 'member_count', width: 80, render: (v) => v ?? '-' },
+ { title: '人数', dataIndex: 'member_count', width: 70, render: (v) => v ?? '-' },
{
title: '状态',
dataIndex: 'status',
- width: 90,
+ width: 80,
render: (s: string) => {s},
},
- { title: '备注', dataIndex: 'remark', width: 160, ellipsis: true, render: (v) => v || '-' },
{ title: '创建时间', dataIndex: 'created_at', width: 150, render: (v: string) => formatUtcTime(v) },
{
title: '操作',
key: 'op',
fixed: 'right',
- width: 170,
+ width: 150,
render: (_, g) => (
e.stopPropagation()}>
{canManage && setLinkGroup(g)}>生成链接}
@@ -272,9 +291,7 @@ function GroupsTab() {
- {canManage && (
-
- )}
+ {canManage && }
`共 ${t} 个群`,
onChange,
}}
- scroll={{ x: 1050 }}
+ scroll={{ x: 1040 }}
/>
- setCreateOpen(false)}
- onDone={reload}
- />
- setEditing(null)}
- onDone={reload}
- />
+ setCreateOpen(false)} onDone={reload} />
+ setEditing(null)} onDone={reload} />
setLinkGroup(null)} />
);
@@ -323,18 +330,22 @@ function GroupFormModal({
}) {
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]);
@@ -357,26 +368,32 @@ function GroupFormModal({
};
return (
-
+
- {!isEdit && (
+
+
+
+ {hasMeituan && !isEdit && (
)}
+ {hasMeituan && isEdit && (
+
+ 当前 sid:{group?.sid || '(无,保存后如缺会在生成美团链接时报错)'}
+
+ )}
@@ -400,13 +417,13 @@ function GroupFormModal({
function ReferralLinkModal({ group, onClose }: { group: CpsGroup | null; onClose: () => void }) {
const [activities, setActivities] = useState([]);
- const [activityId, setActivityId] = useState();
- const [result, setResult] = useState(null);
+ const [activityIds, setActivityIds] = useState([]);
+ const [result, setResult] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!group) {
- setActivityId(undefined);
+ setActivityIds([]);
setResult(null);
return;
}
@@ -416,16 +433,19 @@ function ReferralLinkModal({ group, onClose }: { group: CpsGroup | null; onClose
.catch((e) => message.error(errMsg(e)));
}, [group]);
+ // 只列群平台范围内的活动
+ const groupActivities = activities.filter((a) => (group?.platforms || []).includes(a.platform));
+
const generate = async () => {
- if (!group || !activityId) {
- message.warning('请选择活动');
+ if (!group || !activityIds.length) {
+ message.warning('请勾选要生成的活动');
return;
}
setLoading(true);
try {
- const resp = await api.post('/admin/api/cps/referral-link', {
+ const resp = await api.post('/admin/api/cps/referral-links', {
group_id: group.id,
- activity_id: activityId,
+ activity_ids: activityIds,
});
setResult(resp.data);
} catch (e) {
@@ -437,68 +457,68 @@ function ReferralLinkModal({ group, onClose }: { group: CpsGroup | null; onClose
return (
-
-
-
+
+ 勾选要发的活动,每个生成一条咱的落地页链接;复制后发到群里。
+
+ {groupActivities.length === 0 ? (
+
+ 该群平台({(group?.platforms || []).map((p) => PLATFORM_LABEL[p]).join('/')})下没有活动,请先到「活动管理」新建。
+
+ ) : (
+ setActivityIds(v as number[])}
+ style={{ width: '100%' }}
+ >
+
+ {groupActivities.map((a) => (
+
+ {PLATFORM_LABEL[a.platform]}
+ {a.name}
+ {a.act_id ? (actId={a.act_id}) : null}
+
+ ))}
+
+
+ )}
{result && (
-
-
👇 发群就用这条链接(已带 sid={result.sid},点击会被统计):
-
-
+ 👇 生成结果(逐条复制发群):
+ {result.results.map((item) => (
+
- {result.redirect_url}
-
-
- {!result.redirect_url.startsWith('http') && (
-
- ⚠️ 后端未配 CPS_REDIRECT_BASE,这是相对路径;请在 .env 配跳转域名后重启
-
- )}
-
- 美团原始链接(参考/备用,直接发不带点击统计):
-
- {Object.entries(result.link_map).map(([type, url]) => (
-
-
{LINK_TYPE_NAME[type] ?? type}
-
- {url.length > 56 ? `${url.slice(0, 56)}…` : url}
+
+ {PLATFORM_LABEL[item.platform]}
+ {item.activity_name}
+
+
+ {item.redirect_url}
))}
+ {result.results.some((i) => !i.redirect_url.startsWith('http')) && (
+
+ ⚠️ 部分为相对路径:后端未配 CPS_REDIRECT_BASE,请配跳转域名后重启。
+
+ )}
)}
@@ -515,26 +535,31 @@ function ActivitiesTab() {
const [createOpen, setCreateOpen] = useState(false);
const columns: ColumnsType
= [
- { title: 'ID', dataIndex: 'id', width: 70 },
- { title: '平台', dataIndex: 'platform', width: 90, render: (v: string) => {v} },
- { title: '活动名', dataIndex: 'name', width: 220, ellipsis: true },
+ { title: 'ID', dataIndex: 'id', width: 60 },
{
- title: 'actId',
+ title: '平台',
+ dataIndex: 'platform',
+ width: 80,
+ render: (v: string) => {PLATFORM_LABEL[v] || v},
+ },
+ { title: '活动名', dataIndex: 'name', width: 180, ellipsis: true },
+ {
+ title: 'actId(美团)',
dataIndex: 'act_id',
- width: 120,
+ width: 110,
render: (v: string | null) => v || '-',
},
{
- title: 'productViewSign',
- dataIndex: 'product_view_sign',
- width: 160,
+ title: '淘口令/链接',
+ dataIndex: 'payload',
+ width: 240,
ellipsis: true,
render: (v: string | null) => v || '-',
},
{
title: '状态',
dataIndex: 'status',
- width: 90,
+ width: 80,
render: (s: string) => {s},
},
{ title: '创建时间', dataIndex: 'created_at', width: 150, render: (v: string) => formatUtcTime(v) },
@@ -579,15 +604,24 @@ function ActivityFormModal({
onDone: () => void;
}) {
const [form] = Form.useForm();
+ const platform = (Form.useWatch('platform', form) as string | undefined) || 'meituan';
useEffect(() => {
- if (open) form.resetFields();
+ if (open) {
+ form.resetFields();
+ form.setFieldsValue({ platform: 'meituan' });
+ }
}, [open, form]);
const submit = async () => {
const v = await form.validateFields();
- if (!v.act_id && !v.product_view_sign) {
- message.warning('actId 与 productViewSign 至少填一个');
+ 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 {
@@ -606,21 +640,39 @@ function ActivityFormModal({
-
-
-
-
-
-
-
-
+
+
+
+ {platform === 'meituan' && (
+ <>
+
+
+
+
+
+
+ >
+ )}
+ {platform === 'taobao' && (
+
+
+
+ )}
+ {platform === 'jd' && (
+
+
+
+ )}
+
diff --git a/src/lib/types.ts b/src/lib/types.ts
index d3e5ea3..3d2f4ab 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -307,8 +307,9 @@ export interface PriceReportSummary {
// ===== CPS 分发与对账 =====
export interface CpsGroup {
id: number;
- sid: string; // 美团二级渠道追踪位,每群唯一
+ sid: string | null; // 仅美团群有(渠道追踪位)
name: string;
+ platforms: string[]; // meituan/taobao/jd 多选
member_count: number | null;
status: string; // active / archived
remark: string | null;
@@ -321,6 +322,7 @@ export interface CpsActivity {
name: string;
act_id: string | null; // 美团活动物料 ID
product_view_sign: string | null;
+ payload: string | null; // 淘宝整段淘口令 / 京东链接
status: string;
remark: string | null;
created_at: string;
@@ -340,14 +342,17 @@ export interface CpsOrder {
pay_time: string | null;
}
-export interface CpsReferralLink {
- redirect_url: string; // ★ 我们的群发短链(/c/{code}),发群用这个,点击经我们统计
- code: string;
- sid: string;
- group_name: string;
+export interface CpsReferralLinkItem {
+ activity_id: number;
activity_name: string;
- target_url: string; // 实际 302 跳转的美团短链
- link_map: Record; // 美团原始 1长链/2短链/3deeplink(参考)
+ platform: string;
+ redirect_url: string; // 我们的落地页 /c/{code}(发群用)
+ code: string;
+}
+
+export interface CpsReferralLinks {
+ group_name: string;
+ results: CpsReferralLinkItem[];
}
export interface CpsReconcileResult {
@@ -358,18 +363,22 @@ export interface CpsReconcileResult {
}
export interface CpsGroupStat {
- group_id: number | null; // null = 未归群的 sid
+ group_id: number | null;
sid: string | null;
name: string;
+ platforms: string[];
member_count: number | null;
- click_pv: number; // 点击总次数
- click_uv: number; // 独立点击(ip+ua 近似去重)
- order_count: number; // 有效单(不含取消/风控)
- settled_count: number;
- canceled_count: number;
- gmv_cents: number;
- est_commission_cents: number;
- settled_commission_cents: number;
+ click_pv: number;
+ click_uv: number;
+ copy_pv: number; // 淘宝"复制口令"次数
+ copy_uv: number;
+ // 对账类:淘宝/京东无法对账 → null(前端显示 "-")
+ order_count: number | null;
+ settled_count: number | null;
+ canceled_count: number | null;
+ gmv_cents: number | null;
+ est_commission_cents: number | null;
+ settled_commission_cents: number | null;
}
export interface CpsStats {
From ecbce54e70e9dcc303c42144d3a70411b4cdf3d6 Mon Sep 17 00:00:00 2001
From: marco
Date: Wed, 17 Jun 2026 21:32:12 +0800
Subject: [PATCH 10/19] =?UTF-8?q?feat(cps):=20=E6=96=B0=E5=BB=BA=E6=B7=98?=
=?UTF-8?q?=E5=AE=9D=E6=B4=BB=E5=8A=A8=E6=94=AF=E6=8C=81=E4=B8=8A=E4=BC=A0?=
=?UTF-8?q?/=E9=80=89=E6=8B=A9=E8=90=BD=E5=9C=B0=E9=A1=B5=E5=9B=BE=20+=20?=
=?UTF-8?q?=E5=88=97=E8=A1=A8=E7=BC=A9=E7=95=A5=E5=9B=BE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- ActivityFormModal 淘宝分支加 ImagePicker(上传新图 / 选已有 / 预览),必填
- 活动列表加落地页图缩略图列
- types: CpsActivity.image_url
Co-Authored-By: Claude Opus 4.8
---
src/app/(main)/cps/page.tsx | 110 +++++++++++++++++++++++++++++++++---
src/lib/types.ts | 1 +
2 files changed, 103 insertions(+), 8 deletions(-)
diff --git a/src/app/(main)/cps/page.tsx b/src/app/(main)/cps/page.tsx
index fa3901e..87e18e0 100644
--- a/src/app/(main)/cps/page.tsx
+++ b/src/app/(main)/cps/page.tsx
@@ -19,6 +19,7 @@ import {
Tabs,
Tag,
Typography,
+ Upload,
message,
} from 'antd';
import { api, errMsg } from '@/lib/api';
@@ -556,6 +557,17 @@ function ActivitiesTab() {
ellipsis: true,
render: (v: string | null) => v || '-',
},
+ {
+ title: '落地页图',
+ dataIndex: 'image_url',
+ width: 80,
+ render: (v: string | null) =>
+ v ? (
+
+ ) : (
+ '-'
+ ),
+ },
{
title: '状态',
dataIndex: 'status',
@@ -587,13 +599,86 @@ function ActivitiesTab() {
showTotal: (t) => `共 ${t} 个活动`,
onChange,
}}
- scroll={{ x: 1000 }}
+ scroll={{ x: 1080 }}
/>
setCreateOpen(false)} 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,
onClose,
@@ -655,13 +740,22 @@ function ActivityFormModal({
>
)}
{platform === 'taobao' && (
-
-
-
+ <>
+
+
+
+
+
+
+ >
)}
{platform === 'jd' && (
Date: Wed, 17 Jun 2026 21:43:35 +0800
Subject: [PATCH 11/19] =?UTF-8?q?feat(cps):=20=E7=BE=A4/=E6=B4=BB=E5=8A=A8?=
=?UTF-8?q?=E5=88=97=E8=A1=A8=E5=8A=A0=E5=88=A0=E9=99=A4=E6=8C=89=E9=92=AE?=
=?UTF-8?q?(Popconfirm=20=E4=BA=8C=E6=AC=A1=E7=A1=AE=E8=AE=A4)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 群操作列 + 删除;活动列表新增操作列含删除
- 确认弹窗说明影响(已发链接仍可用 / 淘宝活动删后落地页用默认图)
Co-Authored-By: Claude Opus 4.8
---
src/app/(main)/cps/page.tsx | 59 +++++++++++++++++++++++++++++++++++--
1 file changed, 56 insertions(+), 3 deletions(-)
diff --git a/src/app/(main)/cps/page.tsx b/src/app/(main)/cps/page.tsx
index 87e18e0..b82f8b4 100644
--- a/src/app/(main)/cps/page.tsx
+++ b/src/app/(main)/cps/page.tsx
@@ -12,6 +12,7 @@ import {
Input,
InputNumber,
Modal,
+ Popconfirm,
Select,
Space,
Statistic,
@@ -246,6 +247,16 @@ function GroupsTab() {
const [createOpen, setCreateOpen] = useState(false);
const [linkGroup, setLinkGroup] = useState(null);
+ const delGroup = async (g: CpsGroup) => {
+ try {
+ await api.delete(`/admin/api/cps/groups/${g.id}`);
+ message.success('已删除群');
+ reload();
+ } catch (e) {
+ message.error(errMsg(e));
+ }
+ };
+
const columns: ColumnsType = [
{ title: 'ID', dataIndex: 'id', width: 60 },
{ title: '群名', dataIndex: 'name', width: 160, ellipsis: true },
@@ -268,11 +279,22 @@ function GroupsTab() {
title: '操作',
key: 'op',
fixed: 'right',
- width: 150,
+ width: 200,
render: (_, g) => (
e.stopPropagation()}>
{canManage && setLinkGroup(g)}>生成链接}
{canManage && setEditing(g)}>编辑}
+ {canManage && (
+ delGroup(g)}
+ >
+ 删除
+
+ )}
),
},
@@ -308,7 +330,7 @@ function GroupsTab() {
showTotal: (t) => `共 ${t} 个群`,
onChange,
}}
- scroll={{ x: 1040 }}
+ scroll={{ x: 1090 }}
/>
setCreateOpen(false)} onDone={reload} />
@@ -535,6 +557,16 @@ function ActivitiesTab() {
const canManage = canDo(['operator']);
const [createOpen, setCreateOpen] = useState(false);
+ const delActivity = async (a: CpsActivity) => {
+ try {
+ await api.delete(`/admin/api/cps/activities/${a.id}`);
+ message.success('已删除活动');
+ reload();
+ } catch (e) {
+ message.error(errMsg(e));
+ }
+ };
+
const columns: ColumnsType = [
{ title: 'ID', dataIndex: 'id', width: 60 },
{
@@ -575,6 +607,27 @@ function ActivitiesTab() {
render: (s: string) => {s},
},
{ title: '创建时间', dataIndex: 'created_at', width: 150, render: (v: string) => formatUtcTime(v) },
+ ...(canManage
+ ? ([
+ {
+ title: '操作',
+ key: 'op',
+ fixed: 'right' as const,
+ width: 80,
+ render: (_: unknown, a: CpsActivity) => (
+ delActivity(a)}
+ >
+ 删除
+
+ ),
+ },
+ ] as ColumnsType)
+ : []),
];
return (
@@ -599,7 +652,7 @@ function ActivitiesTab() {
showTotal: (t) => `共 ${t} 个活动`,
onChange,
}}
- scroll={{ x: 1080 }}
+ scroll={{ x: 1160 }}
/>
setCreateOpen(false)} onDone={reload} />
From cef5ea014050f6b6a07f258a763aabc51ff9a497 Mon Sep 17 00:00:00 2001
From: marco
Date: Wed, 17 Jun 2026 22:13:36 +0800
Subject: [PATCH 12/19] =?UTF-8?q?feat(cps):=20=E6=B4=BB=E5=8A=A8=E8=A1=A8?=
=?UTF-8?q?=E5=8D=95=E6=94=AF=E6=8C=81=E7=BC=96=E8=BE=91=20+=20=E5=88=97?=
=?UTF-8?q?=E8=A1=A8=E5=8A=A0=E7=BC=96=E8=BE=91=E6=8C=89=E9=92=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- ActivityFormModal 改 create+edit 双模式(回填字段, 编辑走 PATCH, 加状态切换)
- 活动列表操作列加「编辑」
Co-Authored-By: Claude Opus 4.8
---
src/app/(main)/cps/page.tsx | 72 +++++++++++++++++++++++++++----------
1 file changed, 54 insertions(+), 18 deletions(-)
diff --git a/src/app/(main)/cps/page.tsx b/src/app/(main)/cps/page.tsx
index b82f8b4..2231e7a 100644
--- a/src/app/(main)/cps/page.tsx
+++ b/src/app/(main)/cps/page.tsx
@@ -556,6 +556,7 @@ function ActivitiesTab() {
);
const canManage = canDo(['operator']);
const [createOpen, setCreateOpen] = useState(false);
+ const [editing, setEditing] = useState(null);
const delActivity = async (a: CpsActivity) => {
try {
@@ -613,17 +614,20 @@ function ActivitiesTab() {
title: '操作',
key: 'op',
fixed: 'right' as const,
- width: 80,
+ width: 120,
render: (_: unknown, a: CpsActivity) => (
- delActivity(a)}
- >
- 删除
-
+
+ setEditing(a)}>编辑
+ delActivity(a)}
+ >
+ 删除
+
+
),
},
] as ColumnsType)
@@ -652,9 +656,10 @@ function ActivitiesTab() {
showTotal: (t) => `共 ${t} 个活动`,
onChange,
}}
- scroll={{ x: 1160 }}
+ scroll={{ x: 1200 }}
/>
- setCreateOpen(false)} onDone={reload} />
+ setCreateOpen(false)} onDone={reload} />
+ setEditing(null)} onDone={reload} />
);
}
@@ -734,22 +739,38 @@ function ImagePicker({ value, onChange }: { value?: string | null; onChange?: (v
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) {
- form.resetFields();
- form.setFieldsValue({ platform: 'meituan' });
+ 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, form]);
+ }, [open, activity, form]);
const submit = async () => {
const v = await form.validateFields();
@@ -763,8 +784,13 @@ function ActivityFormModal({
return;
}
try {
- await api.post('/admin/api/cps/activities', v);
- message.success('已新建活动');
+ 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) {
@@ -773,7 +799,7 @@ function ActivityFormModal({
};
return (
-
+