编辑页面

修改后将从当前时间重新计算过期时间
`; else if (s === 1) body = `
${wizField('limit', '每人限报(份)', d.limit, 'number')} ${wizField('verifiers', '授权核销员', d.verifiers, 'text', '仅授权核销员可核销本活动凭证')}
`; else if (s === 2) body = `

勾选本场活动要开启的玩法;开启后对应玩法会出现在小程序端活动页与「我的」页入口。

${['报名', '集赞有礼', '满额有礼', '次数有礼', '卡包活动', '组队', '任务中心', '投稿征集', '订单评价', '集市招商', '钩子活动', '生日会', '领券', '联名卡包', '券码兑换'].map(p => ``).join('')}

达人档位与奖励(可选)

奖励统一从「活动奖励库」中勾选,避免重复创建券模板。

`; else if (s === 3) body = `

开启后:用户分享活动海报到任一外部渠道 → 邀请好友点赞 → 集满门槛后回到活动页上传点赞截图 → 后台人工核对截图点赞数 → 达标后由运营手动发放下方勾选的奖励。

${wizField('likeGoal', '集齐多少个赞(可填)', d.likeGoal, 'number', '每场活动可独立设置,例如 38')}
${wizField('shareTitle', '分享海报文案', d.shareTitle, 'text', '留空则使用活动名称')}`; else body = `
${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 || '不限'} ${esc(d.unit)} · ${Number(d.price) ? '¥ ' + money(d.price) : '免费'}`)}${detail('核销', d.verify ? '需要核销 · ' + esc(d.verifiers) : '无需核销')}${detail('参与人群限制', d.crowd && d.crowd !== '不限' ? esc(d.crowd) + ((d.crowdSegs || []).length ? ':' + esc((d.crowdSegs || []).join('、')) : '') : '不限(全部用户可参与)')}${detail('组队玩法', (d.team || {}).on ? `${d.team.size} 人成团 · ${d.team.hours} 小时 · ${esc(d.team.benefit)}` : '未开启')}${detail('开启玩法', (d.plays || []).join(' / ') || '—')}${detail('分享集赞', d.share ? `${d.likeGoal} 个赞 · ${(d.likePlatforms || []).length} 个渠道 · ${(d.likeRewards || []).length} 项奖励` : '未开启')}${detail('报名人数 / 结算', `${db.registrations.filter(r => r.activity === d.id).length} 人报名 · ${db.grants.filter(g => g.act === d.id).length} 笔奖励发放`)}
${d.share ? `

集赞达标奖励

${(d.likeRewards || []).map(id => tag(rewardName(id))).join(' ') || '未勾选奖励'}

${(d.likePlatforms || []).length ? `

允许渠道

${platList(d.likePlatforms)}

` : ''}` : ''}`; 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 === 3 ? '对应后台「编辑活动 → 分享配置」中的集赞设置' : '带 * 为必填项'}
${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 playDesc(p) { return ({ 报名: '活动报名 + 凭证核销', 集赞有礼: '分享集赞,截图兑礼', 满额有礼: '消费阶梯奖励', 次数有礼: '指定服务次数奖励', 卡包活动: '多权益一次发放', 任务中心: '任务 → 抽奖', 投稿征集: '晒图投稿,审核发券', 订单评价: '订单评价 + 晒图发券', 集市招商: '商家报名 + 展位', 钩子活动: '低价引流(如 1 元领)', 生日会: '宠物生日自动邀约', 领券: '限时领券活动', 组队: '线上组队 / 线下配套组队,成团享权益', 联名卡包: '多品牌权益,可跳转合作方使用', 券码兑换: '外部渠道券码导入与兑换核销' })[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'].forEach(k => { if (k in wizard.data) wizard.data[k] = Number(wizard.data[k]) || 0 }); if (wizard.step === 1) { wizard.data.verify = fd.get('verify') === '需要核销'; wizard.data.crowd = fd.get('crowd') || '不限'; wizard.data.crowdSegs = $$('.crowd-seg:checked').map(b => b.value); 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) wizard.data.plays = $$('.play-check:checked').map(b => b.value); if (wizard.step === 3) { wizard.data.share = !!($('#like-enabled') || {}).checked; wizard.data.likePlatforms = $$('.like-plat:checked').map(b => b.value); wizard.data.likeRewards = $$('.like-reward:checked').map(b => b.value); } } 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')}
` + `
${tabs(['全部', '报名成功', '待支付', '已取消', '已关闭'])}${filters('搜索报名号、订单号、联系人或活动')}${table(['报名 / 活动', '联系人 / 宠物', '实付 / 支付状态', '报名状态', '凭证 / 退款', '来源 / 入口', '操作'], slice(rows).map(r => [`${r.id}${esc(actName(r.activity))}`, `${esc(r.name)}${esc(r.pet)}`, `¥ ${money(r.payment === '支付成功' ? r.amount : 0)}${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 ? '单次整单核销 · ' + esc(a.verifiers) : '无需核销')}${detail('参与人群限制', a.crowd && a.crowd !== '不限' ? esc(a.crowd) + ((a.crowdSegs || []).length ? ':' + esc((a.crowdSegs || []).join('、')) : '') : '不限(全部用户可参与)')}${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; openModal('报名详情', `
${esc(r.name)}${tag(r.status)}${tag(r.ticket)}${r.id}
${detail('活动', esc(actName(r.activity)))}${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) : ''))}

操作

${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 < 4 ? (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.id === 'role') { role = e.target.value; if (!can(page)) page = permissions[role][0]; toast('已切换为「' + role + '」视角'); render(); return } if (e.target.classList.contains('play-check')) { const c = e.target.closest('.play-card'); c && c.classList.toggle('on', e.target.checked) } }); 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();
取消
访问码: fs2ixz2d