编辑页面

修改后将从当前时间重新计算过期时间
`; else if (s === 1) body = `
${wizField('limit', '每人限报(份)', d.limit, 'number')}
`; else if (s === 2) body = playCfgBody(d); else body = confirmBody(d); return heading(wizard.editing ? '编辑活动' : '发布活动', `第 ${s + 1} / ${WIZ_STEPS.length} 步 · ${WIZ_STEPS[s]}`, btn('返回活动列表', 'activities', '', 'secondary')) + `
${WIZ_STEPS.map((t, i) => ``).join('')}
` + (wizard.error ? `
${icon('alert')}${esc(wizard.error)}
` : '') + `

${WIZ_STEPS[s]}

${s === 2 ? '勾选即开启,配置块实时展开;勾选结果同步小程序端入口' : '带 * 为必填项'}
${body}
${s > 0 ? btn('上一步', 'wiz-prev') : ''}${s < WIZ_STEPS.length - 1 ? btn('保存并继续', 'wiz-next', '', 'primary') : btn('存为草稿', 'wiz-draft') + btn(wizard.editing ? '保存并发布' : '确认发布活动', 'wiz-submit', '', 'primary')}
`; } function playCfgBody(d) { const on = p => (d.plays || []).includes(p); const cfg = []; if (on('集赞有礼')) cfg.push(`

集赞有礼 · 本场配置

用户分享到外部渠道集赞 → 上传截图 → 后台人工核对 → 达标手动发放。勾选后活动进入小程序「集赞有礼」列表。

${wizField('likeGoal', '集齐多少个赞(可填)', d.likeGoal, 'number', '每场活动独立设置,例如 38')} ${wizField('likePeriod', '集赞有效期', d.likePeriod || '活动开始前均可提交')} ${wizField('shareTitle', '分享海报文案', d.shareTitle, 'text', '留空则使用活动名称')}
`); if (on('组队')) cfg.push(`

组队 · 本场配置(对应小程序「组队广场」拼多多式拼团)

${wizField('teamSize', '成团人数', (d.team || {}).size || 3, 'number')} ${wizField('teamHours', '成团有效期(小时)', (d.team || {}).hours || 48, 'number')} ${wizField('teamBenefit', '成团权益', (d.team || {}).benefit || '成团享套餐优惠')}

未成团自动失效并全额原路退回;邀请弹窗、正在拼单列表由小程序端固定展示。

`); if (on('生日优惠')) cfg.push(`

生日优惠 · 本场配置(寿星专享,其余用户正常报名)

${wizField('bdayPrice', '寿星专享价(元)', (d.bdayPrice != null && d.bdayPrice !== '') ? d.bdayPrice : 30, 'number', '0 = 寿星免费;原价 ¥ ' + money(Number(d.price) || 0) + ' 取第 2 步票价')} ${wizField('bdayPerks', '寿星专属福利', d.bdayPerks || '生日礼包 · 专属生日帽 · 免费派对写真', 'text', '展示在活动详情与报名页,核销时发放')} ${wizField('bdayStock', '寿星专属名额', d.bdayStock || 0, 'number', '留空 / 0 = 不限;独立于第 2 步总名额')} ${wizField('bdayDays', '推送提前天数', d.bdayDays || 3, 'number')}

与「参与人群限制」的关系:第 2 步选「不限」时所有用户可见可报名,按原价结算;勾选本玩法后,宠物生日命中窗口的用户报名自动按寿星专享价结算并享专属福利,价差与福利由系统按档案判定,无需用户提交证明。

`); const NOTE_MAP = { '报名': '基础报名 + 凭证核销,配置见第 1 / 2 步', '满额有礼': '消费阶梯档位在「玩法配置 → 满额有礼」统一维护,达标自动 / 人工发放', '次数有礼': '次数阶梯在「玩法配置 → 次数有礼」维护,按指定服务完成次数计', '任务中心': '任务与抽奖奖池在「玩法配置 → 任务与抽奖」维护', '投稿征集': '奖励规则在「玩法配置 → 投稿与评价」维护;投稿进入「内容与互动 → 投稿审核」', '订单评价': '带图评价审核通过自动发 ¥15 通用券,规则在「玩法配置 → 投稿与评价」', '集市招商': '展位类型与定价在「集市招商 → 集市与展位」维护;商家报名进入「商家报名审核」', }; const notes = (d.plays || []).filter(p => NOTE_MAP[p]).map(p => `
${icon('check')}
${esc(p)}${NOTE_MAP[p]}
`).join(''); return `

勾选本场活动要开启的玩法;勾选后对应配置块立即展开,字段与小程序端逻辑一一对应。开启的玩法会出现在小程序端活动页与「我的」页入口。卡包活动、钩子活动、领券、联名卡包、券码兑换属用户权益类福利(用户的使用权),统一在「玩法配置 / 奖励中心」维护,不与单个活动绑定,故不在此处配置。

${['报名', '集赞有礼', '满额有礼', '次数有礼', '组队', '任务中心', '投稿征集', '订单评价', '集市招商', '生日优惠'].map(p => ``).join('')}
${cfg.join('')} ${notes ? `

勾选玩法 · 统一配置说明

${notes}
` : ''}`; } function confirmBody(d) { const on = p => (d.plays || []).includes(p); return `
${icon('info')}请确认以下配置。发布后可在活动列表继续编辑;已开启集赞的活动会立即出现在小程序「集赞有礼」列表。
${detail('活动名称', esc(d.title) || '未填写')}${detail('场景 / 收费', `${d.scene} · ${d.charge}`)}${detail('时间', `${esc(String(d.start).replace('T', ' '))} 至 ${esc(String(d.end).replace('T', ' '))}`)}${detail('地点', esc(d.place || '—'))}${detail('名额 / 单价', `${d.capacity || '不限'} · ${Number(d.price) ? '¥ ' + money(d.price) : '免费'}`)}${detail('计价模式', d.pricing === 'pet' ? '按宠物数量计费 · 宠物票 ¥ ' + money(Number(d.petPrice) || 0) + ' / 只' : '按报名人数计费 · 宠物免费携带')}${detail('核销', d.verify ? '需要核销(核销员授权见「账号与权限」)' : '无需核销')}${detail('兑换 / 领取', d.scene === '线上' ? (d.redeemOffline ? '线下兑换 / 到店领取 · 报名成功即发核销码' : '纯线上参与,无核销环节') : '到店核销入场')}${detail('参与人群限制', esc(crowdText(d)))}${detail('开启玩法', (d.plays || []).join(' / ') || '—')}${detail('报名人数 / 结算', `${db.registrations.filter(r => r.activity === d.id).length} 人报名 · ${db.grants.filter(g => g.act === d.id).length} 笔奖励发放`)}
${on('集赞有礼') ? `

集赞有礼

${detail('集赞门槛', d.likeGoal + ' 个赞')}${detail('允许渠道', platList(d.likePlatforms))}${detail('达标奖励', (d.likeRewards || []).map(x => esc(rewardName(x))).join('、') || '未勾选')}
` : ''} ${on('组队') ? `

组队

${detail('成团人数', (d.team || {}).size + ' 人')}${detail('成团有效期', (d.team || {}).hours + ' 小时')}${detail('成团权益', esc((d.team || {}).benefit || '—'))}${detail('支付方式', esc((d.team || {}).pay || '报名时支付'))}
` : ''} ${on('生日优惠') ? `

生日优惠

${detail('寿星专享价', '¥ ' + money(Number(d.bdayPrice) || 0) + '(原价 ¥ ' + money(Number(d.price) || 0) + ')')}${detail('生日判定窗口', esc(d.bdayWindow || '生日当月(自然月)'))}${detail('寿星专属福利', esc(d.bdayPerks || '—'))}${detail('寿星专属名额', Number(d.bdayStock) ? int(d.bdayStock) + ' 个' : '不限')}${detail('定向推送', d.bdayPush === false ? '关闭(仅列表展示)' : '提前 ' + (d.bdayDays || 3) + ' 天推送')}
` : ''} `; } function playDesc(p) { return ({ 报名: '活动报名 + 凭证核销', 集赞有礼: '分享集赞,截图兑礼', 满额有礼: '消费阶梯奖励', 次数有礼: '指定服务次数奖励', 任务中心: '任务 → 抽奖', 投稿征集: '晒图投稿,审核发券', 订单评价: '订单评价 + 晒图发券', 集市招商: '商家报名 + 展位', 生日优惠: '寿星专享价与专属福利(全场可报名)', 组队: '线上组队 / 线下配套组队,成团享权益'})[p] || ''; } pages.wizard = () => { if (!wizard) wizard = { data: A({ id: '', title: '', place: '', price: 0, capacity: 30 }), step: 0, error: '', editing: false }; return drawWizard() }; function captureWizard() { const f = $('#activity-form'); if (!f) return; const fd = new FormData(f); for (const [k, v] of fd) if (!k.startsWith('field-') && !k.startsWith('play-')) wizard.data[k] = v; ['price', 'capacity', 'limit', 'likeGoal', 'petPrice'].forEach(k => { if (k in wizard.data) wizard.data[k] = Number(wizard.data[k]) || 0 }); if (wizard.step === 0) wizard.data.redeemOffline = fd.get('redeemOffline') === 'on'; if (wizard.step === 1) { wizard.data.verify = fd.get('verify') === '需要核销'; const cs = $$('.crowd-opt:checked').map(b => b.value); wizard.data.crowds = cs.includes('不限') ? ['不限'] : cs; wizard.data.crowd = wizard.data.crowds.length && !wizard.data.crowds.includes('不限') ? '指定人群' : '不限'; wizard.data.fields = ['联系人', '联系电话', '宠物昵称', '宠物品种', '备注', '宠物数量'].map(n => ({ name: n, required: !!fd.get('field-' + n) })).filter(x => x.required).map(x => ({ name: x.name, required: ['联系人', '联系电话'].includes(x.name) })); } if (wizard.step === 2) { const d = wizard.data; d.plays = $$('.play-check:checked').map(b => b.value); d.share = d.plays.includes('集赞有礼'); d.likeGoal = Number(fd.get('likeGoal')) || d.likeGoal || 38; d.likePeriod = fd.get('likePeriod') || d.likePeriod; d.likePlatforms = $$('.like-plat:checked').map(b => b.value); d.likeRewards = $$('.like-reward:checked').map(b => b.value); d.shareTitle = fd.get('shareTitle') ?? d.shareTitle; d.team = { on: d.plays.includes('组队'), size: Number(fd.get('teamSize')) || 3, hours: Number(fd.get('teamHours')) || 48, benefit: fd.get('teamBenefit') || '成团享套餐优惠', pay: fd.get('teamPay') || '报名时支付' }; d.bdayDays = Number(fd.get('bdayDays')) || 3; d.bdayPrice = Number(fd.get('bdayPrice')) || 0; d.bdayWindow = fd.get('bdayWindow') || d.bdayWindow || '生日当月(自然月)'; d.bdayPerks = fd.get('bdayPerks') || d.bdayPerks || ''; d.bdayStock = Number(fd.get('bdayStock')) || 0; d.bdayPush = fd.get('bdayPush') === 'on'; } } function wizSaveDraft(publish) { captureWizard(); const d = wizard.data; if (!d.title) { wizard.error = '请先填写活动名称'; wizard.step = 0; return } if (!d.id) d.id = 'ACT' + String(Date.now()).slice(-6); d.status = publish ? '已发布' : '草稿'; const t = db.activities.findIndex(a => a.id === d.id); if (t >= 0) db.activities[t] = d; else db.activities.unshift(d); save(); log(publish ? '发布活动' : '保存活动草稿', d.id, `${d.title}(${d.plays.join(' / ')})`); toast(publish ? `活动已发布:${d.title}` : `已保存草稿:${d.title}`); wizard = null; navigate('activities'); } /* ============================ 报名与订单 ============================ */ pages.registrations = () => { const rows = db.registrations.filter(r => matches({ ...r, title: actName(r.activity) }) && (activeTab === '全部' || r.status === activeTab)); const t = db.registrations; const paid = t.filter(r => r.payment === '支付成功').reduce((s, r) => s + r.amount, 0); return heading('报名与订单', '免费报名、支付记录、入场凭证与退款分别追踪。一个订单可含多名报名人(本人 + 代报名同行人):一单只支付一次,订单下按人挂报名记录、按人发凭证;拼团优惠只统计成团队伍,代报名人数不计入成团判定。', btn(icon('download') + '导出当前结果', 'export', 'registrations')) + `
${stat('报名记录', t.length, '全部活动合计', 'orders')}${stat('成功支付', `¥ ${money(paid)}`, '已完成支付的报名订单', 'wallet')}${stat('待核销凭证', t.filter(r => r.ticket === '可使用').length, '到场出示凭证核销', 'scan')}${stat('退款处理中', t.filter(r => r.refund === '申请中' || r.refund === '退款失败').length, '需逐笔等待结果', 'refund')}${stat('订单数', new Set(t.map(r => r.order).filter(o => o && o !== '—')).size, '一次支付 = 一个订单,可含多名报名人', 'orders')}${stat('拼团订单', t.filter(r => r.teamFlag === '拼团成团 · 享拼团价').length, '仅统计成团享价的报名人;代报名按原价不计入', 'users')}
` + `
${tabs(['全部', '报名成功', '待支付', '已取消', '已关闭'])}${filters('搜索报名号、订单号、联系人或活动')}${table(['报名 / 活动', '订单号', '报名人 / 宠物', '报名人角色', '实付 / 计价', '报名状态', '凭证 / 退款', '来源 / 入口', '操作'], slice(rows).map(r => [`${r.id}${esc(actName(r.activity))}`, `${esc(r.order)}${esc(r.orderOwner || '单报名人订单')}`, `${esc(r.name)}${esc(r.pet)}`, `${esc(r.role || '预订人')}${r.teamFlag ? `${esc(r.teamFlag)}` : ''}`, `¥ ${money(r.payment === '支付成功' ? r.amount : 0)}${esc(r.pricing || r.payment)}`, tag(r.status), `${tag(r.ticket)}${r.refund}`, `${esc(r.channel)}${esc(r.from || '—')}`, link('详情', 'registration-detail', r.id)]))}${foot(rows.length)}
`; }; /* ============================ 凭证核销 ============================ */ pages.verify = () => { const rows = db.verifications; return heading('凭证核销', '核销确认的是活动入场资格;集赞奖励与投稿奖励不在此核销。') + notice('演示时间固定为 2026-09-21。示例凭证:XB260103(周一萌宠见面会 · 可使用)、XB260104(已使用)、XB260102(退款冻结)、XB260110(集市入场 · 可使用)。') + `
${stat('今日核销', rows.length, '演示数据', 'scan')}${stat('未使用凭证', db.registrations.filter(r => r.ticket === '可使用').length, '含线上无需核销之外的活动凭证', 'ticket')}${stat('冻结凭证', db.registrations.filter(r => r.ticket === '冻结').length, '退款处理中,暂不可核销', 'refund')}${stat('授权核销员', db.accounts.filter(a => a.role === '核销员').length, '按活动范围授权', 'users')}
` + `

手动核销

授权活动校验
${icon('scan')}

输入活动凭证码

先查验活动与资格,再确认入场核销

核销须知

01 · 校验授权范围核销员仅可核销被授权的活动
02 · 确认活动与参与人核对活动名称、规格、脱敏联系人与有效期
03 · 确认入场已使用、退款冻结、过期与作废凭证不可核销
04 · 留存记录保存操作人、时间与结果,重复提交不可重复使用
${notice('扫码设备与真实凭证接口尚未接入,此处演示手动输码流程。')}

核销记录

${rows.length} 条
${table(['凭证码', '活动', '联系人', '核销人员', '核销时间', '结果'], rows.map(v => [esc(v.code), esc(actName(v.activity)), esc(v.name), esc(v.staff), v.time, tag(v.result)]))}
`; }; function doVerify(code) { const box = $('#verify-result'); const c = String(code || '').trim().toUpperCase(); const r = db.registrations.find(x => x.code === c); if (!r) { box.innerHTML = `
未找到凭证 ${esc(c)},请核对后重试。`; return } const st = r.ticket; const bad = { '冻结': '该凭证处于退款冻结状态,需先处理退款申请。', '已使用': '该凭证已于此前核销,不可重复使用。', '已作废': '该凭证已作废(订单已取消 / 退款成功)。', '未发放': '订单尚未支付成功,凭证未发放。' }[st]; box.innerHTML = `
${esc(r.name)}${tag(st)}
${detail('活动', esc(actName(r.activity)))}${detail('凭证码', esc(c))}${detail('报名号', r.id)}${detail('有效期', esc(String((act(r.activity) || {}).validEnd || '—').replace('T', ' ')))}
` + (bad ? `
${icon('alert')}${bad}
` : btn('确认入场核销', 'verify-confirm', r.id, 'primary')); } /* ============================ 弹窗与通用弹窗 ============================ */ function openModal(title, body, footer = '', sub = '') { returnFocus = document.activeElement; const m = $('#modal'); m.innerHTML = `${footer ? `` : ''}`; if (!m.open) m.showModal(); m.scrollTop = 0; } function closeModal() { const m = $('#modal'); if (m.open) m.close(); returnFocus?.focus?.() } function confirmAction(title, description, action, id, label = '确认') { openModal(title, `

${description}

`, btn('返回', 'close') + btn(label, action, id, 'primary')) } function activityModal(id) { const a = act(id); if (!a) return; const regs = db.registrations.filter(r => r.activity === id); const likes = db.likes.filter(l => l.act === id); const cost = db.grants.filter(g => g.act === id && g.status === '已发放').reduce((s, g) => s + ((reward(g.reward) || {}).cost || 0), 0); openModal(a.title, `
${tag(a.status)}${a.id} · 规则版本 V${a.version || 1}
${detail('场景 / 收费', `${a.scene} · ${a.charge}`)}${detail('活动时间', `${esc(String(a.start).replace('T', ' '))} 至 ${esc(String(a.end).replace('T', ' '))}`)}${detail('活动地点', esc(a.place || '—'))}${detail('报名时间', `${esc(String(a.signupStart).replace('T', ' '))} 至 ${esc(String(a.signupEnd).replace('T', ' '))}`)}${detail('名额 / 单价', `${a.capacity || '不限'} ${esc(a.unit)} · ${Number(a.price) ? '¥ ' + money(a.price) : '免费'}`)}${detail('退款规则', esc(a.refund))}${detail('核销规则', a.verify ? '单次整单核销(核销员授权见「账号与权限」)' : '无需核销')}${detail('兑换 / 领取', a.scene === '线上' ? (a.redeemOffline ? '线下兑换 / 到店领取 · 报名成功即发核销码' : '纯线上参与,无核销环节') : '到店核销入场')}${detail('参与人群限制', esc(crowdText(a)))}${detail('组队玩法', (a.team || {}).on ? `${a.team.size} 人成团 · ${a.team.hours} 小时内 · ${esc(a.team.benefit)}` : '未开启')}${detail('报名 / 凭证', `${regs.length} 条报名 · ${regs.filter(r => r.ticket === '可使用').length} 张可用凭证`)}

开启的玩法

${(a.plays || []).map(p => tag(p)).join(' ') || '仅基础报名'}

分享集赞配置

${a.share ? `
${detail('集赞门槛', a.likeGoal + ' 个赞')}${detail('允许渠道', platList(a.likePlatforms))}${detail('达标奖励', (a.likeRewards || []).map(x => esc(rewardName(x))).join('、') || '未勾选')}${detail('审核提交流程', '分享 → 集赞 → 上传截图 → 人工核对 → 手动发奖')}${detail('集赞提交 / 已通过', `${likes.length} 条 · ${likes.filter(l => l.status === '已通过').length} 条`)}
` : notice('本活动未开启分享集赞。如需开启,请在「编辑活动 → 分享配置」中勾选。')} ${(a.team || {}).on ? `

组队配置(线上组队 / 线下配套组队)

${detail('成团人数', a.team.size + ' 人')}${detail('成团有效期', a.team.hours + ' 小时')}${detail('成团权益', esc(a.team.benefit))}${detail('支付方式', esc(a.team.pay))}${detail('未成团处理', esc(a.team.fail || '自动失效并全额原路退回'))}${detail('队伍数 / 已成团', `${(db.teams || []).filter(t => t.act === a.id).length} 支 · ${(db.teams || []).filter(t => t.act === a.id && t.status === '已成团').length} 支`)}
` : ''}

奖励成本

已发放奖励成本合计 ¥ ${money(cost)}(${db.grants.filter(g => g.act === id).length} 笔发放记录)

活动介绍

${esc(a.description)}

`, `${link('报名名单', 'activity-orders', id)}${link('集赞审核', 'like-filter-act', id)}${link('发奖记录', 'grant-filter-act', id)}${can('wizard') ? btn('编辑活动', 'edit-activity', id) + (a.status === '已发布' ? btn('下架', 'ask-down', id) : btn('发布', 'publish-activity', id, 'primary')) : ''}`); } function registrationModal(id) { const r = db.registrations.find(x => x.id === id); if (!r) return; const sibs = db.registrations.filter(x => x.order === r.order && x.order !== '—'); openModal('报名详情', `
${esc(r.name)}${tag(r.status)}${tag(r.ticket)}${r.id}
${detail('活动', esc(actName(r.activity)))}${detail('报名人角色', esc(r.role || '预订人'))}${detail('订单号', `${esc(r.order)}${sibs.length > 1 ? `(本单 ${sibs.length} 名报名人 · 只支付一次)` : '(单报名人)'}`)}${detail('订单归属', esc(r.orderOwner || '—'))}${detail('计价', esc(r.pricing || '按活动票价结算'))}${detail('拼团', esc(r.teamFlag || '非拼团报名'))}${detail('联系人', esc(r.name) + ' · ' + esc(r.phone))}${detail('宠物', esc(r.pet))}${detail('报名时间', esc(r.date))}${detail('订单 / 实付', `${r.order} · ¥ ${money(r.amount)}`)}${detail('支付状态', esc(r.payment))}${detail('凭证码', esc(r.code || '—'))}${detail('来源渠道', esc(r.channel))}${detail('进入入口', esc(r.from || '—'))}${detail('退款', esc(r.refund) + (r.reason ? ' · ' + esc(r.reason) : ''))}
${sibs.length > 1 ? `

本单报名人(${sibs.length})

${table(['报名号', '报名人', '角色', '宠物', '实付', '凭证'], sibs.map(x => [esc(x.id), esc(x.name), esc(x.role || '预订人'), esc(x.pet), '¥ ' + money(x.amount), esc(x.code || '—')]))}${notice('同一订单只支付一次;报名记录、凭证与核销按报名人分别记录,退款按订单整单处理。')}` : ''}

操作

${r.refund === '申请中' ? btn('同意退款', 'refund-approve', r.id, 'primary') + btn('驳回申请', 'refund-reject', r.id) : r.refund === '退款失败' ? btn('重新发起退款', 'refund-retry', r.id, 'primary') : '无待处理退款'}${r.ticket === '可使用' ? btn('冻结凭证', 'ticket-freeze', r.id) : ''}${r.status !== '已关闭' ? btn('关闭报名', 'reg-close', r.id, 'danger') : ''}
`, btn('关闭', 'close')); } /* ============================ 全局事件与启动 ============================ */ Object.assign(actions, { tab: t => { activeTab = String(t); pageIndex = 1; render() }, 'reset-filter': () => { query = ''; activeTab = '全部'; pageIndex = 1; render() }, 'prev-page': () => { if (pageIndex > 1) { pageIndex--; render() } }, 'next-page': () => { pageIndex++; render() }, 'set-page': n => { pageIndex = Number(n); render() }, period: d => { chartPeriod = String(d); render() }, close: () => closeModal(), activities: () => navigate('activities'), 'goto-wizard': () => can('wizard') ? openWizard(null) : toast('当前演示角色没有发布活动权限'), 'edit-activity': id => can('wizard') ? openWizard(String(id)) : toast('当前演示角色没有编辑活动权限'), 'wiz-next': () => { captureWizard(); wizard.error = ''; if (wizard.step === 0 && !wizard.data.title) { wizard.error = '请先填写活动名称'; render(); return } wizard.step++; render() }, 'wiz-prev': () => { captureWizard(); wizard.step--; render() }, 'wiz-step': i => { captureWizard(); if (Number(i) <= wizard.step) { wizard.step = Number(i); render() } }, 'wiz-draft': () => wizSaveDraft(false), 'wiz-submit': () => wizSaveDraft(true), 'activity-detail': id => activityModal(String(id)), 'registration-detail': id => registrationModal(String(id)), 'activity-orders': id => { closeModal(); navigate('registrations'); toast('已按活动「' + actName(String(id)) + '」筛选(演示:可在报名列表中检索活动名称)') }, 'publish-activity': id => { const a = act(String(id)); a.status = '已发布'; save(); log('发布活动', a.id, a.title); toast('已发布:' + a.title); render() }, 'ask-down': id => confirmAction('下架活动', '下架后停止新报名,已产生的报名与凭证继续有效。确认下架该活动?', 'do-down', id, '确认下架'), 'do-down': id => { const a = act(String(id)); a.status = '已下架'; save(); log('下架活动', a.id, a.title); closeModal(); toast('已下架:' + a.title); render() }, 'verify-confirm': id => { const r = db.registrations.find(x => x.id === String(id)); r.ticket = '已使用'; db.verifications.unshift({ code: r.code, activity: r.activity, name: r.name, staff: role === '核销员' ? '核销员小林' : role, time: nowStr(), result: '核销成功' }); save(); log('核销活动凭证', r.code, '核销成功'); toast('核销成功:' + r.code); $('#verify-result').innerHTML = ''; render(); }, 'refund-approve': id => { const r = db.registrations.find(x => x.id === String(id)); r.refund = '退款处理中'; r.ticket = '冻结'; save(); log('同意退款申请', r.id, '进入退款处理中'); closeModal(); toast('已受理退款申请,等待支付渠道回执'); render() }, 'refund-reject': id => { const r = db.registrations.find(x => x.id === String(id)); r.refund = '已驳回'; r.ticket = '可使用'; save(); log('驳回退款申请', r.id, '凭证恢复可使用'); closeModal(); toast('已驳回退款申请'); render() }, 'refund-retry': id => { const r = db.registrations.find(x => x.id === String(id)); r.refund = '退款处理中'; save(); log('重新发起退款', r.id, '等待回执'); closeModal(); toast('已重新发起退款'); render() }, 'ticket-freeze': id => { const r = db.registrations.find(x => x.id === String(id)); r.ticket = '冻结'; save(); log('冻结凭证', r.code, '暂停核销'); closeModal(); toast('凭证已冻结'); render() }, 'reg-close': id => { const r = db.registrations.find(x => x.id === String(id)); r.status = '已关闭'; save(); log('关闭报名', r.id, '停止后续流程'); closeModal(); toast('报名已关闭'); render() }, 'notify-reviewers': () => { log('批量提醒审核人', '待办队列', '已发送提醒(演示)'); toast('已向内容审核与活动运营发送待办提醒(演示)') }, todo: () => navigate('todo'), export: what => { log('导出名单', what, '按当前筛选条件导出(演示)'); toast('已生成导出文件(演示,不产生真实下载)') }, scope: () => openModal('原型说明', `

本原型为「小布在家 · 活动运营中心」,围绕小程序活动模块整体重建,覆盖:

活动管理线上 / 线下活动发布、报名与订单、凭证核销
玩法配置集赞有礼、满额有礼、次数有礼、卡包活动(含联名卡包)、组队活动、限时领券、推广钩子、任务与抽奖、投稿与评价
审核中心集赞截图人工核对、投稿审核、评价审核、商家报名审核
奖励中心活动奖励库、发奖记录(自动 / 手动)
集市招商展位图、商家报名审核与展位分配
数据与设置活动效果、奖励成本 ROI、渠道、审核规则、通知模板、权限

数据保存在当前浏览器,可切换右上角演示角色查看权限差异。

`, btn('知道了', 'close', '', 'primary')) }); document.addEventListener('click', e => { const p = e.target.closest('[data-page]'); if (p) { navigate(p.dataset.page); return } const a = e.target.closest('[data-action]'); if (!a) return; const fn = actions[a.dataset.action]; if (fn) { e.preventDefault(); fn(a.dataset.id, a) } }); document.addEventListener('submit', e => { if (e.target.id === 'filter-form') { e.preventDefault(); query = new FormData(e.target).get('query') || ''; pageIndex = 1; render(); return } if (e.target.id === 'verify-form') { e.preventDefault(); doVerify(new FormData(e.target).get('code')); return } if (e.target.id === 'activity-form') { e.preventDefault(); captureWizard(); wizard.step < 3 ? (wizard.step++, render()) : wizSaveDraft(true); return } if (e.target.id === 'like-verify-form') { e.preventDefault(); const v = Number(new FormData(e.target).get('verified')); const id = e.target.dataset.likeId || ''; if (!v) { toast('请填写截图上可见的点赞数'); return } checkLike(id, v); return } }); document.addEventListener('change', e => { if (e.target.name === 'scene') { captureWizard(); render(); return } if (e.target.id === 'role') { role = e.target.value; if (!can(page)) page = permissions[role][0]; toast('已切换为「' + role + '」视角'); render(); return } if (e.target.name === 'pricing') { const w = $('#pet-price-wrap'); if (w) w.style.display = e.target.value === 'pet' ? '' : 'none' } if (e.target.classList.contains('crowd-opt')) { const f = $('#activity-form'); if (f) { if (e.target.value === '不限' && e.target.checked) f.querySelectorAll('.crowd-opt').forEach(b => { if (b.value !== '不限') b.checked = false }); else if (e.target.checked) { const nb = f.querySelector('.crowd-opt[value="不限"]'); if (nb) nb.checked = false } } } if (e.target.classList.contains('play-check')) { captureWizard(); render() } }); window.addEventListener('hashchange', () => { const h = location.hash.slice(1); if (h && h !== page && can(h)) { page = h; render() } }); const state = {}; if (location.hash && can(location.hash.slice(1))) page = location.hash.slice(1); render(); 'use strict'; /* ============================================================================ 小布在家 · 活动运营中心 —— 模块二 玩法配置 / 审核中心 / 奖励中心 / 内容与互动 / 集市招商 / 用户 / 数据分析 / 系统设置 ========================================================================== */ /* 评价自动发券所需的券模板(小程序端为「¥15 通用券」) */ if (!db.rewards.some(r => r.id === 'RW07')) db.rewards.push({ id: 'RW07', name: '¥15 通用券', type: '优惠券', cost: 15, stock: 9999, used: 0, status: '启用', note: '订单评价(带图)审核通过后自动发放' }); /* 兼容迁移:旧数据 crowd/crowdSegs → crowds 数组(平铺多选) */ db.activities.forEach(a => { if (!a.crowds) a.crowds = (a.crowd === '不限') ? ['不限'] : ((a.crowdSegs && a.crowdSegs.length) ? a.crowdSegs.slice() : (a.crowd && a.crowd !== '不限' ? [a.crowd] : ['不限'])) }); const grant = (actId, user, rid, from, way = '人工发放', status = '已发放') => { const r = reward(rid); if (!r) return null; const g = { id: 'FF' + String(Date.now()).slice(-6) + Math.floor(Math.random() * 90 + 10), act: actId, user, reward: rid, way, from, at: nowStr(), status, op: way === '自动发放' ? '系统' : role }; db.grants.unshift(g); if (status === '已发放') r.used++; return g; }; const costOf = g => ((reward(g.reward) || {}).cost || 0); const actOptions = () => db.activities.map(a => ``).join(''); const rewardOptions = () => db.rewards.filter(r => r.status === '启用').map(r => ``).join(''); /* ============================ 玩法:集赞有礼(配置) ============================ */ pages.playlike = () => { const on = db.activities.filter(a => a.share); const pend = db.likes.filter(l => l.status === '待审核').length; return heading('集赞有礼 · 玩法配置', '每场活动(线上 / 线下)独立配置「是否开启、集齐多少个赞、允许哪些渠道、达标奖励是什么」;用户端「集赞有礼」页展示的就是这里已开启的活动列表。', btn(icon('eye') + '前往集赞审核', 'goto-likereview', '', 'primary') + btn(icon('plus') + '在活动中开启集赞', 'goto-wizard', '')) + `
${stat('已开启集赞的活动', on.length, '线上 + 线下均可开启', 'star')}${stat('待核对截图', pend, '需 1 个工作日内处理', 'clock')}${stat('累计提交', db.likes.length, '含已通过与已驳回', 'image')}${stat('已发放奖励', db.grants.filter(g => g.from.includes('集赞')).length, '人工核对通过后手动发放', 'gift')}
` + notice('集赞流程:用户在活动详情页把活动分享到外部渠道 → 好友点赞(渠道不限平台)→ 集满门槛后上传点赞截图 → 后台人工核对截图上可见的点赞总数 → 达标由运营手动发放该活动勾选的奖励;未达标驳回并回传核对到的点赞数,用户可继续集赞后重新提交。') + `

按活动配置集赞

门槛 / 奖励 / 渠道逐活动设置,保存后实时同步小程序端
${table(['活动', '场景', '集赞门槛', '达标奖励(活动奖励库)', '允许渠道', '状态', '操作'], on.map(a => [`${esc(a.title)}${a.id}`, `${a.scene} / ${a.charge}`, `${a.likeGoal} 个赞`, (a.likeRewards || []).map(x => tag(rewardName(x))).join(' ') || '未勾选', platList(a.likePlatforms), tag(a.status === '已发布' ? '启用' : '未发布'), `${link('编辑配置', 'like-cfg-edit', a.id)}${link('查看提交', 'like-filter-act', a.id)}`]))}

未开启集赞的活动

点击「去开启集赞」直达该活动编辑页的第 3 步「玩法与奖励」,勾选集赞有礼并配置门槛 / 渠道 / 奖励
${table(['活动', '场景', '开启的玩法', '操作'], db.activities.filter(a => !a.share).map(a => [esc(a.title), `${a.scene} / ${a.charge}`, (a.plays || []).map(p => `${p}`).join(' '), link('去开启集赞', 'open-like-wiz', a.id)]))}
`; }; function likeCfgModal(id) { const a = act(id); if (!a) return; openModal('集赞配置 · ' + a.title, `

对应活动发布向导第 3 步「玩法与奖励」。修改后立即同步小程序端「集赞有礼」页与活动详情页。

`, btn('取消', 'close') + btn('保存配置', 'like-cfg-save', id, 'primary')); } /* ============================ 玩法:集赞截图审核 ============================ */ pages.likereview = () => { const rows = db.likes.filter(l => matches({ ...l, title: actName(l.act) }) && (activeTab === '全部' || l.status === activeTab)); const p = db.likes.filter(l => l.status === '待审核').length; return heading('集赞截图审核', '人工核对用户上传的截图:截图上可见的点赞总数 ≥ 该活动门槛 → 通过并手动发放奖励;< 门槛 → 驳回并回传核对结果。', btn(icon('star') + '集赞玩法配置', 'goto-playlike')) + `
${stat('待审核', p, '建议 1 个工作日内处理', 'clock')}${stat('已通过', db.likes.filter(l => l.status === '已通过').length, '奖励已手动发放', 'check')}${stat('已驳回', db.likes.filter(l => l.status === '已驳回').length, '含原因,可重新提交', 'close')}${stat('疑似重复', 1, '同图重复提交自动置顶提醒', 'alert')}
` + notice('核对要点:截图需清晰显示点赞总数与该活动内容;与历史截图一致或高度相似时标记为疑似重复;核对结果与发放奖励都会留痕,用户端可查看审核状态与驳回原因。') + `
${tabs(['全部', '待审核', '已通过', '已驳回'])}${filters('搜索审核单号、用户或活动')}${table(['用户 / 审核单号', '活动 / 渠道', '人工核对结果', '提交时间', '状态', '操作'], slice(rows).map(l => [`${esc(l.user)}${l.id}`, `${esc(actName(l.act))}${platOf(l.plat).ic} ${platOf(l.plat).n} · ${l.shots} 张截图`, l.verified != null ? `截图点赞 ${l.verified} 个` : '待核对', esc(l.at), tag(l.status), link(l.status === '待审核' ? '审核' : '查看', 'like-detail', l.id)]))}${foot(rows.length)}
`; }; function likeModal(id) { const l = db.likes.find(x => x.id === id); if (!l) return; const a = act(l.act) || {}, goal = a.likeGoal || 38, P = platOf(l.plat); const shots = Array.from({ length: l.shots }, (_, i) => ``).join(''); const gifts = (a.likeRewards || []).map(x => tag(rewardName(x))).join(' ') || '该活动未勾选奖励'; openModal('集赞截图审核', `
${esc(l.user)}${tag(l.status)}${l.id} · ${esc(l.at)}
${detail('集赞活动', esc(a.title || '—'))}${detail('集赞渠道', `${P.ic} ${esc(P.n)}`)}${detail('上传截图', l.shots + ' 张')}${detail('活动门槛', `${goal} 个赞`)}${detail('达标奖励', gifts)}${detail('人工核对', l.verified != null ? `截图点赞 ${l.verified} 个 · 门槛 ${goal} 个` : '尚未核对')}${l.status === '已驳回' && l.reason ? detail('驳回原因', esc(l.reason)) : ''}${l.status === '已通过' ? detail('发放结果', l.granted ? '奖励已手动发放至用户「我的卡包」' : '已通过(无奖励配置)') : ''}

用户上传的截图

${shots} ${l.status === '待审核' ? `
` : ''}`, l.status === '待审核' ? btn('驳回(材料不清晰 / 疑似重复)', 'like-reject', id) + btn('核对确认', 'like-approve', id, 'primary') : l.status === '已通过' ? btn('查看发奖记录', 'grant-filter-act', l.act) + btn('关闭', 'close') : btn('允许重新提交', 'like-reopen', id, 'primary') + btn('关闭', 'close')); } function checkLike(id, v) { const l = db.likes.find(x => x.id === String(id)); if (!l) return; const a = act(l.act) || {}, goal = a.likeGoal || 38; l.verified = v; if (v >= goal) { l.status = '已通过'; l.reason = ''; const names = []; (a.likeRewards || []).forEach(rid => { const r = reward(rid); if (!r) return; if (db.grants.some(g => g.act === a.id && g.user === l.user && g.reward === rid)) { names.push(r.name + '(该活动已发放,不重复发放)'); return } grant(a.id, l.user, rid, '集赞有礼 · 人工核对通过', '人工发放', '已发放'); names.push(r.name); }); l.granted = true; save(); log('集赞核对通过并手动发奖', l.id, `截图点赞 ${v} 个(门槛 ${goal})→ ${names.join('、') || '该活动未配置奖励'}`); closeModal(); render(); openModal('核对通过 ✅ · 奖励已发放', `

审核单 ${l.id}(${esc(l.user)} · ${esc(platOf(l.plat).n)})人工核对结果:截图显示点赞 ${v} 个,达到「${esc(a.title || '')}」门槛 ${goal} 个。

奖励已手动发放:${names.join('、') || '该活动未勾选奖励'}(已进入用户「我的卡包」)。

用户端活动详情页状态同步为「审核通过 · 奖励已发放」,审核记录与截图永久留存,便于申诉与追溯。

`, btn('查看发奖记录', 'grant-filter-act', l.act, 'primary') + btn('返回列表', 'close')); } else { l.status = '已驳回'; l.reason = `人工核对:截图显示点赞 ${v} 个,未达到本活动门槛 ${goal} 个;继续集赞后可重新上传截图。`; save(); log('驳回集赞截图', l.id, `截图点赞 ${v} 个,未达门槛 ${goal} 个`); closeModal(); render(); openModal('核对未通过', `

审核单 ${l.id} 未通过。

${esc(l.reason)}

驳回结果已回传用户端(状态:审核未通过),用户可继续集赞后重新上传截图。

`, btn('知道了', 'close', '', 'primary')); } } /* ============================ 玩法:满额有礼 / 次数有礼 ============================ */ pages.playspend = () => { const live = db.activities.filter(a => (a.plays || []).includes('满额有礼')); return heading('满额有礼 · 阶梯配置', '活动周期内累计消费达标即领奖;自动发放与人工复核档位可分别设置。', btn(icon('plus') + '新增阶梯', 'tier-new', 'spend', 'primary')) + `
${stat('开启该玩法的活动', live.length, '通常为平台级活动', 'wallet')}${stat('阶梯档位', db.spendTiers.length, '按累计消费金额划分', 'chart')}${stat('自动发放档位', db.spendTiers.filter(t => t.auto).length, '达标即时入卡包', 'check')}${stat('人工复核档位', db.spendTiers.filter(t => !t.auto).length, '高价值奖励需复核', 'eye')}
` + notice('统计口径:活动周期内已支付且未退款的服务与活动订单金额;退款订单不计入。达标后按档位自动发放,或进入「发奖记录」等待运营手动发放。') + `

消费阶梯

金额从低到高,逐档达标逐档领取
${table(['档位', '达标条件', '奖励(活动奖励库)', '发放方式', '已发放', '操作'], db.spendTiers.map((t, i) => [`第 ${i + 1} 档${esc(t.label)}`, `累计消费 ≥ ¥ ${money(t.v)}`, tag(rewardName(t.reward)) + `${esc(t.sub || '')}`, t.auto ? tag('自动发放') : tag('人工发放'), db.grants.filter(g => g.from.includes('满额')).length ? int(Math.round(db.grants.filter(g => g.from.includes('满额')).length * (i + 1) / 3)) : 0, `${link('编辑', 'tier-edit', 'spend-' + i)}${link('停用', 'tier-toggle', 'spend-' + i)}`]))}

参与本玩法的活动

${table(['活动', '周期', '统计口径', '状态', '操作'], live.map(a => [esc(a.title), `${esc(String(a.start).slice(0, 10))} – ${esc(String(a.end).slice(0, 10))}`, '已支付且未退款订单金额', tag(a.status), link('查看活动', 'activity-detail', a.id)]))}
`; }; pages.playcount = () => { const live = db.activities.filter(a => (a.plays || []).includes('次数有礼')); return heading('次数有礼 · 指定服务', '指定服务累计完成次数达标领奖;次数按「订单完成」计算,退款订单不计入。', btn(icon('plus') + '新增阶梯', 'tier-new', 'count', 'primary')) + `
${stat('开启该玩法的活动', live.length, '通常与洗护类目绑定', 'clock')}${stat('阶梯档位', db.countTiers.length, '按完成次数划分', 'chart')}${stat('适用服务', 3, '上门洗护 / 喂遛 / 喂养', 'service')}${stat('达成发放', db.grants.filter(g => g.from.includes('次数')).length, '均为自动发放', 'gift')}
` + notice('同一订单多只宠物只计 1 次;仅统计活动周期内「服务完成」的订单。') + `

次数阶梯

${table(['档位', '达标次数', '奖励(活动奖励库)', '适用服务', '发放方式', '操作'], db.countTiers.map((t, i) => [`第 ${i + 1} 档${esc(t.label)}`, `完成 ${t.v} 次`, tag(rewardName(t.reward)) + `${esc(t.sub || '')}`, esc(t.svc), tag('自动发放'), `${link('编辑', 'tier-edit', 'count-' + i)}${link('停用', 'tier-toggle', 'count-' + i)}`]))}

参与本玩法的活动

${table(['活动', '周期', '指定服务', '状态', '操作'], live.map(a => [esc(a.title), `${esc(String(a.start).slice(0, 10))} – ${esc(String(a.end).slice(0, 10))}`, '上门洗护类目', tag(a.status), link('查看活动', 'activity-detail', a.id)]))}
`; }; /* ============================ 玩法:卡包活动 / 任务与抽奖 ============================ */ pages.playpack = () => heading('卡包活动 · 权益打包', '把多项权益打包一次性发放;领取条件分「无条件」与「有条件」,站内权益直接入卡包,站外权益跳转合作方。', btn(icon('plus') + '新建卡包', 'pack-new', '', 'primary')) + `
${stat('卡包总数', db.packs.length, '含联名卡包', 'ticket')}${stat('启用中', db.packs.filter(p => p.status === '启用').length, '停用后保留历史领取记录', 'check')}${stat('累计领取', int(db.packs.reduce((s, p) => s + p.claim, 0)), '含站内与站外权益', 'users')}${stat('有条件卡包', db.packs.filter(p => p.need.includes('有条件')).length, '需满足人群或档案条件', 'filter')}
` + `
${table(['卡包', '权益数', '领取条件', '适用人群', '活动周期', '累计领取', '状态', '操作'], db.packs.map(p => [`${esc(p.name)}${p.id}`, p.items + ' 项', p.need === '无条件领取' ? tag('无条件') : tag('有条件'), esc(p.crowd), esc(p.period), int(p.claim), tag(p.status), link('编辑', 'pack-edit', p.id) + link(p.status === '启用' ? '停用' : '启用', 'pack-toggle', p.id)]))}
`; pages.playtask = () => heading('任务与抽奖', '任务完成即发奖励,抽奖奖池按概率与库存配置;中奖记录可人工补发。', btn(icon('plus') + '新增任务', 'task-new', '', 'primary')) + `
${stat('任务数', db.tasks.length, '启用中的任务可被用户领取', 'target')}${stat('累计完成', int(db.tasks.reduce((s, t) => s + t.done, 0)), '全部任务合计', 'check')}${stat('奖池品类', db.prizes.length, '含优惠券 / 实物 / 服务权益', 'gift')}${stat('中奖待发放', db.draws.filter(d => d.status === '待发放').length, '实物类需填写收货地址', 'clock')}
` + `

任务配置

${table(['任务', '完成条件', '奖励', '频次限制', '累计完成', '状态', '操作'], db.tasks.map(t => [`${esc(t.name)}${t.id}`, esc(t.need), tag(rewardName(t.reward)), esc(t.limit), int(t.done), tag(t.status), link('编辑', 'task-edit', t.id) + link(t.status === '启用' ? '停用' : '启用', 'task-toggle', t.id)]))}

抽奖奖池

概率合计需为 100%
${table(['奖品', '中奖概率', '库存', '成本估算', '操作'], db.prizes.map((p, i) => [esc(p.name), p.rate + '%', p.stock ? int(p.stock) : '不限', `¥ ${money(p.cost)}`, link('编辑', 'prize-edit', String(i))]))}

中奖记录

实物与高价值奖励需人工发放
${table(['记录', '中奖用户', '奖品', '中奖时间', '状态', '操作'], db.draws.map(d => [d.id, esc(d.user), esc(d.prize), d.at, tag(d.status), d.status === '待发放' ? link('确认发放', 'draw-confirm', d.id) : '—']))}
`; /* ============================ 玩法:投稿与评价(规则配置) ============================ */ pages.playreview = () => heading('投稿与评价 · 规则配置', '投稿征集与订单评价共用的奖励规则;评价是活动效果与内容素材的主要来源。', btn(icon('plus') + '新增奖励规则', 'rule-new-review', '', 'primary')) + `
${stat('投稿奖励规则', 3, '通过 / 精选 / 分享回流三档', 'image')}${stat('评价奖励规则', 2, '带图评价自动发券', 'message')}${stat('累计投稿', db.posts.length, '含待审核与已驳回', 'edit')}${stat('累计评价', db.reviews.length, '服务订单 + 活动订单', 'star')}
` + `

投稿征集奖励规则

${table(['规则', '触发条件', '奖励', '发放方式', '状态'], [['投稿通过', '图片 + 文字通过机审与人审', tag('¥15 通用券'), tag('自动发放'), tag('启用')], ['被选为精选', '内容优质,运营标记精选', tag('¥50 通用券 + 社区置顶'), tag('自动发放'), tag('启用')], ['分享回流', '好友从分享卡片进入活动页满 30 人次', tag('抽奖次数 ×1(上限 3 次)'), tag('自动发放'), tag('启用')]])}

订单评价奖励规则

服务订单与活动订单通用
${table(['规则', '触发条件', '奖励', '发放方式', '状态'], [['带图评价', '上传宠物照片 + 文字评价并提交', tag('¥15 通用券'), tag('自动发放'), tag('启用')], ['精选评价', '运营标记精选', tag('¥50 通用券'), tag('人工发放'), tag('启用')]])}
`; /* ============================ 奖励中心 ============================ */ pages.rewards = () => heading('活动奖励库', '所有玩法(集赞 / 满额 / 次数 / 任务 / 投稿 / 评价)的奖励来源;发布活动时从此处勾选,避免重复创建券模板。', btn(icon('plus') + '新增奖励', 'reward-new', '', 'primary')) + `
${stat('奖励条目', db.rewards.length, '券 / 次数 / 实物 / 内容 / 服务权益', 'gift')}${stat('启用中', db.rewards.filter(r => r.status === '启用').length, '停用后不可在新活动中勾选', 'check')}${stat('库存总量', int(db.rewards.reduce((s, r) => s + r.stock, 0)), '不含不限量权益', 'box')}${stat('已发放', int(db.rewards.reduce((s, r) => s + r.used, 0)), `成本合计 ¥ ${money(db.rewards.reduce((s, r) => s + r.used * r.cost, 0))}`, 'wallet')}
` + notice('奖励是「活动 → 奖励」的唯一来源:活动发布向导的集赞配置只能从本库勾选启用中的奖励。停用不影响已发放记录。') + `
${filters('搜索奖励名称或使用条件')}${table(['奖励名称', '类型', '成本估算', '库存', '已发放', '累计成本', '库存占比', '状态', '操作'], slice(db.rewards.filter(matches)).map(r => [`${esc(r.name)}${esc(r.note || '')}`, tag(r.type), `¥ ${money(r.cost)}`, int(r.stock), int(r.used), `¥ ${money(r.used * r.cost)}`, `
`, tag(r.status), `${link('编辑', 'reward-edit', r.id)}${link(r.status === '启用' ? '停用' : '启用', 'reward-toggle', r.id)}`]))}${foot(db.rewards.filter(matches).length)}
`; pages.grants = () => { const rows = db.grants.filter(g => matches({ ...g, title: actName(g.act), rn: rewardName(g.reward) }) && (activeTab === '全部' || g.status === activeTab)); return heading('发奖记录', '每笔奖励都可追溯到来源玩法、活动与操作人;人工发放与自动发放分别标记。', btn(icon('download') + '导出', 'export', 'grants')) + `
${stat('发放记录', db.grants.length, '含待发放', 'check')}${stat('已发放', db.grants.filter(g => g.status === '已发放').length, `成本 ¥ ${money(db.grants.filter(g => g.status === '已发放').reduce((s, g) => s + costOf(g), 0))}`, 'gift')}${stat('人工发放', db.grants.filter(g => g.way === '人工发放').length, '集赞核对 / 精选投稿等', 'users')}${stat('待发放', db.grants.filter(g => g.status === '待发放').length, '需运营手动操作', 'clock')}
` + `
${tabs(['全部', '已发放', '待发放', '已作废'])}${filters('搜索活动、用户或奖励名称')}${table(['奖励记录', '活动', '用户', '来源玩法', '发放方式', '时间 / 操作人', '状态', '操作'], slice(rows).map(g => [`${esc(rewardName(g.reward))}${g.id}`, esc(actName(g.act)), esc(g.user), esc(g.from), g.way === '人工发放' ? tag('人工发放') : tag('自动发放'), `${esc(g.at)}${esc(g.op)}`, tag(g.status), g.status === '待发放' ? link('确认发放', 'grant-confirm', g.id) : link('详情', 'grant-detail', g.id)]))}${foot(rows.length)}
`; }; /* ============================ 内容与互动 ============================ */ pages.posts = () => { const rows = db.posts.filter(p => matches({ ...p, title2: actName(p.act) }) && (activeTab === '全部' || p.status === activeTab)); return heading('投稿审核', '机审(图片清晰度 / 敏感词)后进入人工精选;通过自动发券,精选额外奖励。', btn(icon('star') + '投稿规则配置', 'goto-playreview')) + `
${stat('待审核', db.posts.filter(p => p.status === '待审核').length, '建议 1 个工作日内处理', 'clock')}${stat('已精选', db.posts.filter(p => p.star).length, '额外奖励 ¥50 通用券', 'star')}${stat('已通过', db.posts.filter(p => p.status === '已通过').length, '自动发放 ¥15 通用券', 'check')}${stat('已驳回', db.posts.filter(p => p.status === '已驳回').length, '含驳回原因', 'close')}
` + notice('驳回常见原因:搬运网络图片、图文不符、含广告或联系方式、与活动主题无关。驳回后用户可修改重新提交。') + `
${tabs(['全部', '待审核', '已通过', '已精选', '已驳回'])}${filters('搜索投稿编号、用户或内容')}${table(['投稿', '用户', '图片', '提交时间', '状态', '操作'], slice(rows).map(p => [`${esc(p.title)}${esc(p.text).slice(0, 28)}…`, `${esc(p.user)}${esc(actName(p.act))}`, p.shots + ' 张', p.at, tag(p.status), link(p.status === '待审核' ? '审核' : '查看', 'post-detail', p.id)]))}${foot(rows.length)}
`; }; pages.reviews = () => { const rows = db.reviews.filter(r => matches({ ...r, title: actName(r.act) }) && (activeTab === '全部' || r.status === activeTab)); return heading('评价管理', '服务订单与活动订单的评价统一管理;带图评价审核通过后自动发券,优质评价可精选。', btn(icon('star') + '评价规则', 'goto-playreview')) + `
${stat('待审核', db.reviews.filter(r => r.status === '待审核').length, '含带图评价', 'clock')}${stat('带图评价', db.reviews.filter(r => r.shots > 0).length, '自动发放 ¥15 通用券', 'image')}${stat('精选评价', db.reviews.filter(r => r.star).length, '额外 ¥50 通用券(人工发放)', 'star')}${stat('平均星级', (db.reviews.reduce((s, r) => s + r.stars, 0) / db.reviews.length).toFixed(1), `${db.reviews.length} 条评价`, 'check')}
` + `
${tabs(['全部', '待审核', '已通过', '已精选', '已驳回'])}${filters('搜索评价编号、用户或订单')}${table(['评价', '类型 / 订单', '用户', '星级', '图片', '时间', '状态', '操作'], slice(rows).map(r => [`${esc(r.text).slice(0, 24)}…${r.id}`, `${esc(r.type)}${esc(actName(r.act) || r.order)}`, esc(r.user), '★'.repeat(r.stars) + '' + '★'.repeat(5 - r.stars) + '', r.shots ? r.shots + ' 张' : '无图', r.at, tag(r.status), link(r.status === '待审核' ? '审核' : '查看', 'review-detail', r.id)]))}${foot(rows.length)}
`; }; pages.shares = () => { const tot = db.shares.reduce((s, x) => s + x.share, 0), back = db.shares.reduce((s, x) => s + x.back, 0), conv = db.shares.reduce((s, x) => s + x.converted, 0); return heading('分享回流', '外部渠道分享 → 回流浏览 → 报名转化;渠道不限平台,新增渠道无需改动流程。', btn(icon('link') + '渠道管理', 'goto-channels')) + `
${stat('分享次数', int(tot), '活动海报分享到外部渠道', 'share')}${stat('回流浏览', int(back), `人均回流 ${(back / Math.max(1, tot)).toFixed(1)} 次`, 'users')}${stat('回流转化', int(conv), `转化率 ${(conv / Math.max(1, back) * 100).toFixed(1)}%`, 'chart')}${stat('集赞达标奖励', db.grants.filter(g => g.from.includes('集赞')).length, '人工核对通过后发放', 'gift')}
` + notice('回流数据基于分享卡片携带的渠道参数统计(演示数据)。集赞活动本身不依赖平台接口:无论用户在哪个渠道集赞,最终都以上传截图 + 后台人工核对为准。') + `

按渠道统计

${table(['渠道', '分享次数', '回流浏览', '回流转化', '分享 → 回流', '回流 → 转化', '集赞奖励触发'], db.shares.map(s => [`${platOf(s.ch).ic} ${esc(platOf(s.ch).n)}`, int(s.share), int(s.back), int(s.converted), `
${(s.back / Math.max(1, s.share)).toFixed(1)} 倍`, `${(s.converted / Math.max(1, s.back) * 100).toFixed(1)}%`, s.reward ? int(s.reward) + ' 次' : '—']))}

分活动分享效果

${table(['活动', '是否开启分享集赞', '集赞门槛', '进入集赞列表', '分享文案'], db.activities.filter(a => a.share).map(a => [esc(a.title), tag('已开启'), a.likeGoal + ' 个赞', tag('是'), esc(a.shareTitle || a.title)]))}
`; }; /* ============================ 集市招商 ============================ */ pages.fair = () => { const sold = db.booths.filter(b => b.sold), free = db.booths.filter(b => !b.sold && !b.reserved); const fee = sold.reduce((s, b) => s + b.price, 0); return heading('集市与展位', '宠物集市(线下大型活动)的展位配置与招商总览;商家在小程序端提交报名,资质审核与展位分配在后台完成。', btn(icon('plus') + '新增展位', 'booth-new', '', 'primary') + btn(icon('users') + '商家报名审核', 'goto-merchants')) + `
${stat('展位总数', db.booths.length, `已售 ${sold.length} · 空余 ${free.length}`, 'box')}${stat('已确认展位费', `¥ ${money(fee)}`, '已通过商家合计', 'wallet')}${stat('待审核商家', db.merchants.filter(m => m.status === '待审核').length, '需核验资质与品类', 'clock')}${stat('招商展位类型', 4, '标准 / 精品 / 特装 / 公益', 'layers')}
` + notice('审核要点:营业执照经营范围、行业资质、品类与招商范围匹配(活体销售不在招商范围);展位分配后自动通知商家,展位费审核通过后 7 日内缴清。') + `

展位图 · N3 馆 A 区

绿色已分配 · 灰色平台预留 · 白色空余
${db.booths.map(b => `
${b.no}${b.sold ? esc(b.brand) : b.reserved ? '平台预留' : '空余'}${b.type} · ${b.price ? '¥' + money(b.price) : '免费'}
`).join('')}

展位类型与定价

${table(['展位类型', '规格', '价格', '本届数量', '已售', '说明'], [['标准展位', '3m × 3m', '¥ 1,200.00', 6, db.booths.filter(b => b.type === '标准展位' && b.sold).length, '含基础搭建、1 桌 2 椅、电源、楣板'], ['精品展位', '3m × 6m', '¥ 2,400.00', 3, db.booths.filter(b => b.type === '精品展位' && b.sold).length, '含主题搭建、洽谈区、双电源、主视觉露出'], ['特装展位', '6m × 6m', '¥ 4,800.00', 1, db.booths.filter(b => b.type === '特装展位' && b.sold).length, '独立展区、舞台露出、直播机位'], ['公益展位', '3m × 3m', '免费', 2, 0, '公益机构 / 义诊,需资质审核']])}
`; }; pages.merchants = () => { const rows = db.merchants.filter(m => matches(m) && (activeTab === '全部' || m.status === activeTab)); return heading('商家报名审核', '商家在小程序端「宠物集市 → 商家参展报名」提交,后台审核资质、品类与展位分配,结果回传商家端。', btn(icon('box') + '查看展位图', 'goto-fair') + btn(icon('upload') + '模拟小程序提交报名', 'merchant-simulate')) + `
${stat('待审核', db.merchants.filter(m => m.status === '待审核').length, '1 个工作日内处理', 'clock')}${stat('已通过', db.merchants.filter(m => m.status === '已通过').length, '已分配展位', 'check')}${stat('已驳回', db.merchants.filter(m => m.status === '已驳回').length, '含原因,可重新提交', 'close')}${stat('展位费合计', `¥ ${money(db.merchants.filter(m => m.status === '已通过').reduce((s, m) => s + m.fee, 0))}`, '已通过商家合计', 'wallet')}
` + `
${tabs(['全部', '待审核', '已通过', '已驳回'])}${filters('搜索商家名称、品类或联系人或申请编号')}${table(['商家 / 申请编号', '品类 / 展位类型', '联系人', '展位', '展位费', '申请时间', '状态', '操作'], slice(rows).map(m => [`${esc(m.brand)}${m.id}`, `${esc(m.cat)}${esc(m.typeName)}`, `${esc(m.contact)}${esc(m.phone)}`, m.booth ? `${m.booth}` : '未分配', `¥ ${money(m.fee)}`, m.at, tag(m.status), link(m.status === '待审核' ? '审核' : '查看', 'merchant-detail', m.id)]))}${foot(rows.length)}
`; }; function merchantModal(id) { const m = db.merchants.find(x => x.id === id); if (!m) return; const freeBooths = db.booths.filter(b => !b.sold && !b.reserved).map(b => b.no); openModal('商家参展审核', `
${esc(m.brand)}${tag(m.status)}${m.id} · ${esc(m.at)}
${detail('经营品类', esc(m.cat))}${detail('展位类型', esc(m.typeName))}${detail('联系人', esc(m.contact) + ' · ' + esc(m.phone))}${detail('微信 / 邮箱', esc(m.wechat || '—'))}${detail('展位费', '¥ ' + money(m.fee))}${detail('意向展位(商家自选)', m.boothIntent ? '' + esc(m.boothIntent) + '(招商团队最终确认)' : '未填写')}${detail('当前展位', m.booth || '未分配')}${detail('申请来源', '小程序端 · 商家参展报名')}

品牌介绍

${esc(m.intro || '—')}

资质材料

${m.license ? tag('营业执照 · 已上传') : tag('营业执照 · 缺失')} ${m.cert ? tag('行业资质 · 已上传') : tag('行业资质 · 缺失')}

${m.reason ? notice('驳回原因:' + esc(m.reason), true) : ''} ${m.status !== '已驳回' ? `` : ''}`, `${m.status === '待审核' ? btn('驳回申请', 'merchant-reject', id) + btn('审核通过并分配展位', 'merchant-approve', id, 'primary') : m.status === '已通过' ? btn('保存展位调整', 'merchant-approve', id, 'primary') + btn('关闭', 'close') : btn('允许重新提交', 'merchant-reopen', id, 'primary') + btn('关闭', 'close')}`); } function postModal(id) { const p = db.posts.find(x => x.id === id); if (!p) return; openModal('投稿审核', `
${esc(p.user)}${tag(p.status)}${p.id} · ${esc(p.at)}
${detail('投稿标题', esc(p.title))}${detail('所属活动', esc(actName(p.act)))}${detail('图片数量', p.shots + ' 张')}${detail('机审结果', p.status === '已驳回' ? tag('未通过') : tag('通过 · 无敏感词'))}

投稿正文

${esc(p.text)}

${Array.from({ length: p.shots }, (_, i) => `
🖼 宠物照片 ${i + 1}
`).join('')}
${p.reason ? notice('驳回原因:' + esc(p.reason), true) : ''} ${p.status === '待审核' ? notice('通过后将自动发放「¥15 通用券」;标记精选额外发放「¥50 通用券」并社区置顶。') : ''}`, p.status === '待审核' ? btn('驳回', 'post-reject', id) + btn('通过并精选', 'post-star', id) + btn('通过并发放 ¥15 券', 'post-approve', id, 'primary') : p.status === '已驳回' ? btn('允许重新提交', 'post-reopen', id, 'primary') + btn('关闭', 'close') : btn('关闭', 'close')); } function reviewModal(id) { const r = db.reviews.find(x => x.id === id); if (!r) return; openModal('评价审核', `
${esc(r.user)}${tag(r.status)}${r.id} · ${esc(r.at)}
${detail('评价类型', esc(r.type))}${detail('关联订单', esc(r.order))}${detail('关联活动', esc(actName(r.act) || '—'))}${detail('评分', '★'.repeat(r.stars) + `(${r.stars} 星)`)}${detail('图片', r.shots ? r.shots + ' 张' : '无图(不发券)')}${detail('已发奖励', r.reward ? esc(rewardName(r.reward)) : '未发放')}

评价内容

${esc(r.text)}

${r.shots ? `
${Array.from({ length: r.shots }, (_, i) => `
🖼 宠物照片 ${i + 1}
`).join('')}
` : ''} ${r.reason ? notice('驳回原因:' + esc(r.reason), true) : ''} ${r.status === '待审核' ? notice(r.shots ? '带图评价通过后自动发放「¥15 通用券」;标记精选额外发放「¥50 通用券」。' : '无图评价不触发自动发券,仍可标记精选。') : ''}`, r.status === '待审核' ? btn('驳回', 'review-reject', id) + btn('通过并精选', 'review-star', id) + btn('通过', 'review-approve', id, 'primary') : btn('关闭', 'close')); } /* ============================ 用户与人群 ============================ */ pages.members = () => { const rows = db.members.filter(m => matches(m) && (activeTab === '全部' || m.level === activeTab)); return heading('会员与参与', '查看用户的参与次数、集赞提交与奖励获得,便于识别活跃用户与异常账号。', btn(icon('download') + '导出', 'export', 'members')) + `
${stat('会员总数', int(10000 + db.members.length), '演示数据', 'users')}${stat('参与活动会员', db.members.filter(m => m.acts > 0).length, '至少参与过 1 场活动', 'activity')}${stat('集赞活跃', db.members.filter(m => m.likes > 0).length, '提交过集赞截图', 'star')}${stat('累计获得奖励', int(db.members.reduce((s, m) => s + m.rewards, 0)), '含自动与人工发放', 'gift')}
` + `
${tabs(['全部', '铂金', '金卡', '银卡'])}${filters('搜索会员昵称、手机号或会员编号')}${table(['会员', '手机号', '宠物数', '等级', '注册时间', '参与活动', '集赞提交', '获得奖励', '累计消费', '操作'], slice(rows).map(m => [`${esc(m.name)}${m.id}`, esc(m.phone), m.pets + ' 只', tag(m.level), m.join, m.acts + ' 场', m.likes + ' 次', m.rewards + ' 项', `¥ ${money(m.spend)}`, link('详情', 'member-detail', m.id)]))}${foot(rows.length)}
`; }; pages.segments = () => heading('人群包', '为活动与奖励圈选目标人群;人群包可按条件自动计算,供活动定向邀请与召回使用。', btn(icon('plus') + '新建人群包', 'segment-new', '', 'primary')) + `
${stat('人群包数', db.segments.length, '启用中可用于活动邀请', 'layers')}${stat('覆盖用户', int(db.segments.reduce((s, x) => s + x.size, 0)), '含重叠,实际去重后略少', 'users')}${stat('启用中', db.segments.filter(s => s.status === '启用').length, '停用后不再参与计算', 'check')}${stat('关联活动', db.segments.filter(s => s.used && s.used !== '—').length, '已用于活动定向', 'activity')}
` + `
${table(['人群包名称', '圈选条件', '预估人数', '关联活动 / 用途', '状态', '操作'], db.segments.map(s => [`${esc(s.name)}`, esc(s.cond), int(s.size), esc(s.used), tag(s.status), link('编辑', 'segment-edit', s.name) + link(s.status === '启用' ? '停用' : '启用', 'segment-toggle', s.name)]))}
`; /* ============================ 数据分析 ============================ */ pages.reports = () => { const paid = db.registrations.filter(r => r.payment === '支付成功').reduce((s, r) => s + r.amount, 0); const live = db.activities.filter(a => a.status === '已发布'); return heading('活动效果分析', '以活动为单位汇总报名、支付、集赞、奖励与核销;分享访问 UV 与归因窗口暂无事件数据,不虚构转化率。', btn(icon('download') + '导出汇总', 'export', 'reports')) + `
${stat('活动支付金额', `¥ ${money(paid)}`, '成功支付报名订单合计', 'wallet')}${stat('集赞提交', db.likes.length, `通过 ${db.likes.filter(l => l.status === '已通过').length} · 驳回 ${db.likes.filter(l => l.status === '已驳回').length}`, 'star')}${stat('投稿 / 评价', db.posts.length + db.reviews.length, `投稿 ${db.posts.length} · 评价 ${db.reviews.length}`, 'image')}${stat('凭证核销', db.verifications.length, '活动入场核销记录', 'scan')}

报名支付趋势

${['7天', '14天'].map(d => ``).join('')}
${trendChart()}

报名来源分布

含待支付与取消记录
${(() => { const c = {}; db.registrations.forEach(r => c[r.channel] = (c[r.channel] || 0) + 1); return Object.entries(c).sort((a, b) => b[1] - a[1]).map(([k, n]) => `
${n}
`).join('') })()}

分活动汇总

报名 / 支付 / 集赞 / 奖励 / 核销一表看清
${table(['活动', '玩法', '报名', '支付金额', '集赞提交 / 通过', '奖励发放', '奖励成本', '核销', '操作'], db.activities.map(a => { const rr = db.registrations.filter(r => r.activity === a.id); const amt = rr.filter(r => r.payment === '支付成功').reduce((s, r) => s + r.amount, 0); const lk = db.likes.filter(l => l.act === a.id); const gs = db.grants.filter(g => g.act === a.id); const vf = db.verifications.filter(v => v.activity === a.id).length; return [esc(a.title), (a.plays || []).map(p => `${p}`).join(' '), rr.length + ' 人', `¥ ${money(amt)}`, lk.length ? `${lk.length} / ${lk.filter(l => l.status === '已通过').length}` : '—', gs.length + ' 笔', `¥ ${money(gs.filter(g => g.status === '已发放').reduce((s, g) => s + costOf(g), 0))}`, a.verify ? vf + ' 次' : '无需核销', link('详情', 'activity-detail', a.id)] }))}
`; }; pages.cost = () => { const issued = db.grants.filter(g => g.status === '已发放'); const cost = issued.reduce((s, g) => s + costOf(g), 0); const byAct = db.activities.map(a => { const gs = issued.filter(g => g.act === a.id); return { a, n: gs.length, c: gs.reduce((s, g) => s + costOf(g), 0) } }).filter(x => x.n).sort((x, y) => y.c - x.c); const byReward = db.rewards.map(r => ({ r, n: issued.filter(g => g.reward === r.id).length })).filter(x => x.n).sort((x, y) => y.n * y.r.cost - x.n * x.r.cost); return heading('奖励成本与 ROI', '按活动与奖励品类核对发放成本;ROI 以活动报名支付金额 ÷ 奖励成本近似估算。', btn(icon('download') + '导出成本表', 'export', 'cost')) + `
${stat('已发奖励', issued.length, `待发放 ${db.grants.filter(g => g.status === '待发放').length} 笔`, 'check')}${stat('奖励总成本', `¥ ${money(cost)}`, '按奖励库成本估算单价核算', 'wallet')}${stat('人工发放占比', (issued.filter(g => g.way === '人工发放').length / Math.max(1, issued.length) * 100).toFixed(0) + '%', '集赞与精选类奖励', 'users')}${stat('综合 ROI', (db.registrations.filter(r => r.payment === '支付成功').reduce((s, r) => s + r.amount, 0) / Math.max(1, cost)).toFixed(2) + ' 倍', '活动支付金额 ÷ 奖励成本', 'chart')}
` + notice('成本为奖励库中的成本估算单价 × 已发放数量,用于活动预算与效果评估,不代表实际财务结算金额。', true) + `

按活动排名

${table(['活动', '发放笔数', '奖励成本'], byAct.map(x => [esc(x.a.title), x.n + ' 笔', `¥ ${money(x.c)}`]))}

按奖励品类

${table(['奖励', '类型', '发放次数', '累计成本'], byReward.map(x => [esc(x.r.name), tag(x.r.type), x.n + ' 次', `¥ ${money(x.n * x.r.cost)}`]))}

奖励库库存与占用

${table(['奖励', '类型', '库存', '已发放', '占用率', '累计成本', '操作'], db.rewards.map(r => [esc(r.name), tag(r.type), int(r.stock), int(r.used), `
`, `¥ ${money(r.used * r.cost)}`, link('编辑', 'reward-edit', r.id)]))}
`; }; /* ============================ 系统设置 ============================ */ pages.channels = () => heading('渠道管理', '集赞与分享支持的外部渠道;渠道不限平台,新增渠道不影响审核流程(仍以截图人工核对为准)。', btn(icon('plus') + '新增渠道', 'channel-new', '', 'primary')) + notice('集赞门槛仍按活动单独配置;这里的「渠道」决定用户在活动详情页分享时可选择的范围,以及后台审核列表的渠道标签。') + `
${table(['渠道', '类型', '分享', '集赞收集', '奖励触发', '说明', '状态', '操作'], db.channels.map(c => [`${platOf(c.key).ic} ${esc(c.cn)}${esc(c.key)}`, esc(c.type), tag(c.share), tag(c.collect), tag(c.reward), esc(c.note), tag(c.status), link('编辑', 'channel-edit', c.key) + link(c.status === '启用' ? '停用' : '启用', 'channel-toggle', c.key)]))}
`; pages.rules = () => heading('审核规则', '把人工审核的判断标准固化为规则,减少驳回争议;规则变更会写入操作日志。', btn(icon('plus') + '新增规则', 'rule-new', '', 'primary')) + `
${table(['规则名称', '规则说明', '执行方', '状态', '操作'], db.rules.map((r, i) => [`${esc(r.name)}`, esc(r.desc), tag(r.who), tag(r.on ? '启用' : '停用'), link('编辑', 'rule-edit', String(i)) + link(r.on ? '停用' : '启用', 'rule-toggle', String(i))]))}

审核时效约定(建议)

集赞截图审核1 个工作日内完成;审核期间用户端状态为「奖励审核中」
投稿 / 评价审核机审即时,人审 1 个工作日内;通过后立即发券
商家报名审核1 个工作日内完成,展位分配后自动通知商家
奖励发放自动类即时到账;人工类在审核通过当日完成发放
`; pages.messages = () => heading('消息通知', '审核结果、发奖与活动提醒的通知模板;模板变量会在发送时替换。', btn(icon('plus') + '新增模板', 'msg-new', '', 'primary')) + `
${table(['模板名称', '模板内容(含变量)', '触达渠道', '状态', '操作'], db.messages.map((m, i) => [`${esc(m.name)}`, `${esc(m.tpl)}`, tag(m.ch), tag(m.status), link('编辑', 'msg-edit', String(i)) + link(m.status === '启用' ? '停用' : '启用', 'msg-toggle', String(i))]))}

可用变量

${['{活动名称}', '{活动时间}', '{活动地点}', '{奖励名称}', '{门槛点赞数}', '{核对点赞数}', '{驳回原因}', '{凭证码}', '{展位号}', '{金额}', '{投稿标题}'].map(v => tag(v)).join(' ')}

${notice('小程序订阅消息需用户授权;未授权时降级为站内消息中心。')}

近期发送情况

集赞审核通过通知 · 128 条送达率 98.4% · 点击率 22.1%
集赞审核驳回通知 · 34 条送达率 99.1% · 二次提交率 41.2%
投稿通过通知 · 76 条送达率 97.4% · 点击率 18.6%
活动开始提醒 · 412 条送达率 99.2% · 核销率提升 6.8%

发送统计为演示数据;模板停用后不再触达。

`; pages.accounts = () => heading('账号与权限', '按岗位分配能力与活动范围,敏感操作(发奖、核销、退款)全部留痕。', btn(icon('plus') + '新增账号', 'account-new', '', 'primary')) + notice('顶部角色切换用于评审权限差异,不是真实登录。正式系统需要服务端鉴权;本原型不采集密码。') + `

后台账号

${table(['账号', '角色', '数据范围', '状态', '操作'], db.accounts.map(a => [esc(a.name), tag(a.role), esc(a.scope), tag(a.status), `${link('配置授权', 'account-edit', a.id)}${a.role !== '管理员' ? link(a.status === '启用' ? '停用' : '启用', 'account-toggle', a.id) : '管理员账号'}`]))}

角色能力矩阵

${table(['角色', '活动配置', '集赞审核', '投稿 / 评价审核', '发奖与奖励库', '凭证核销', '成本与分析'], [['管理员', '✓', '✓', '✓', '✓', '全部活动', '✓'], ['活动运营', '✓', '✓', '✓', '✓', '—', '✓'], ['内容审核', '—', '✓', '✓', '—', '—', '—'], ['核销员', '—', '—', '—', '—', '仅授权活动', '—'], ['财务', '—', '—', '—', '只读', '—', '✓']])}
`; pages.logs = () => { const rows = db.logs.filter(matches); return heading('操作日志', '记录活动变更、集赞核对与发奖、投稿与评价审核、展位分配、核销与退款等关键操作。', btn(icon('download') + '导出日志', 'export', 'logs')) + `
${filters('搜索操作人、操作内容或业务编号')}${table(['操作时间', '操作人', '操作内容', '业务对象', '结果 / 变更'], slice(rows).map(l => [esc(l.time), esc(l.actor), esc(l.action), esc(l.object), esc(l.result)]))}${foot(rows.length)}
`; }; /* ============================ 通用弹窗(编辑类) ============================ */ function rewardModal(id) { const r = id ? reward(String(id)) : { name: '', type: '优惠券', cost: 0, stock: 100, used: 0, note: '', status: '启用' }; openModal(id ? '编辑奖励' : '新增奖励', `
${field('rw-name', '奖励名称', r.name)}${field('rw-cost', '成本估算(元)', r.cost, 'number')}${field('rw-stock', '库存总量', r.stock, 'number')}${field('rw-note', '使用条件 / 说明', r.note)}

奖励是活动配置的唯一下拉来源;停用后不可在新活动中勾选,已发放记录不受影响。

`, btn('取消', 'close') + btn('保存', 'reward-save', id || '', 'primary')); } function field(name, title, value = '', type = 'text', hint = '') { return `` } function simpleForm(title, html, action, id = '') { openModal(title, html, btn('取消', 'close') + btn('保存', action, id, 'primary')) } const val = id => { const el = $('#' + id); return el ? el.value : '' }; const num = (id, d = 0) => Number(val(id)) || d; function pickList(items, cls) { return items.map(x => ``).join('') } /* ============================ 模块二的 actions ============================ */ Object.assign(actions, { 'goto-likereview': () => navigate('likereview'), 'goto-playlike': () => navigate('playlike'), 'goto-playreview': () => navigate('playreview'), 'goto-channels': () => navigate('channels'), 'goto-fair': () => navigate('fair'), 'goto-merchants': () => navigate('merchants'), 'like-cfg-edit': id => likeCfgModal(String(id)), 'open-like-wiz': id => { if (!can('wizard')) { toast('当前演示角色没有编辑活动权限'); return } openWizard(String(id)); if (wizard) { wizard.step = 2; wizard.error = ''; render() } }, 'like-cfg-save': id => { const a = act(String(id)); if (!a) return; a.share = $('#cfg-share').checked; a.likeGoal = Math.max(1, num('cfg-goal', 38)); a.likePlatforms = $$('.cfg-plat:checked').map(b => b.value); a.likeRewards = $$('.cfg-reward:checked').map(b => b.value); a.shareTitle = val('cfg-title'); if (a.share && !(a.plays || []).includes('集赞有礼')) a.plays.push('集赞有礼'); save(); log('修改活动集赞配置', a.id, `${a.share ? '已开启' : '已关闭'} · 门槛 ${a.likeGoal} 个赞 · ${a.likePlatforms.length} 个渠道 · ${a.likeRewards.length} 项奖励`); closeModal(); toast('集赞配置已保存并同步小程序端'); render(); }, 'like-detail': id => likeModal(String(id)), 'like-approve': id => { const v = num('like-verified', 0); if (!$('#like-verified') || !$('#like-verified').value) { toast('请先填写截图上可见的点赞数'); return } checkLike(id, v) }, 'like-reject': id => { const l = db.likes.find(x => x.id === String(id)); const a = act(l.act) || {}; l.status = '已驳回'; save(); log('驳回集赞截图', l.id, '材料不清晰 / 疑似重复,需重新提交'); closeModal(); render(); openModal('已驳回', `

审核单 ${l.id} 已驳回,原因:材料不清晰或疑似重复提交,请用户重新上传更清晰的点赞截图。

用户端状态同步为「审核未通过」,可继续集赞后重新上传。

`, btn('知道了', 'close', '', 'primary')); }, 'like-reopen': id => { const l = db.likes.find(x => x.id === String(id)); l.status = '待审核'; l.verified = null; l.reason = ''; save(); log('允许重新提交集赞截图', l.id, '重置为待审核'); closeModal(); toast('已重置为待审核'); render() }, 'like-filter-act': id => { closeModal(); navigate('likereview'); toast('已定位活动「' + actName(String(id)) + '」的集赞提交(演示:列表中可按活动名称检索)') }, 'grant-filter-act': id => { closeModal(); navigate('grants'); toast('已定位活动「' + actName(String(id)) + '」的发奖记录') }, 'grant-confirm': id => { const g = db.grants.find(x => x.id === String(id)); g.status = '已发放'; g.at = nowStr(); g.op = role; const r = reward(g.reward); if (r) r.used++; save(); log('手动发放奖励', g.id, `${rewardName(g.reward)} → ${g.user}`); toast('奖励已发放:' + rewardName(g.reward)); render(); }, 'grant-detail': id => { const g = db.grants.find(x => x.id === String(id)); openModal('发奖详情', `
${esc(rewardName(g.reward))}${tag(g.status)}${g.id}
${detail('活动', esc(actName(g.act)))}${detail('用户', esc(g.user))}${detail('来源玩法', esc(g.from))}${detail('发放方式', esc(g.way))}${detail('操作人', esc(g.op))}${detail('时间', esc(g.at))}${detail('成本估算', '¥ ' + money(costOf(g)))}
`, g.status === '待发放' ? btn('确认发放', 'grant-confirm', id, 'primary') + btn('关闭', 'close') : btn('关闭', 'close')) }, 'reward-new': () => rewardModal(null), 'reward-edit': id => rewardModal(String(id)), 'reward-save': id => { const name = val('rw-name'); if (!name) { toast('请填写奖励名称'); return } if (id) { const r = reward(String(id)); r.name = name; r.type = val('rw-type'); r.cost = num('rw-cost'); r.stock = num('rw-stock', 1); r.note = val('rw-note'); log('编辑奖励', r.id, r.name) } else { const r = { id: 'RW' + String(Date.now()).slice(-4), name, type: val('rw-type'), cost: num('rw-cost'), stock: num('rw-stock', 1), used: 0, status: '启用', note: val('rw-note') }; db.rewards.push(r); log('新增奖励', r.id, r.name) } save(); closeModal(); toast('已保存奖励'); render(); }, 'reward-toggle': id => { const r = reward(String(id)); r.status = r.status === '启用' ? '停用' : '启用'; save(); log('调整奖励状态', r.id, r.status); toast('已' + r.status + ':' + r.name); render() }, 'tier-new': kind => simpleForm('新增阶梯', `
${field('tier-v', kind === 'count' ? '达标次数' : '达标金额(元)', kind === 'count' ? 3 : 299, 'number')}${field('tier-label', '奖励名称(展示文案)', '')}
`, 'tier-save', kind), 'tier-save': kind => { const v = num('tier-v', 1); const item = { v, label: val('tier-label') || rewardName(val('tier-reward')), sub: '由后台阶梯配置', reward: val('tier-reward'), auto: val('tier-auto') === '1' }; (String(kind) === 'count' ? db.countTiers : db.spendTiers).push(item); save(); log('新增奖励阶梯', String(kind), `${v} → ${item.label}`); closeModal(); toast('已新增阶梯'); render(); }, 'tier-edit': key => { const [kind, i] = String(key).split('-'); const t = (kind === 'count' ? db.countTiers : db.spendTiers)[Number(i)]; if (!t) return; simpleForm('编辑阶梯', `
${field('tier-v', kind === 'count' ? '达标次数' : '达标金额(元)', t.v, 'number')}${field('tier-label', '奖励名称(展示文案)', t.label)}
`, 'tier-save2', kind + '-' + i); }, 'tier-save2': key => { const [kind, i] = String(key).split('-'); const t = (kind === 'count' ? db.countTiers : db.spendTiers)[Number(i)]; if (!t) return; t.v = num('tier-v', t.v); t.label = val('tier-label') || t.label; t.reward = val('tier-reward'); t.auto = val('tier-auto') === '1'; save(); log('编辑奖励阶梯', kind, `${t.v} → ${t.label}`); closeModal(); toast('已保存阶梯'); render(); }, 'tier-toggle': key => { const [kind, i] = String(key).split('-'); const arr = kind === 'count' ? db.countTiers : db.spendTiers; const t = arr[Number(i)]; if (!t) return; t.status = t.status === '停用' ? '启用' : '停用'; save(); toast('已' + t.status + '该档位(演示)'); log('调整阶梯状态', key, t.status) }, 'pack-new': () => simpleForm('新建卡包', `
${field('pack-name', '卡包名称', '')}${field('pack-items', '权益数量', 3, 'number')}${field('pack-crowd', '适用人群', '全体用户')}${field('pack-period', '活动周期', '2026.09.22 – 10.31')}
`, 'pack-save'), 'pack-save': id => { const p = { id: 'PK' + String(Date.now()).slice(-4), name: val('pack-name') || '未命名卡包', items: num('pack-items', 3), need: val('pack-need'), crowd: val('pack-crowd'), period: val('pack-period'), claim: 0, status: '启用' }; db.packs.push(p); save(); log('新建卡包', p.id, p.name); closeModal(); toast('已新建卡包'); render() }, 'pack-edit': id => { const p = db.packs.find(x => x.id === String(id)); simpleForm('编辑卡包', `
${field('pack-name', '卡包名称', p.name)}${field('pack-items', '权益数量', p.items, 'number')}${field('pack-crowd', '适用人群', p.crowd)}${field('pack-period', '活动周期', p.period)}
`, 'pack-save2', p.id) }, 'pack-save2': id => { const p = db.packs.find(x => x.id === String(id)); p.name = val('pack-name'); p.items = num('pack-items', p.items); p.need = val('pack-need'); p.crowd = val('pack-crowd'); p.period = val('pack-period'); save(); log('编辑卡包', p.id, p.name); closeModal(); toast('已保存卡包'); render() }, 'pack-toggle': id => { const p = db.packs.find(x => x.id === String(id)); p.status = p.status === '启用' ? '停用' : '启用'; save(); log('调整卡包状态', p.id, p.status); toast('已' + p.status + ':' + p.name); render() }, 'task-new': () => simpleForm('新增任务', `
${field('tk-name', '任务名称', '')}${field('tk-need', '完成条件', '')}${field('tk-limit', '频次限制', '每人 1 次')}
`, 'task-save'), 'task-save': id => { const t = { id: 'TK' + String(Date.now()).slice(-4), name: val('tk-name') || '未命名任务', need: val('tk-need'), reward: val('tk-reward'), limit: val('tk-limit'), done: 0, status: '启用' }; db.tasks.push(t); save(); log('新增任务', t.id, t.name); closeModal(); toast('已新增任务'); render() }, 'task-edit': id => { const t = db.tasks.find(x => x.id === String(id)); simpleForm('编辑任务', `
${field('tk-name', '任务名称', t.name)}${field('tk-need', '完成条件', t.need)}${field('tk-limit', '频次限制', t.limit)}
`, 'task-save2', t.id) }, 'task-save2': id => { const t = db.tasks.find(x => x.id === String(id)); t.name = val('tk-name'); t.need = val('tk-need'); t.reward = val('tk-reward'); t.limit = val('tk-limit'); save(); log('编辑任务', t.id, t.name); closeModal(); toast('已保存任务'); render() }, 'task-toggle': id => { const t = db.tasks.find(x => x.id === String(id)); t.status = t.status === '启用' ? '停用' : '启用'; save(); log('调整任务状态', t.id, t.status); toast('已' + t.status + ':' + t.name); render() }, 'prize-edit': i => { const p = db.prizes[Number(i)]; simpleForm('编辑奖品', `
${field('pz-name', '奖品名称', p.name)}${field('pz-rate', '中奖概率(%)', p.rate, 'number')}${field('pz-stock', '库存', p.stock, 'number')}${field('pz-cost', '成本估算(元)', p.cost, 'number')}
`, 'prize-save', String(i)) }, 'prize-save': i => { const p = db.prizes[Number(i)]; p.name = val('pz-name'); p.rate = num('pz-rate', p.rate); p.stock = num('pz-stock', p.stock); p.cost = num('pz-cost', p.cost); save(); log('编辑抽奖奖品', p.name, `概率 ${p.rate}%`); closeModal(); toast('已保存奖品'); render() }, 'draw-confirm': id => { const d = db.draws.find(x => x.id === String(id)); d.status = '已发放'; save(); log('确认发放中奖奖品', d.id, `${d.prize} → ${d.user}`); toast('已发放:' + d.prize); render() }, 'rule-new-review': () => toast('投稿 / 评价奖励规则为固定三档结构,可在奖励库中调整对应奖励内容(演示)'), 'rule-new': () => simpleForm('新增审核规则', `
${field('rl-name', '规则名称', '')}${field('rl-desc', '规则说明', '')}${field('rl-who', '执行方', '活动运营')}
`, 'rule-save'), 'rule-save': id => { const r = { name: val('rl-name') || '未命名规则', desc: val('rl-desc'), who: val('rl-who'), on: true }; db.rules.push(r); save(); log('新增审核规则', r.name, '启用'); closeModal(); toast('已新增规则'); render() }, 'rule-edit': i => { const r = db.rules[Number(i)]; simpleForm('编辑规则', `
${field('rl-name', '规则名称', r.name)}${field('rl-desc', '规则说明', r.desc)}${field('rl-who', '执行方', r.who)}
`, 'rule-save2', String(i)) }, 'rule-save2': i => { const r = db.rules[Number(i)]; r.name = val('rl-name'); r.desc = val('rl-desc'); r.who = val('rl-who'); save(); log('编辑审核规则', r.name, '已更新'); closeModal(); toast('已保存规则'); render() }, 'rule-toggle': i => { const r = db.rules[Number(i)]; r.on = !r.on; save(); log('调整审核规则', r.name, r.on ? '启用' : '停用'); toast('已' + (r.on ? '启用' : '停用') + ':' + r.name); render() }, 'channel-new': () => simpleForm('新增渠道', `
${field('ch-name', '渠道名称', '')}${field('ch-type', '渠道类型', '社交')}${field('ch-note', '说明', '')}

新增渠道后可在活动分享配置中勾选;集赞仍以截图人工核对为准。

`, 'channel-save'), 'channel-save': id => { const key = 'ch' + String(Date.now()).slice(-4); db.channels.push({ key, cn: val('ch-name') || '新渠道', type: val('ch-type'), collect: '支持', share: '支持', reward: '支持', status: '启用', note: val('ch-note') }); save(); log('新增渠道', val('ch-name'), '启用'); closeModal(); toast('已新增渠道(演示)'); render() }, 'channel-edit': k => { const c = db.channels.find(x => x.key === String(k)); simpleForm('编辑渠道', `
${field('ch-name', '渠道名称', c.cn)}${field('ch-type', '渠道类型', c.type)}${field('ch-note', '说明', c.note)}
`, 'channel-save2', c.key) }, 'channel-save2': k => { const c = db.channels.find(x => x.key === String(k)); c.cn = val('ch-name'); c.type = val('ch-type'); c.note = val('ch-note'); save(); log('编辑渠道', c.cn, '已更新'); closeModal(); toast('已保存渠道'); render() }, 'channel-toggle': k => { const c = db.channels.find(x => x.key === String(k)); c.status = c.status === '启用' ? '停用' : '启用'; save(); log('调整渠道状态', c.cn, c.status); toast('已' + c.status + ':' + c.cn); render() }, 'msg-new': () => simpleForm('新增通知模板', `
${field('ms-name', '模板名称', '')}${field('ms-tpl', '模板内容', '')}${field('ms-ch', '触达渠道', '小程序订阅消息')}
`, 'msg-save'), 'msg-save': id => { const m = { name: val('ms-name') || '未命名模板', tpl: val('ms-tpl'), ch: val('ms-ch'), status: '启用' }; db.messages.push(m); save(); log('新增通知模板', m.name, '启用'); closeModal(); toast('已新增模板'); render() }, 'msg-edit': i => { const m = db.messages[Number(i)]; simpleForm('编辑通知模板', `
${field('ms-name', '模板名称', m.name)}${field('ms-tpl', '模板内容', m.tpl)}${field('ms-ch', '触达渠道', m.ch)}
`, 'msg-save2', String(i)) }, 'msg-save2': i => { const m = db.messages[Number(i)]; m.name = val('ms-name'); m.tpl = val('ms-tpl'); m.ch = val('ms-ch'); save(); log('编辑通知模板', m.name, '已更新'); closeModal(); toast('已保存模板'); render() }, 'msg-toggle': i => { const m = db.messages[Number(i)]; m.status = m.status === '启用' ? '停用' : '启用'; save(); log('调整通知模板', m.name, m.status); toast('已' + m.status + ':' + m.name); render() }, 'account-new': () => simpleForm('新增账号', `
${field('ac-name', '账号名称', '')}${field('ac-scope', '数据范围', '全部活动')}

正式系统需要服务端鉴权与审批流,本原型仅演示权限差异。

`, 'account-save'), 'account-save': id => { const a = { id: 'U' + String(Date.now()).slice(-3), name: val('ac-name') || '新账号', role: val('ac-role'), scope: val('ac-scope'), status: '启用' }; db.accounts.push(a); save(); log('新增后台账号', a.name, a.role); closeModal(); toast('已新增账号'); render() }, 'account-edit': id => { const a = db.accounts.find(x => x.id === String(id)); simpleForm('配置授权', `
${field('ac-name', '账号名称', a.name)}${field('ac-scope', '数据范围', a.scope)}
`, 'account-save2', a.id) }, 'account-save2': id => { const a = db.accounts.find(x => x.id === String(id)); a.name = val('ac-name'); a.role = val('ac-role'); a.scope = val('ac-scope'); save(); log('配置账号授权', a.id, `${a.name} · ${a.role}`); closeModal(); toast('已保存授权'); render() }, 'account-toggle': id => { const a = db.accounts.find(x => x.id === String(id)); a.status = a.status === '启用' ? '停用' : '启用'; save(); log('调整账号状态', a.id, a.status); toast('已' + a.status + ':' + a.name); render() }, 'member-detail': id => { const m = db.members.find(x => x.id === String(id)); if (!m) return; openModal('会员详情 · ' + m.name, `
${esc(m.name)}${tag(m.level)}${m.id}
${detail('手机号', esc(m.phone))}${detail('宠物数量', m.pets + ' 只')}${detail('注册时间', m.join)}${detail('参与活动', m.acts + ' 场')}${detail('集赞提交', m.likes + ' 次')}${detail('获得奖励', m.rewards + ' 项')}${detail('累计消费', '¥ ' + money(m.spend))}

集赞提交记录

${table(['审核单', '活动', '渠道', '状态'], db.likes.filter(l => l.user.includes(m.name)).map(l => [l.id, esc(actName(l.act)), platOf(l.plat).n, tag(l.status)]))}

获得奖励

${table(['奖励', '来源', '状态'], db.grants.filter(g => g.user.includes(m.name)).map(g => [esc(rewardName(g.reward)), esc(g.from), tag(g.status)]))}`, btn('关闭', 'close')); }, 'segment-new': () => simpleForm('新建人群包', `
${field('sg-name', '人群包名称', '')}${field('sg-cond', '圈选条件', '')}${field('sg-used', '关联活动 / 用途', '')}
`, 'segment-save'), 'segment-save': id => { const s = { name: val('sg-name') || '新人群包', cond: val('sg-cond'), size: 0, used: val('sg-used'), status: '启用' }; db.segments.push(s); save(); log('新建人群包', s.name, '启用'); closeModal(); toast('已新建人群包(人数将在下次计算后更新)'); render() }, 'segment-edit': name => { const s = db.segments.find(x => x.name === String(name)); if (!s) return; simpleForm('编辑人群包', `
${field('sg-name', '人群包名称', s.name)}${field('sg-cond', '圈选条件', s.cond)}${field('sg-used', '关联活动 / 用途', s.used)}
`, 'segment-save2', s.name) }, 'segment-save2': name => { const s = db.segments.find(x => x.name === String(name)); if (!s) return; s.name = val('sg-name'); s.cond = val('sg-cond'); s.used = val('sg-used'); save(); log('编辑人群包', s.name, '已更新'); closeModal(); toast('已保存人群包'); render() }, 'segment-toggle': name => { const s = db.segments.find(x => x.name === String(name)); s.status = s.status === '启用' ? '停用' : '启用'; save(); log('调整人群包状态', s.name, s.status); toast('已' + s.status + ':' + s.name); render() }, 'post-detail': id => postModal(String(id)), 'post-approve': id => { const p = db.posts.find(x => x.id === String(id)); p.status = '已通过'; grant(p.act, p.user, 'RW07', '投稿征集 · 审核通过', '自动发放', '已发放'); save(); log('通过投稿', p.id, `${p.title} → ¥15 通用券`); closeModal(); toast('投稿已通过,¥15 通用券已发放'); render() }, 'post-star': id => { const p = db.posts.find(x => x.id === String(id)); p.status = '已精选'; p.star = true; grant(p.act, p.user, 'RW07', '投稿征集 · 审核通过', '自动发放', '已发放'); grant(p.act, p.user, 'RW01', '投稿征集 · 精选奖励', '人工发放', '已发放'); save(); log('精选投稿', p.id, `${p.title} → ¥50 通用券 + 社区置顶`); closeModal(); toast('已精选并发放 ¥50 通用券'); render() }, 'post-reject': id => simpleModalReason('驳回投稿', 'post-reject-do', id), 'post-reject-do': id => { const p = db.posts.find(x => x.id === String(id)); p.status = '已驳回'; p.reason = val('reason-text') || '内容不符合投稿征集规则。'; save(); log('驳回投稿', p.id, p.reason); closeModal(); render(); openModal('已驳回投稿', `

驳回原因:${esc(p.reason)}

结果已回传用户端,用户可修改后重新提交。

`, btn('知道了', 'close', '', 'primary')) }, 'post-reopen': id => { const p = db.posts.find(x => x.id === String(id)); p.status = '待审核'; p.reason = ''; save(); log('允许重新提交投稿', p.id, '重置为待审核'); closeModal(); toast('已重置为待审核'); render() }, 'review-detail': id => reviewModal(String(id)), 'review-approve': id => { const r = db.reviews.find(x => x.id === String(id)); r.status = '已通过'; if (r.shots > 0) { r.reward = 'RW07'; grant(r.act, r.user, 'RW07', '订单评价 · 带图评价', '自动发放', '已发放') } save(); log('通过评价', r.id, r.shots > 0 ? '带图评价 → ¥15 通用券' : '无图评价通过(不发券)'); closeModal(); toast('评价已通过' + (r.shots > 0 ? ',¥15 通用券已发放' : '')); render() }, 'review-star': id => { const r = db.reviews.find(x => x.id === String(id)); r.status = '已精选'; r.star = true; if (r.shots > 0) { r.reward = 'RW01'; grant(r.act, r.user, 'RW07', '订单评价 · 带图评价', '自动发放', '已发放'); grant(r.act, r.user, 'RW01', '订单评价 · 精选评价', '人工发放', '已发放') } else { r.reward = 'RW01'; grant(r.act, r.user, 'RW01', '订单评价 · 精选评价', '人工发放', '已发放') } save(); log('精选评价', r.id, '¥50 通用券已发放'); closeModal(); toast('已精选并发放 ¥50 通用券'); render() }, 'review-reject': id => simpleModalReason('驳回评价', 'review-reject-do', id), 'review-reject-do': id => { const r = db.reviews.find(x => x.id === String(id)); r.status = '已驳回'; r.reason = val('reason-text') || '评价内容不符合社区规范。'; save(); log('驳回评价', r.id, r.reason); closeModal(); render(); openModal('已驳回评价', `

驳回原因:${esc(r.reason)}

`, btn('知道了', 'close', '', 'primary')) }, 'merchant-simulate': () => { const names = [['汪汪食堂', '宠物食品', '鲜食与烘焙,可现场试吃'], ['尾巴写真馆', '宠物摄影', '户外写真套餐与现场快拍'], ['萌爪乐园', '宠物服务', '宠物游乐区与免费体验']] const pick = names[Math.floor(Math.random() * names.length)]; const freeBooths = db.booths.filter(b => !b.sold && !b.reserved); const m = { id: 'BZ' + String(Date.now()).slice(-6), brand: pick[0], cat: pick[1], contact: '演示商家', phone: '138****' + String(Math.floor(1000 + Math.random() * 8999)), wechat: 'wx_' + String(Date.now()).slice(-5), intro: pick[2], typeName: '标准展位 · 3m×3m', boothIntent: freeBooths.length ? freeBooths[Math.floor(Math.random() * freeBooths.length)].no : '', booth: '', fee: 1200, status: '待审核', at: nowStr(), license: 1, cert: Math.random() > 0.5 ? 1 : 0, reason: '' }; db.merchants.unshift(m); save(); log('小程序提交参展报名', m.id, `${m.brand} · ${m.cat} · 意向展位 ${m.boothIntent || '未填写'}`); toast('已模拟小程序端提交:' + m.brand); render(); }, 'merchant-detail': id => merchantModal(String(id)), 'merchant-approve': id => { const m = db.merchants.find(x => x.id === String(id)); const no = val('booth-pick') || m.booth; if (m.booth && m.booth !== no) { const old = db.booths.find(b => b.no === m.booth); if (old) { old.sold = false; old.brand = '' } } m.status = '已通过'; m.booth = no; m.reason = ''; const b = db.booths.find(x => x.no === no); if (b) { b.sold = true; b.brand = m.brand } save(); log('审核通过商家参展', m.id, `${m.brand} → 展位 ${no || '未分配'}`); closeModal(); toast('已通过招商审核' + (no ? ',展位 ' + no : '')); render() }, 'merchant-reject': id => simpleModalReason('驳回参展申请', 'merchant-reject-do', id), 'merchant-reject-do': id => { const m = db.merchants.find(x => x.id === String(id)); m.status = '已驳回'; m.reason = val('reason-text') || '资质或经营范围不符合本届集市招商范围。'; save(); log('驳回参展申请', m.id, m.reason); closeModal(); render(); openModal('已驳回参展申请', `

驳回原因:${esc(m.reason)}

结果已回传商家端「我的参展申请」,商家可修改后重新提交。

`, btn('知道了', 'close', '', 'primary')) }, 'merchant-reopen': id => { const m = db.merchants.find(x => x.id === String(id)); m.status = '待审核'; m.reason = ''; save(); log('允许商家重新提交', m.id, '重置为待审核'); closeModal(); toast('已重置为待审核'); render() }, 'booth-new': () => simpleForm('新增展位', `
${field('bt-no', '展位号', 'A')}${field('bt-type', '展位类型', '标准展位')}${field('bt-price', '展位费(元)', 1200, 'number')}
`, 'booth-save'), 'booth-save': id => { const b = { no: val('bt-no') || ('A' + String(db.booths.length + 1).padStart(2, '0')), type: val('bt-type'), price: num('bt-price', 0), sold: false, brand: '' }; db.booths.push(b); save(); log('新增展位', b.no, b.type); closeModal(); toast('已新增展位'); render() }, 'code-new': () => simpleForm('批量生成券码', `
${field('cd-batch', '批次号', 'B' + String(Date.now()).slice(-5))}${field('cd-count', '生成数量', 100, 'number')}${field('cd-valid', '有效期至', '2026-10-31')}

券码用于小程序端「券码兑换」与活动发放;导出后可导入第三方发券系统。

`, 'code-save'), 'code-save': id => { const n = Math.min(50, num('cd-count', 10)); const batch = val('cd-batch'), cid = val('cd-coupon'), valid = val('cd-valid'); for (let i = 0; i < n; i++) db.codes.unshift({ code: 'YH' + batch.slice(-4) + String(i + 1).padStart(4, '0'), coupon: cid, batch, status: '未使用', owner: '', to: '活动发放', at: nowStr(), valid }); save(); log('批量生成券码', batch, `${n} 张 · ${cid}`); closeModal(); toast(`已生成 ${n} 张券码(演示)`); page = 'codes' in pages ? 'codes' : 'rewards'; render(); }, 'code-freeze': c => { const x = db.codes.find(y => y.code === String(c)); x.status = x.status === '已冻结' ? '未使用' : '已冻结'; save(); log('调整券码状态', x.code, x.status); toast('券码已' + x.status); render() }, 'coupon-new': () => simpleForm('新建券模板', `
${field('cp-name', '券名称', '')}${field('cp-discount', '抵扣金额(元)', 20, 'number')}${field('cp-min', '使用门槛(满多少元)', 88, 'number')}${field('cp-scope', '适用范围', '全部活动')}${field('cp-count', '发放总量', 1000, 'number')}${field('cp-expiry', '有效期至', '2026-10-31')}
`, 'coupon-save'), 'coupon-save': id => { const c = { id: 'YH' + String(Date.now()).slice(-4), name: val('cp-name') || '新券模板', discount: num('cp-discount'), minimum: num('cp-min'), scope: val('cp-scope'), count: num('cp-count', 100), used: 0, expiry: val('cp-expiry'), status: '启用' }; db.coupons.push(c); save(); log('新建券模板', c.id, c.name); closeModal(); toast('已新建券模板'); render() }, 'coupon-toggle': id => { const c = db.coupons.find(x => x.id === String(id)); c.status = c.status === '启用' ? '停用' : '启用'; save(); log('调整券模板状态', c.id, c.status); toast('已' + c.status + ':' + c.name); render() } }); function simpleModalReason(title, action, id) { openModal(title, ``, btn('取消', 'close') + btn('确认驳回', action, id, 'primary')) } /* ============================ 券码 / 券模板页面(并入奖励中心) ============================ */ pages.codes = () => { const rows = db.codes.filter(c => matches({ ...c, cn: (db.coupons.find(x => x.id === c.coupon) || {}).name || c.coupon })); return heading('券模板与券码', '券模板定义抵扣规则,券码是实际发放与核销的载体;活动奖励中的优惠券默认引用券模板。', btn(icon('plus') + '批量生成券码', 'code-new', '', 'primary') + btn(icon('plus') + '新建券模板', 'coupon-new', '')) + `
${stat('券模板', db.coupons.length, '抵扣规则配置', 'ticket')}${stat('已发放券码', int(db.codes.length + db.coupons.reduce((s, c) => s + c.used, 0)), '含历史发放', 'check')}${stat('未使用券码', db.codes.filter(c => c.status === '未使用').length, '本页演示批次', 'box')}${stat('已冻结', db.codes.filter(c => c.status === '已冻结').length, '退款或风控冻结', 'alert')}
` + `

券模板

${table(['券名称', '抵扣规则', '适用范围', '发放总量', '已使用', '有效期', '状态', '操作'], db.coupons.map(c => [esc(c.name), c.rate ? `${(c.rate * 10).toFixed(1)} 折` : `满 ¥${c.minimum} 减 ¥${c.discount}`, esc(c.scope), int(c.count), int(c.used), c.expiry, tag(c.status), link(c.status === '启用' ? '停用' : '启用', 'coupon-toggle', c.id)]))}

券码 / 活动凭证

活动报名凭证与优惠券码统一管理
${filters('搜索券码、批次号或持有人')}${table(['券码', '券模板 / 用途', '批次', '持有人', '状态', '有效期', '操作'], slice(rows).map(c => [`${esc(c.code)}`, esc((db.coupons.find(x => x.id === c.coupon) || {}).name || c.coupon), esc(c.batch), esc(c.owner || '—'), tag(c.status), esc(c.valid), link(c.status === '已冻结' ? '解冻' : '冻结', 'code-freeze', c.code)]))}${foot(rows.length)}
`; }; menu.splice(menu.findIndex(m => m[0] === 'rewardsoff'), 0); menu.splice(menu.findIndex(m => m[0] === 'grants') + 1, 0, ['codes', '券模板与券码', 'key', '奖励中心']); ['管理员', '活动运营', '财务'].forEach(r => { if (!permissions[r].includes('codes')) permissions[r].push('codes') }); /* app-ops.js 加载完成后重新渲染一次,确保新增页面可用 */ render(); 'use strict'; /* ============================================================================ 小布在家 · 活动运营中心 —— 模块三(需求文档闭环补齐) 补齐清单: 1. 组队活动(线上组队 / 线下组队) —— 需求:线上会员端 8、线下反扑线上 5 2. 限时领券活动 —— 需求:线上会员端 2 3. 联名卡包(多品牌 / 跨平台跳转) —— 需求:线上会员端 4 4. 外部渠道券码导入与兑换核销 —— 需求:线上会员端 1 5. 生日会与推送(结合宠物档案) —— 需求:线下反扑线上 2 6. 线下推广钩子(1 元领 / 填档案抽奖) —— 需求:线下反扑线上 4 7. 需求覆盖对照(自检页) —— 15 条需求 ↔ 后台实现位置 加载顺序:app.js → app-ops.js → app-ops2.js ========================================================================== */ /* --------------------------- 新增种子数据 --------------------------- */ const seedOps2 = () => ({ teams: [ { id: 'TD26092001', act: 'ACT260901', leader: '陈女士 · 豆包', need: 3, joined: 3, status: '已成团', at: '2026-09-18 10:12', done: '2026-09-18 16:40', expire: '', pay: '已支付', benefit: '3 人成团 · 每人 ¥79(省 ¥20)', members: ['陈女士 · 豆包', '李女士 · 小满', '周女士 · 布丁'] }, { id: 'TD26092002', act: 'ACT260901', leader: '林先生 · 年糕', need: 3, joined: 2, status: '组队中', at: '2026-09-21 09:20', done: '', expire: '2026-09-23 09:20', pay: '已支付', benefit: '3 人成团 · 每人 ¥79(省 ¥20)', members: ['林先生 · 年糕', '许女士 · 棉花'] }, { id: 'TD26092003', act: 'ACT260902', leader: '王先生 · Lucky', need: 5, joined: 5, status: '已成团', at: '2026-09-19 20:02', done: '2026-09-20 11:30', expire: '', pay: '免费参与', benefit: '5 人成团 · 讲师答疑专场', members: ['王先生 · Lucky', '李女士 · 小满', '张女士 · 可乐', '赵女士 · 奶糖', '孙先生 · 布丁'] }, { id: 'TD26092004', act: 'ACT260905', leader: '张女士 · 可乐', need: 2, joined: 1, status: '已过期', at: '2026-09-17 12:40', done: '', expire: '2026-09-19 12:40', pay: '已自动退款(原路退回)', benefit: '2 人成团 · 洗护 8 折', members: ['张女士 · 可乐'] }, { id: 'TD26092005', act: 'ACT260903', leader: '赵女士 · 奶糖', need: 3, joined: 3, status: '已成团', at: '2026-09-19 08:10', done: '2026-09-19 14:00', expire: '', pay: '已支付', benefit: '3 人成团 · 每人 ¥49(线下见面会)', members: ['赵女士 · 奶糖', '周女士 · 布丁', '吴女士 · 米粒'] } ], coBrands: [ { id: 'CB01', brand: '毛孩超市', cat: '宠物食品零售', rights: '满 99 减 30 券 ×2 + 到店自提礼包', jump: '跳转合作方小程序', target: '毛孩超市小程序 · 会员中心', verify: '跨平台核销(回传)', cross: 213, status: '合作中' }, { id: 'CB02', brand: '爪爪咖啡', cat: '宠物友好餐饮', rights: '宠物友好座位券 ×1 + 饮品第二杯半价', jump: '跳转 H5 领券页', target: 'https://demo.zhua.zhua/coupon', verify: '门店扫码核销', cross: 96, status: '合作中' }, { id: 'CB03', brand: '毛球摄影', cat: '宠物写真服务', rights: '拍摄 8 折券 + 1 张精修底片', jump: '跳转公众号菜单', target: '毛球摄影公众号 · 预约', verify: '线下核销', cross: 41, status: '合作中' }, { id: 'CB04', brand: '安心宠物保险', cat: '宠物保险', rights: '首月保费抵扣 ¥30', jump: '跳转合作方 H5', target: 'https://demo.anxin.pet/quote', verify: '合作方回传投保单', cross: 18, status: '已暂停' } ], couponActs: [ { id: 'LQ26092001', name: '国庆出游 · 限时领券', coupon: 'YH004', start: '2026-09-28 10:00', end: '2026-10-08 23:59', total: 1000, perUser: 1, claimed: 642, used: 118, status: '启用' }, { id: 'LQ26092002', name: '秋日洗护季 · 限时领券', coupon: 'YH003', start: '2026-09-22 00:00', end: '2026-10-15 23:59', total: 2000, perUser: 2, claimed: 1120, used: 486, status: '启用' }, { id: 'LQ26092003', name: '新人见面礼 · 限时领券', coupon: 'YH001', start: '2026-09-01 00:00', end: '2026-09-20 23:59', total: 5000, perUser: 1, claimed: 1860, used: 1420, status: '已结束' } ], couponClaims: [ { id: 'LQC2609A001', act: 'LQ26092002', user: '陈女士 · 豆包', coupon: 'YH003', at: '2026-09-22 09:12', code: 'YH2609230112', status: '已使用', usedAt: '2026-09-22 15:40' }, { id: 'LQC2609A002', act: 'LQ26092002', user: '许女士 · 棉花', coupon: 'YH003', at: '2026-09-22 10:02', code: 'YH2609230113', status: '未使用', usedAt: '' }, { id: 'LQC2609A003', act: 'LQ26092001', user: '林先生 · 年糕', coupon: 'YH004', at: '2026-09-21 19:44', code: 'YH2609230114', status: '未使用', usedAt: '' }, { id: 'LQC2609A004', act: 'LQ26092003', user: '李女士 · 小满', coupon: 'YH001', at: '2026-09-12 11:20', code: 'YH2609230115', status: '已使用', usedAt: '2026-09-14 09:30' } ], hooks: [ { id: 'HG01', name: '到店 1 元领 · 上门喂遛体验券', type: '1 元领', action: '支付 ¥1 下单体验券', reward: '上门喂遛体验券(1 次)', lead: '手机号 + 宠物昵称', channel: '门店二维码 / 收银台立牌', joins: 1268, done: 986, status: '启用' }, { id: 'HG02', name: '填写宠物档案后抽奖', type: '填档案抽奖', action: '完整录入宠物档案(昵称 / 品种 / 生日)', reward: '抽奖 1 次(奖池:优惠券 / 零食礼包 / 谢谢参与)', lead: '宠物档案 + 手机号', channel: '门店扫码 / 线下活动摊位', joins: 842, done: 617, status: '启用' }, { id: 'HG03', name: '地推海报扫码领券', type: '扫码领券', action: '扫描地推海报二维码进入活动页并留资', reward: '¥20 新客券', lead: '手机号', channel: '地推海报 / 社区展位', joins: 2103, done: 1450, status: '停用' } ], hookJoins: [ { id: 'HGJ26092001', hook: 'HG01', user: '陈女士 · 豆包', from: '门店二维码', at: '2026-09-21 10:12', action: '已支付 ¥1', result: '体验券已入卡包' }, { id: 'HGJ26092002', hook: 'HG02', user: '周先生 · 汤圆', from: '门店扫码', at: '2026-09-21 11:04', action: '档案已完善', result: '中奖:宠物零食礼包(待发放)' }, { id: 'HGJ26092003', hook: 'HG02', user: '李女士 · 小满', from: '线下活动摊位', at: '2026-09-20 15:30', action: '档案已完善', result: '中奖:谢谢参与' }, { id: 'HGJ26092004', hook: 'HG03', user: '孙先生 · 布丁', from: '地推海报', at: '2026-09-19 17:22', action: '已留资', result: '¥20 新客券已发放' } ], codeSources: [ { id: 'SRC01', name: '毛孩超市(合作方)', type: '第三方合作', prefix: 'MHCS', coupon: 'YH001', count: 5000, redeemed: 1860, at: '2026-09-10', status: '启用' }, { id: 'SRC02', name: '抖音本地生活', type: '第三方平台', prefix: 'DY', coupon: 'YH002', count: 2000, redeemed: 412, at: '2026-09-15', status: '启用' }, { id: 'SRC03', name: '自营活动发券', type: '自营', prefix: 'YH', coupon: 'YH003', count: 2000, redeemed: 1120, at: '长期', status: '启用' } ], codeRedeems: [ { code: 'MHCS0000123', source: 'SRC01', user: '李女士 · 小满', at: '2026-09-20 14:12', coupon: 'YH001', result: '兑换成功' }, { code: 'DY0000451', source: 'SRC02', user: '王先生 · Lucky', at: '2026-09-20 09:40', coupon: 'YH002', result: '兑换成功' }, { code: 'MHCS0000124', source: 'SRC01', user: '(未注册用户)', at: '2026-09-19 20:02', coupon: 'YH001', result: '兑换失败 · 券码已使用' } ], birthdays: [ { pet: '豆包', owner: '陈女士 · 138****2601', birthday: '2023-10-06', days: 13, month: '10 月', status: '待推送' }, { pet: '年糕', owner: '林先生 · 139****0518', birthday: '2024-10-11', days: 18, month: '10 月', status: '待推送' }, { pet: '可乐', owner: '张女士 · 136****9136', birthday: '2022-10-21', days: 28, month: '10 月', status: '待推送' }, { pet: '煤球', owner: '陈先生 · 137****7712', birthday: '2023-09-24', days: 2, month: '9 月', status: '已推送', pushAt: '2026-09-21 10:00', joined: '已报名' }, { pet: '米粒', owner: '吴女士 · 139****1324', birthday: '2024-09-28', days: 6, month: '9 月', status: '已推送', pushAt: '2026-09-21 10:00', joined: '已报名' }, { pet: '布丁', owner: '周女士 · 130****7788', birthday: '2024-09-20', days: -2, month: '9 月', status: '已过期', pushAt: '2026-09-10 10:00', joined: '未报名' } ], birthdayPushes: [ { id: 'BD260901', month: '2026-09', target: 213, sent: 213, click: 96, joined: 58, at: '2026-09-01 10:00', status: '已完成' }, { id: 'BD260801', month: '2026-08', target: 186, sent: 186, click: 74, joined: 41, at: '2026-08-01 10:00', status: '已完成' }, { id: 'BD261001', month: '2026-10', target: 3, sent: 0, click: 0, joined: 0, at: '待执行', status: '待推送' } ] }); const _s2 = seedOps2(); Object.keys(_s2).forEach(k => { if (!db[k]) db[k] = _s2[k] }); if (!db.birthdayCfg) db.birthdayCfg = { on: true, advance: 7, pushDay: 1, sendTime: '10:00', channel: '小程序订阅消息 + 短信', autoCreate: true, activity: 'ACT260908', gift: 'RW05', grace: 3 }; /* 券码补充「来源渠道」示例(第三方合作 / 第三方平台) */ if (!db.codes.some(c => c.source)) { db.codes.unshift( { code: 'MHCS0000123', coupon: 'YH001', batch: 'MHCS2609', source: 'SRC01', status: '已使用', owner: '李女士 · 小满', to: '第三方券码兑换', at: '2026-09-20 14:12', valid: '2026-10-31' }, { code: 'MHCS0000124', coupon: 'YH001', batch: 'MHCS2609', source: 'SRC01', status: '未使用', owner: '', to: '第三方券码兑换', at: '2026-09-20 14:12', valid: '2026-10-31' }, { code: 'DY0000451', coupon: 'YH002', batch: 'DY2609', source: 'SRC02', status: '已使用', owner: '王先生 · Lucky', to: '第三方券码兑换', at: '2026-09-20 09:40', valid: '2026-10-31' } ); save(); } /* 组队玩法演示:为三场活动开启组队(线上 + 线下) */ if (!db.activities.some(a => (a.team || {}).on)) { const teamCfg = { ACT260901: { size: 3, hours: 48, benefit: '3 人成团 · 每人 ¥79(省 ¥20)', pay: '报名时支付', teamPrice: 79, inviteeCount: '不计入' }, ACT260902: { size: 5, hours: 72, benefit: '5 人成团 · 讲师答疑专场', pay: '免费参与', teamPrice: 0, inviteeCount: '不计入' }, ACT260903: { size: 3, hours: 24, benefit: '3 人成团 · 每人 ¥49(线下见面会)', pay: '报名时支付', teamPrice: 49, inviteeCount: '不计入' } }; db.activities.forEach(a => { const c = teamCfg[a.id]; if (!c) return; a.team = Object.assign({ on: true, fail: '有效期内未成团,系统自动失效并按原支付路径全额退回' }, c); if (!(a.plays || []).includes('组队')) a.plays.push('组队'); }); save(); } /* 人群限制演示:新客专享活动 + 指定人群包活动 */ if (!db.activities.some(a => a.crowd && a.crowd !== '不限')) { const a1 = db.activities.find(x => x.id === 'ACT260902'); if (a1) { a1.crowd = '指定人群'; a1.crowds = ['新客(注册 30 天内)'] } const a2 = db.activities.find(x => x.id === 'ACT260910'); if (a2) { a2.crowd = '指定人群包'; a2.crowdSegs = ['新客(注册 30 天内)'] } save(); } /* 人群限制迁移:移除「仅新客」选项 → 统一迁移到「新客(注册 30 天内)」人群包 */ db.activities.forEach(a => { if ((a.crowds || []).includes('仅新客')) { a.crowds = a.crowds.map(x => x === '仅新客' ? '新客(注册 30 天内)' : x); if (a.crowd === '仅新客') a.crowd = '指定人群'; save() } }); /* 卡包类型:联名 / 自有 */ db.packs.forEach(p => { if (/联名/.test(p.name)) { p.type = '联名'; p.brand = p.brand || '毛孩超市' } else { p.type = p.type || '自有' } }); const srcName = id => ((db.codeSources || []).find(s => s.id === id) || {}).name || '自营'; const couponName = id => ((db.coupons || []).find(c => c.id === id) || {}).name || '—'; /* ============================ 1. 玩法:组队活动 ============================ */ pages.playteam = () => { const on = db.activities.filter(a => (a.team || {}).on); const t = db.teams; const running = t.filter(x => x.status === '组队中'); const failed = t.filter(x => ['已过期', '已流团', '已解散'].includes(x.status)); return heading('组队活动 · 成团享权益', '线上与线下活动均可开启组队(拼团):用户发起或加入队伍,凑齐指定人数即享套餐优惠;有效期内未成团则自动失效并按原支付路径全额退回。', btn(icon('star') + '在活动中开启组队', 'goto-wizard', '', 'primary')) + `
${stat('开启组队的活动', on.length, '线上 + 线下均可开启', 'users')}${stat('进行中队伍', running.length, '等待更多成员加入', 'clock')}${stat('已成团', t.filter(x => x.status === '已成团').length, '成团后锁定权益', 'check')}${stat('未成团 / 已失效', failed.length, '系统自动退回,不扣手续费', 'alert')}
` + notice('成团判定:仅统计队伍中的独立参团用户(各自开团 / 参团并支付),达到该活动配置的成团人数即视为成团;同一订单里的代报名(帮家人朋友一起报名)不计入成团人数,也不享受拼团价,按原价结算。拼团价在开团 / 参团时即按团价收取,支付成功才锁定拼团名额;人满系统自动成团并发放参团凭证(核销码)。限时未成团由系统自动全额原路退款,无需人工申请。报名页单独报名 / 代报名均按原价,不做人数凑单判定。') + `

按活动配置组队

成团人数 / 有效期 / 成团权益 / 支付方式逐活动设置
${table(['活动', '场景', '成团人数', '拼团价 / 原价', '代报名口径', '有效期', '成团权益', '支付方式', '操作'], on.map(a => { const tm = a.team; return [`${esc(a.title)}${a.id}`, `${a.scene} / ${a.charge}`, `${tm.size} 人`, `${tm.teamPrice ? '¥ ' + money(tm.teamPrice) : '未设置'}原价 ¥ ${money(a.price)}`, tag(tm.inviteeCount === '计入' ? '计入成团' : '不计入成团'), `${tm.hours} 小时内成团`, esc(tm.benefit), esc(tm.pay), `${link('编辑配置', 'team-cfg-edit', a.id)}${link('查看队伍', 'team-filter-act', a.id)}`] }))}
` + `

组队记录

${t.length} 支队伍 · 可手动成团 / 延时 / 解散
${table(['队伍', '活动', '队长', '成团进度', '支付', '时间', '状态', '操作'], t.map(x => [`${x.id}`, `${esc(actName(x.act))}${esc(x.act)}`, esc(x.leader), `${x.joined} / ${x.need} 人
`, tag(x.pay), `${esc(x.at)}${x.done ? `成团 ${esc(x.done)}` : x.expire ? `截止 ${esc(x.expire)}` : ''}`, tag(x.status), link(x.status === '组队中' ? '管理' : '查看', 'team-detail', x.id)]))}
`; }; function teamCfgModal(id) { const a = act(String(id)); if (!a) return; const t = a.team || { on: true, size: 3, hours: 48, benefit: '成团享套餐优惠', pay: '报名时支付' }; openModal('组队配置 · ' + a.title, `

对应活动发布向导第 3 步「玩法与奖励」。保存后立即同步用户端活动详情页的「发起组队 / 加入队伍」入口。

${notice('支付口径:开团 / 参团时即按拼团价支付,支付成功才锁定名额并进入邀请环节,人满自动成团;代报名(同一订单帮家人朋友一起报名)不计入成团人数,也不享受拼团价,按原价结算。')}
`, btn('取消', 'close') + btn('保存配置', 'team-cfg-save', id, 'primary')); } function teamModal(id) { const t = db.teams.find(x => x.id === String(id)); if (!t) return; const a = act(t.act) || {}; openModal('组队详情 · ' + t.id, `
${esc(t.leader)}${tag(t.status)}${t.id}
${detail('活动', esc(a.title || t.act))}${detail('成团人数', t.need + ' 人')}${detail('当前人数', t.joined + ' 人(仅独立参团用户)')}${detail('拼团价', (a.team || {}).teamPrice ? '¥ ' + money(a.team.teamPrice) + ' / 人' : '按成团权益券抵扣')}${detail('代报名口径', esc((a.team || {}).inviteeCount || '不计入') + '成团人数')}${detail('支付方式', esc(t.pay))}${detail('成团权益', esc(t.benefit))}${detail('创建时间', esc(t.at))}${detail('成团时间 / 截止', esc(t.done || t.expire || '—'))}

队伍成员

${table(['成员', '角色', '加入时间'], t.members.map((m, i) => [esc(m), i === 0 ? tag('队长') : tag('队员'), esc(t.at)]))} ${t.joined < t.need ? notice(`还差 ${t.need - t.joined} 人成团。可延长有效期、手动成团(线下现场补位)或解散并全额退回。`, true) : notice('队伍已成团,权益已锁定;线下活动凭组队结果到场核销。')}`, t.status === '组队中' ? btn('解散并退款', 'team-dismiss', t.id, 'danger') + btn('延长 24 小时', 'team-extend', t.id) + btn('手动成团', 'team-force', t.id, 'primary') : btn('关闭', 'close')); } /* ============================ 2. 玩法:限时领券 ============================ */ pages.playcoupon = () => heading('限时领券 · 活动配置', '限定起止时间的领券活动:用户在活动页直接领取指定优惠券,领完即止。券模板与券码在「奖励中心 → 券模板与券码」统一维护,本页只做活动规则与发放控制。', btn(icon('plus') + '新建领券活动', 'cpnact-new', '', 'primary') + btn(icon('key') + '券模板与券码', 'goto-codes', '')) + `
${stat('领券活动', db.couponActs.length, '含进行中与已结束', 'ticket')}${stat('进行中', db.couponActs.filter(x => x.status === '启用').length, '到点自动上下架', 'check')}${stat('累计领取', int(db.couponActs.reduce((s, x) => s + x.claimed, 0)), '按券码实际发放统计', 'users')}${stat('已核销使用', int(db.couponActs.reduce((s, x) => s + x.used, 0)), '下单抵扣后计入', 'wallet')}
` + notice('领取控制:发放总量决定券码上限,每人限领决定同一用户可领张数;达到上限后活动页显示「已领完」。券码由系统按券模板实时生成并写入用户「我的卡包」,使用情况回写本页。') + `

领券活动

起止时间精确到分钟,过期自动下架
${table(['领券活动', '指定券模板', '活动周期', '发放总量', '每人限领', '领取进度', '已使用', '状态', '操作'], db.couponActs.map(x => [`${esc(x.name)}${x.id}`, tag(couponName(x.coupon)) + `${esc(x.coupon)}`, `${esc(x.start)}至 ${esc(x.end)}`, int(x.total), x.perUser + ' 张', `${int(x.claimed)} / ${int(x.total)}
`, int(x.used), tag(x.status), `${link('编辑', 'cpnact-edit', x.id)}${link(x.status === '启用' ? '停用' : '启用', 'cpnact-toggle', x.id)}`]))}

领取记录

含券码、领取时间与使用时间,可追溯
${table(['记录', '领券活动', '用户', '券模板', '券码', '领取时间', '使用时间', '状态'], db.couponClaims.map(c => [`${c.id}`, esc(((db.couponActs.find(a => a.id === c.act) || {}).name) || c.act), esc(c.user), tag(couponName(c.coupon)), `${esc(c.code)}`, esc(c.at), esc(c.usedAt || '—'), tag(c.status)]))}
`; /* ============================ 3. 玩法:推广钩子 ============================ */ pages.playhook = () => heading('推广钩子 · 线下引流转线上', '线下场景(门店 / 地推 / 摊位)投放钩子,引导用户进入小程序完成指定动作后领取奖励;钩子与奖池、留资字段、投放渠道逐项配置。', btn(icon('plus') + '新建钩子', 'hook-new', '', 'primary') + btn(icon('target') + '任务与抽奖', 'goto-playtask', '')) + `
${stat('钩子数量', db.hooks.length, '1 元领 / 填档案抽奖 / 扫码领券', 'target')}${stat('启用中', db.hooks.filter(h => h.status === '启用').length, '停用后入口不可见', 'check')}${stat('累计参与', int(db.hooks.reduce((s, h) => s + h.joins, 0)), '扫码进入并完成动作', 'users')}${stat('完成动作', int(db.hooks.reduce((s, h) => s + h.done, 0)), `完成率 ${(db.hooks.reduce((s, h) => s + h.done, 0) / Math.max(1, db.hooks.reduce((s, h) => s + h.joins, 0)) * 100).toFixed(1)}%`, 'chart')}
` + notice('完成动作的判定:1 元领以支付成功为准;填档案抽奖以宠物档案必填项(昵称 / 品种 / 生日)完整为准;扫码领券以留资成功为准。抽奖奖池与概率在「玩法配置 → 任务与抽奖」中统一配置,中奖实物类在「发奖记录」人工发放。') + `

钩子配置

钩子可关联具体活动,也可作为独立引流入口
${table(['钩子', '类型', '触发动作', '奖励 / 奖池', '需留资', '投放渠道', '参与 / 完成', '状态', '操作'], db.hooks.map(h => [`${esc(h.name)}${h.id}`, tag(h.type), esc(h.action), esc(h.reward), esc(h.lead), esc(h.channel), `${int(h.joins)} / ${int(h.done)}
`, tag(h.status), `${link('编辑', 'hook-edit', h.id)}${link(h.status === '启用' ? '停用' : '启用', 'hook-toggle', h.id)}`]))}

参与记录

来源渠道可用于评估线下点位效果
${table(['记录', '钩子', '用户', '来源渠道', '完成动作', '时间', '结果'], db.hookJoins.map(j => [`${j.id}`, esc(((db.hooks.find(h => h.id === j.hook) || {}).name) || j.hook), esc(j.user), esc(j.from), esc(j.action), esc(j.at), esc(j.result)]))}
`; /* ============================ 4. 玩法:卡包活动(含联名卡包) ============================ */ pages.playpack = () => heading('卡包活动 · 权益打包', '把多项权益打包一次性发放;领取条件分「无条件」与「有条件」,站内权益直接入卡包,站外权益可跳转合作方(联名卡包)。', btn(icon('plus') + '新建卡包', 'pack-new', '', 'primary') + btn(icon('link') + '新增联名品牌', 'brand-new', '')) + `
${stat('卡包总数', db.packs.length, `自有 ${db.packs.filter(p => p.type !== '联名').length} · 联名 ${db.packs.filter(p => p.type === '联名').length}`, 'ticket')}${stat('启用中', db.packs.filter(p => p.status === '启用').length, '停用后保留历史领取记录', 'check')}${stat('累计领取', int(db.packs.reduce((s, p) => s + p.claim, 0)), '含站内与站外权益', 'users')}${stat('联名合作品牌', db.coBrands.filter(b => b.status === '合作中').length, '跨平台跳转需配置跳转地址', 'link')}
` + notice('卡包发放口径:无条件卡包用户点击即领;有条件卡包需满足人群或档案条件(条件来源「用户与人群 → 人群包」)。联名卡包中含合作方权益时,用户点击权益将按配置跳转合作方小程序 / H5 / 公众号,核销结果由合作方回传。') + `

卡包列表

${table(['卡包', '类型', '权益数', '领取条件', '适用人群', '活动周期', '累计领取', '状态', '操作'], db.packs.map(p => [`${esc(p.name)}${p.id}`, p.type === '联名' ? tag('联名卡包') + `${esc(p.brand || '')}` : tag('自有卡包'), p.items + ' 项', p.need === '无条件领取' ? tag('无条件') : tag('有条件'), esc(p.crowd), esc(p.period), int(p.claim), tag(p.status), `${link('权益明细', 'pack-detail', p.id)}${link('编辑', 'pack-edit', p.id)}${link(p.status === '启用' ? '停用' : '启用', 'pack-toggle', p.id)}`]))}

联名卡包 · 合作品牌与跨平台跳转

合作方权益的跳转方式、地址与核销回传统一在此维护
${table(['合作品牌', '品类', '品牌权益', '跳转方式', '跳转地址 / 目标', '核销方式', '跨平台核销', '状态', '操作'], db.coBrands.map(b => [`${esc(b.brand)}${b.id}`, esc(b.cat), esc(b.rights), tag(b.jump), esc(b.target), tag(b.verify), int(b.cross) + ' 次', tag(b.status), `${link('编辑', 'brand-edit', b.id)}${link(b.status === '合作中' ? '暂停' : '恢复', 'brand-toggle', b.id)}`]))}
`; /* ============================ 5. 奖励中心:券模板与券码(含外部导入) ============================ */ pages.codes = () => { const rows = db.codes.filter(c => matches({ ...c, cn: couponName(c.coupon), sn: srcName(c.source) })); return heading('券模板与券码', '券模板定义抵扣规则;券码是实际发放与核销的载体,既可由平台活动发放,也可导入第三方合作渠道券码,用户在小程序「券码兑换」中输入后完成兑换。', btn(icon('upload') + '导入外部渠道券码', 'code-import', '', 'primary') + btn(icon('plus') + '批量生成券码', 'code-new') + btn(icon('plus') + '新建券模板', 'coupon-new')) + `
${stat('券模板', db.coupons.length, '抵扣规则配置', 'ticket')}${stat('外部合作渠道', db.codeSources.filter(s => s.type !== '自营').length, '第三方券码来源', 'link')}${stat('未使用券码', db.codes.filter(c => c.status === '未使用').length, '含第三方导入', 'box')}${stat('兑换核销', int(db.codeRedeems.filter(r => r.result === '兑换成功').length), '用户输入券码成功兑换', 'check')}
` + notice('外部券码导入:上传合作方发放的码表(或按前缀批量生成),系统按前缀校验归属;用户在小程序活动页输入券码 → 校验状态与有效期 → 兑换为对应券模板的优惠券并写入卡包。兑换记录永久留存,失败原因可追溯(已使用 / 已冻结 / 过期 / 码不存在)。') + `

券模板

${table(['券名称', '抵扣规则', '适用范围', '发放总量', '已使用', '有效期', '状态', '操作'], db.coupons.map(c => [esc(c.name), c.rate ? `${(c.rate * 10).toFixed(1)} 折` : `满 ¥${c.minimum} 减 ¥${c.discount}`, esc(c.scope), int(c.count), int(c.used), c.expiry, tag(c.status), link(c.status === '启用' ? '停用' : '启用', 'coupon-toggle', c.id)]))}

券码来源批次

自营发券与第三方合作渠道
${table(['批次 / 渠道', '来源类型', '码前缀', '对应券模板', '导入数量', '已兑换 / 核销', '导入时间', '状态'], db.codeSources.map(s => [`${esc(s.name)}${s.id}`, tag(s.type), `${esc(s.prefix)}`, tag(couponName(s.coupon)), int(s.count), `${int(s.redeemed)}
`, esc(s.at), tag(s.status)]))}

券码 / 活动凭证

活动报名凭证与优惠券码统一管理
${filters('搜索券码、批次号、来源渠道或持有人')}${table(['券码', '券模板 / 用途', '来源', '批次', '持有人', '状态', '有效期', '操作'], slice(rows).map(c => [`${esc(c.code)}`, esc(couponName(c.coupon) !== '—' ? couponName(c.coupon) : c.coupon), c.source ? tag(srcName(c.source)) : tag('自营'), esc(c.batch), esc(c.owner || '—'), tag(c.status), esc(c.valid), link(c.status === '已冻结' ? '解冻' : '冻结', 'code-freeze', c.code)]))}${foot(rows.length)}

券码兑换记录

用户在活动页输入券码后的核验结果
${table(['券码', '来源渠道', '兑换用户', '兑换券模板', '兑换时间', '结果'], db.codeRedeems.map(r => [`${esc(r.code)}`, esc(srcName(r.source)), esc(r.user), tag(couponName(r.coupon)), esc(r.at), /成功/.test(r.result) ? tag(r.result) : tag(r.result)]))}
`; }; /* ============================ 6. 用户与人群:生日会与推送 ============================ */ pages.birthday = () => { const c = db.birthdayCfg; const wait = db.birthdays.filter(b => b.status === '待推送'); return heading('生日会与推送', '依托小程序宠物档案识别宠物生日,按规则自动推送当月生日邀约并向用户开放报名;推送名单、执行记录与生日礼一并在此维护。', btn(icon('bell') + '立即执行本月推送', 'bd-push-now', '', 'primary') + btn(icon('activity') + '关联生日活动', 'goto-activities', '')) + `
${stat('规则状态', c.on ? '已开启' : '已关闭', `生日前 ${c.advance} 天开始推送`, 'settings')}${stat('待推送名单', wait.length, '档案生日在推送窗口内', 'clock')}${stat('本月已推送', db.birthdays.filter(b => b.status === '已推送').length, '点击与报名情况见执行记录', 'bell')}${stat('本月报名转化', db.birthdays.filter(b => b.joined === '已报名').length, '推送后完成报名', 'check')}
` + notice('推送口径:以宠物档案中「宠物生日」为准,生日前 ' + c.advance + ' 天进入推送窗口;每月 ' + c.pushDay + ' 日 ' + esc(c.sendTime) + ' 汇总推送。用户点击消息进入生日活动详情页完成报名,报名后自动发放生日礼。') + `

推送规则配置

对应「每月生日活动报名及推送」需求
${btn('保存规则', 'bd-save', '', 'primary')}

小程序订阅消息需用户授权;未授权时降级为站内消息中心,规则变更写入操作日志。

生日名单(推送窗口内)

来自宠物档案,可单条补推
${table(['宠物', '主人', '宠物生日', '距生日', '所属月份', '推送状态', '推送时间', '报名结果', '操作'], db.birthdays.map(b => [`${esc(b.pet)}`, esc(b.owner), esc(b.birthday), b.days >= 0 ? `还有 ${b.days} 天` : `已过 ${-b.days} 天`, esc(b.month), tag(b.status), esc(b.pushAt || '—'), b.joined ? tag(b.joined) : '—', b.status === '待推送' ? link('立即推送', 'bd-push-one', b.pet) : '—']))}

推送执行记录

按月汇总
${table(['批次', '月份', '目标人数', '已发送', '点击', '报名转化', '执行时间', '状态'], db.birthdayPushes.map(p => [`${p.id}`, esc(p.month), int(p.target), int(p.sent), int(p.click), int(p.joined), esc(p.at), tag(p.status)]))}
`; }; /* ============================ 7. 需求覆盖对照(自检页) ============================ */ const REQ_COVER = [ ['线上会员端 1', '第三方券码领取', '用户在活动页输入 / 导入外部合作渠道券码,核销领取对应优惠券权益', '奖励中心 → 券模板与券码:外部渠道导入批次 + 兑换核销记录', '本轮补齐'], ['线上会员端 2', '限时优惠券活动', '限定活动起止时间,用户在本页面直接领取指定优惠券', '玩法配置 → 限时领券:周期 / 券模板 / 总量 / 每人限领 / 领取记录', '本轮新增'], ['线上会员端 3', '卡包活动', '领取卡包,有条件或无条件,有人群', '玩法配置 → 卡包活动:领取条件 / 适用人群 / 权益明细 / 领取记录', '已有'], ['线上会员端 4', '线上联名卡包', '多合作品牌,可能存在跨平台跳转合作方使用', '玩法配置 → 卡包活动 · 联名卡包:合作品牌 / 跳转方式与地址 / 跨平台核销回传', '本轮新增'], ['线上会员端 5', '时间线消费满额得奖励', '指定活动周期内累计消费金额达标后领取奖励', '玩法配置 → 满额有礼:金额阶梯 / 统计口径 / 自动或人工发放', '已有'], ['线上会员端 6', '时间线购买某产品 N 次得奖励', '周期内购买指定服务 / 商品达到规定次数即领奖', '玩法配置 → 次数有礼:指定服务 / 次数阶梯 / 达标发放', '已有'], ['线上会员端 7', '人群限制的活动(如新客)', '对参与用户做身份人群过滤,仅符合条件的用户可参与', '发布活动向导 · 报名与资格 → 参与人群限制(新客 / 老客 / 指定人群包)+ 用户与人群 · 人群包', '本轮新增'], ['线上会员端 8', '组队类活动(类似拼团)', '用户发起或加入组队,凑齐指定人数后享活动套餐优惠', '玩法配置 → 组队活动:成团人数 / 有效期 / 成团权益 / 队伍管理', '本轮新增'], ['线上会员端 9', '集赞类活动', '分享活动内容至外部渠道集齐指定点赞数,回页面核验后领取奖励', '玩法配置 → 集赞有礼 + 集赞截图审核:门槛逐活动可配 / 渠道不限 / 人工核对 / 手动发奖', '已有'], ['线上会员端 10', '评价 & 内容互动活动', '投稿征集、晒单:上传宠物照片 + 文字评价,自动发券,要求可分享', '玩法配置 → 投稿与评价 + 内容与互动:投稿审核 / 评价管理 / 分享回流', '已有'], ['线下反扑线上 1', '线下活动线上报名入口', '线下活动提供线上报名通道,支持免费报名或线上支付报名费', '活动管理 → 发布活动 / 报名与订单 / 凭证核销', '已有'], ['线下反扑线上 2', '每月生日活动报名及推送', '依托宠物档案识别宠物生日,推送活动并在页面报名', '用户与人群 → 生日会与推送:档案生日识别 / 每月推送 / 名单与执行记录 / 生日礼', '本轮新增'], ['线下反扑线上 3', '大型集市活动(商家报名入口)', '线下集市开放商家 / 品牌报名提交资料,用于集市入驻申请', '集市招商 → 集市与展位 + 商家报名审核', '已有'], ['线下反扑线上 4', '线下推广钩子', '线下引流进线上页面,完成指定动作(1 元下单 / 录入宠物档案)后抽奖领奖', '玩法配置 → 推广钩子:钩子类型 / 触发动作 / 奖池 / 留资 / 参与记录', '本轮新增'], ['线下反扑线上 5', '线下活动组队玩法', '线下主题活动配套线上组队,凭组队结果参与线下活动并享权益', '玩法配置 → 组队活动(线下活动组队 + 到场凭证核销)', '本轮新增'] ]; pages.coverage = () => { const add = REQ_COVER.filter(r => r[4] !== '已有').length; return heading('需求覆盖对照', '《计划活动类型》15 条需求与后台模块的逐条对照,用于交付前自检。', btn(icon('download') + '导出对照表', 'export', 'coverage')) + `
${stat('需求条目', REQ_COVER.length, '线上会员端 10 + 线下反扑线上 5', 'list')}${stat('已闭环', REQ_COVER.length, '全部有对应后台配置与审核 / 发放闭环', 'check')}${stat('本轮补齐', add, '本轮新增或补强的条目', 'refresh')}${stat('客户已标注满足', 7, '文档中标注「满足」的 7 条同样纳入后台管理', 'star')}
` + notice('说明:文档中客户已标注「满足」的 7 条(第三方券码领取、限时优惠券、人群限制、组队、线下报名、生日会、线下组队)在移动端已实现;本后台为这 7 条补齐了运营配置、审核与数据闭环,避免「前端有、后台管不了」。') + `
${table(['需求来源', '需求名称', '需求内容', '后台实现位置', '闭环状态'], REQ_COVER.map(r => [`${esc(r[0])}`, esc(r[1]), `${esc(r[2])}`, `${esc(r[3])}`, r[4] === '已有' ? tag('已有闭环') : tag('本轮补齐')]))}
`; }; /* ============================ 模块三:actions ============================ */ Object.assign(actions, { 'goto-codes': () => navigate('codes'), 'goto-playtask': () => navigate('playtask'), 'goto-activities': () => navigate('activities'), /* 组队 */ 'team-cfg-edit': id => teamCfgModal(String(id)), 'team-cfg-save': id => { const a = act(String(id)); if (!a) return; a.team = { on: !!$('#tm-on').checked, autoRefund: !!$('#tm-autorefund').checked, size: Math.max(2, num('tm-size', 3)), hours: Math.max(1, num('tm-hours', 48)), benefit: val('tm-benefit'), pay: val('tm-pay'), fail: val('tm-fail'), teamPrice: num('tm-price', 0) || 0, inviteeCount: val('tm-invitee') || '不计入' }; if (a.team.on && !(a.plays || []).includes('组队')) a.plays.push('组队'); save(); log('修改活动组队配置', a.id, `${a.team.on ? '已开启' : '已关闭'} · ${a.team.size} 人成团 · 拼团价 ${a.team.teamPrice ? '¥' + a.team.teamPrice : '按权益券'} · 代报名${a.team.inviteeCount}成团人数 · ${a.team.hours} 小时有效期`); closeModal(); toast('组队配置已保存并同步用户端'); render(); }, 'team-detail': id => teamModal(String(id)), 'team-filter-act': id => { closeModal(); toast('已定位活动「' + actName(String(id)) + '」的组队记录(演示:列表中可按活动检索)') }, 'team-extend': id => { const t = db.teams.find(x => x.id === String(id)); t.expire = '2026-09-24 09:20(已延长 24 小时)'; save(); log('延长组队有效期', t.id, t.expire); closeModal(); toast('已延长 24 小时'); render() }, 'team-force': id => { const t = db.teams.find(x => x.id === String(id)); t.joined = t.need; t.status = '已成团'; t.done = nowStr(); t.expire = ''; save(); log('手动成团', t.id, `${t.need} 人(线下现场补位)`); closeModal(); toast('已手动成团'); render() }, 'team-dismiss': id => { const t = db.teams.find(x => x.id === String(id)); t.status = '已解散'; t.pay = '已全额退款'; save(); log('解散组队并退款', t.id, '按原支付路径全额退回'); closeModal(); toast('已解散并按原路全额退款'); render() }, /* 限时领券 */ 'cpnact-new': () => cpnActModal(null), 'cpnact-edit': id => cpnActModal(String(id)), 'cpnact-save': id => { const name = val('cpa-name'); if (!name) { toast('请填写领券活动名称'); return } const data = { name, coupon: val('cpa-coupon'), start: val('cpa-start'), end: val('cpa-end'), total: num('cpa-total', 100), perUser: num('cpa-per', 1) }; if (id) { const x = db.couponActs.find(a => a.id === String(id)); Object.assign(x, data); log('编辑领券活动', x.id, x.name) } else { const x = Object.assign({ id: 'LQ' + String(Date.now()).slice(-6), claimed: 0, used: 0, status: '启用' }, data); db.couponActs.unshift(x); log('新建领券活动', x.id, x.name) } save(); closeModal(); toast('已保存领券活动'); render(); }, 'cpnact-toggle': id => { const x = db.couponActs.find(a => a.id === String(id)); x.status = x.status === '启用' ? '停用' : '启用'; save(); log('调整领券活动状态', x.id, x.status); toast('已' + x.status + ':' + x.name); render() }, /* 推广钩子 */ 'hook-new': () => hookModal(null), 'hook-edit': id => hookModal(String(id)), 'hook-save': id => { const name = val('hk-name'); if (!name) { toast('请填写钩子名称'); return } const data = { name, type: val('hk-type'), action: val('hk-action'), reward: val('hk-reward'), lead: val('hk-lead'), channel: val('hk-channel') }; if (id) { const h = db.hooks.find(x => x.id === String(id)); Object.assign(h, data); log('编辑推广钩子', h.id, h.name) } else { const h = Object.assign({ id: 'HG' + String(Date.now()).slice(-4), joins: 0, done: 0, status: '启用' }, data); db.hooks.push(h); log('新建推广钩子', h.id, h.name) } save(); closeModal(); toast('已保存钩子配置'); render(); }, 'hook-toggle': id => { const h = db.hooks.find(x => x.id === String(id)); h.status = h.status === '启用' ? '停用' : '启用'; save(); log('调整钩子状态', h.id, h.status); toast('已' + h.status + ':' + h.name); render() }, /* 联名卡包 */ 'brand-new': () => brandModal(null), 'brand-edit': id => brandModal(String(id)), 'brand-save': id => { const brand = val('cb-brand'); if (!brand) { toast('请填写合作品牌名称'); return } const data = { brand, cat: val('cb-cat'), rights: val('cb-rights'), jump: val('cb-jump'), target: val('cb-target'), verify: val('cb-verify') }; if (id) { const b = db.coBrands.find(x => x.id === String(id)); Object.assign(b, data); log('编辑联名合作品牌', b.id, b.brand) } else { const b = Object.assign({ id: 'CB' + String(Date.now()).slice(-3), cross: 0, status: '合作中' }, data); db.coBrands.push(b); log('新增联名合作品牌', b.id, b.brand) } save(); closeModal(); toast('已保存联名品牌'); render(); }, 'brand-toggle': id => { const b = db.coBrands.find(x => x.id === String(id)); b.status = b.status === '合作中' ? '已暂停' : '合作中'; save(); log('调整联名品牌状态', b.id, b.status); toast('已' + b.status + ':' + b.brand); render() }, 'pack-detail': id => { const p = db.packs.find(x => x.id === String(id)); if (!p) return; const own = ['活动报名 9 折券 ×1', '¥20 通用券 ×1', '抽奖次数 ×1']; const rows = own.slice(0, Math.max(1, Math.min(p.items, 3))); const extra = p.type === '联名' ? [((db.coBrands.find(b => b.brand === p.brand) || {}).rights || '合作方权益') + '(合作方)'] : []; openModal('卡包权益明细 · ' + p.name, `
${tag(p.type === '联名' ? '联名卡包' : '自有卡包')}${tag(p.status)}${p.id}
${detail('领取条件', esc(p.need))}${detail('适用人群', esc(p.crowd))}${detail('活动周期', esc(p.period))}${detail('累计领取', int(p.claim))}${detail('合作品牌', esc(p.brand || '—'))}${detail('权益数量', p.items + ' 项')}

包含权益

${table(['权益', '归属', '发放方式', '使用说明'], rows.concat(extra).map((r, i) => [`${esc(r)}`, i < rows.length ? tag('站内') : tag('合作方'), i < rows.length ? '领取后自动入卡包' : '点击跳转合作方使用', i < rows.length ? '见券面规则' : '跨平台核销,结果回传'])).join('')}`, btn('关闭', 'close')); }, /* 外部券码导入 */ 'code-import': () => simpleForm('导入外部渠道券码', `
${field('im-name', '合作方 / 渠道名称', '')}${field('im-type', '来源类型', '第三方合作')}${field('im-prefix', '券码前缀', 'MHCS')}${field('im-count', '导入券码数量', 500, 'number')}

导入后按前缀生成 / 校验券码归属;用户在小程序「券码兑换」中输入券码即可兑换为对应券模板的优惠券,兑换记录可在本页追溯。

`, 'code-import-save'), 'code-import-save': id => { const name = val('im-name'); if (!name) { toast('请填写合作方 / 渠道名称'); return } const cid = val('im-coupon'), prefix = (val('im-prefix') || 'EXT').toUpperCase(), n = Math.min(50, num('im-count', 100)); const sid = 'SRC' + String(Date.now()).slice(-3); db.codeSources.push({ id: sid, name, type: val('im-type'), prefix, coupon: cid, count: num('im-count', 100), redeemed: 0, at: nowStr().slice(0, 10), status: '启用' }); for (let i = 0; i < n; i++) db.codes.unshift({ code: prefix + String(i + 1).padStart(6, '0'), coupon: cid, batch: prefix + '2609', source: sid, status: '未使用', owner: '', to: '第三方券码兑换', at: nowStr(), valid: '2026-10-31' }); save(); log('导入外部渠道券码', sid, `${name} · ${n} 张 · 前缀 ${prefix}`); closeModal(); toast(`已导入 ${n} 张外部券码(演示)`); page = 'codes'; render(); }, /* 生日会与推送 */ 'bd-save': () => { const c = db.birthdayCfg; c.on = val('bd-on') === '1'; c.advance = num('bd-advance', 7); c.pushDay = num('bd-day', 1); c.sendTime = val('bd-time'); c.channel = val('bd-channel'); c.gift = val('bd-gift'); c.activity = val('bd-act'); c.grace = num('bd-grace', 0); save(); log('修改生日推送规则', 'birthdayCfg', `提前 ${c.advance} 天 · 每月 ${c.pushDay} 日 ${c.sendTime} · ${c.on ? '已开启' : '已关闭'}`); toast('生日推送规则已保存'); render(); }, 'bd-push-now': () => { const wait = db.birthdays.filter(b => b.status === '待推送'); wait.forEach(b => { b.status = '已推送'; b.pushAt = nowStr() }); const p = db.birthdayPushes.find(x => x.status === '待推送'); if (p) { p.target = wait.length; p.sent = wait.length; p.at = nowStr(); p.status = '已完成' } save(); log('执行生日会推送', '生日名单', `本次推送 ${wait.length} 人 · 关联活动 ${actName(db.birthdayCfg.activity)}`); toast(`已推送 ${wait.length} 人(演示)`); render(); }, 'bd-push-one': pet => { const b = db.birthdays.find(x => x.pet === String(pet)); if (!b) return; b.status = '已推送'; b.pushAt = nowStr(); save(); log('单条补推生日邀约', b.pet, `主人 ${b.owner}`); toast('已向 ' + b.pet + ' 的主人推送邀约'); render(); } }); function cpnActModal(id) { const x = id ? db.couponActs.find(a => a.id === String(id)) : { name: '', coupon: (db.coupons[0] || {}).id, start: '2026-09-28 10:00', end: '2026-10-08 23:59', total: 1000, perUser: 1 }; openModal(id ? '编辑领券活动' : '新建领券活动', `
${field('cpa-name', '活动名称', x.name)} ${field('cpa-start', '开始时间', x.start)} ${field('cpa-end', '结束时间', x.end)} ${field('cpa-total', '发放总量', x.total, 'number')} ${field('cpa-per', '每人限领(张)', x.perUser, 'number')}

到点自动上下架;领取达到总量上限后活动页显示「已领完」。

`, btn('取消', 'close') + btn('保存', 'cpnact-save', id || '', 'primary')); } function hookModal(id) { const h = id ? db.hooks.find(x => x.id === String(id)) : { name: '', type: '1 元领', action: '', reward: '', lead: '手机号', channel: '' }; openModal(id ? '编辑推广钩子' : '新建推广钩子', `
${field('hk-name', '钩子名称', h.name)} ${field('hk-action', '触发动作(判定条件)', h.action)} ${field('hk-reward', '奖励 / 奖池', h.reward)} ${field('hk-lead', '需留资字段', h.lead)} ${field('hk-channel', '投放渠道', h.channel)}

「填档案抽奖」的奖池与概率在「玩法配置 → 任务与抽奖」中配置;实物类中奖在「发奖记录」人工发放。

`, btn('取消', 'close') + btn('保存', 'hook-save', id || '', 'primary')); } function brandModal(id) { const b = id ? db.coBrands.find(x => x.id === String(id)) : { brand: '', cat: '', rights: '', jump: '跳转合作方小程序', target: '', verify: '跨平台核销(回传)' }; openModal(id ? '编辑联名合作品牌' : '新增联名合作品牌', `
${field('cb-brand', '合作品牌名称', b.brand)} ${field('cb-cat', '品类', b.cat)} ${field('cb-rights', '品牌权益', b.rights)} ${field('cb-target', '跳转地址 / 目标', b.target)} ${field('cb-verify', '核销方式', b.verify)}

联名卡包中的合作方权益按此配置跳转;跨平台核销结果由合作方回传后计入统计。

`, btn('取消', 'close') + btn('保存', 'brand-save', id || '', 'primary')); } /* ============================ 菜单与权限注册 ============================ */ (() => { const add = (after, item) => { const i = menu.findIndex(m => m[0] === after); if (i >= 0 && !menu.some(m => m[0] === item[0])) menu.splice(i + 1, 0, item) }; add('playreview', ['playteam', '组队活动', 'users']); add('playteam', ['playcoupon', '限时领券', 'ticket']); add('playcoupon', ['playhook', '推广钩子', 'target']); add('segments', ['birthday', '生日会与推送', 'gift']); add('logs', ['coverage', '需求覆盖对照', 'list']); const grant = (roles, pages2) => roles.forEach(r => pages2.forEach(p => { if (permissions[r] && !permissions[r].includes(p)) permissions[r].push(p) })); grant(['管理员', '活动运营'], ['playteam', 'playcoupon', 'playhook', 'birthday', 'coverage']); grant(['财务'], ['playcoupon', 'coverage']); grant(['内容审核', '核销员'], ['coverage']); })(); /* app-ops2.js 加载完成后重新渲染,确保新增页面与菜单可用 */ render(); /* 场景联动:地图选点弹窗(演示;真实环境接高德/腾讯地图 JS SDK) */ const a3 = db.activities.find(x => x.id === 'ACT260902'); if (a3 && a3.redeemOffline === undefined) a3.redeemOffline = true; actions.mapPick = () => openModal('地图定位选点', `

演示地图:真实环境接入高德 / 腾讯地图 JS SDK,拖动选点后回填标准地址。

📍静安区 · 演示网格

点击候选点快速回填:

${['上海市静安区南京西路 1888 号 · 小布在家静安店', '上海市徐汇区淮海中路 999 号 · 环贸 iapm 店', '上海市浦东新区张江路 88 号 · 张江店'].map(p => btn('📍 ' + p, 'mapPickApply', p, 'secondary')).join('')}`); actions.mapPickApply = addr => { wizard.data.address = addr; closeModal(); toast('已定位:' + addr); render() };
取消
访问码: h7etfnpk