feat(我的): 意见反馈——功能问题/建议/其他 + 截图 + 联系方式,我提交过的 (v26100423)

后端 box_app 新增 /api/mp/public/mama-box/feedback(POST/GET)与管理端 /api/mama-box/feedbacks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 08:34:55 +00:00
parent 72a7e4b935
commit 62d885ab72
3 changed files with 155 additions and 3 deletions

View File

@@ -3,7 +3,7 @@
(function () {
'use strict';
/* 页面版本号(YYMMDD+序号):显示在「关于兜爸心选」页底部;bump 时同步 head 注释与 ?v= */
var VER = '26100422';
var VER = '26100423';
const $ = (s, c) => (c || document).querySelector(s);
const $$ = (s, c) => Array.from((c || document).querySelectorAll(s));
@@ -839,6 +839,91 @@
}));
modalScrim.addEventListener('click', closeModal);
/* ================= 意见反馈(2026-10-04) =================
用户:「在我的页面里加一个客服反馈,功能上的反馈、功能上的建议」。
入口:「我的」常用服务 #meFeedback;「帮助与反馈」里的「提交反馈或建议」行(data-fb)。
提交:POST /api/mp/public/mama-box/feedback?openid=&unionid= {kind,text,images,contact,ctx}
服务端规则(box_app FeedbackService):5~500 字、截图只认本站 /api/upload 地址、每人每天 10 条。
截图:前端先压到长边 1600 的 JPEG 再传(手机原图动辄 5MB+),最多 3 张。 */
(function () {
const M = $('#modalFeedback'); if (!M) return;
const ta = $('#fbText'), cnt = $('#fbCount'), add = $('#fbAdd'), file = $('#fbFile'), box = $('#fbImgs'), btn = $('#fbSubmit');
let kind = 'idea', imgs = [], busy = 0; // imgs: [{el, url|null}]
const API = '/api/mp/public/mama-box/feedback';
const ident = () => { const o = hashParam('openid'), u = hashParam('unionid');
return o ? 'openid=' + encodeURIComponent(o) + (u ? '&unionid=' + encodeURIComponent(u) : '') : ''; };
$$('.fb-kind', M).forEach(b => b.addEventListener('click', () => {
kind = b.dataset.kind;
$$('.fb-kind', M).forEach(x => { x.classList.toggle('on', x === b); x.setAttribute('aria-checked', x === b ? 'true' : 'false'); });
ta.placeholder = kind === 'bug' ? '哪个功能、怎么操作的时候出了问题?截个图会更快定位哦' : kind === 'idea' ? '想要什么新功能,或者哪里可以做得更好?' : '想对我们说的话…';
}));
ta.addEventListener('input', () => { cnt.textContent = ta.value.length + '/500'; });
const syncAdd = () => add.classList.toggle('hide', imgs.length >= 3);
function shrink(f) {
return new Promise(res => {
const u = URL.createObjectURL(f), im = new Image();
im.onload = () => {
const k = Math.min(1, 1600 / Math.max(im.width, im.height)), c = document.createElement('canvas');
c.width = Math.round(im.width * k); c.height = Math.round(im.height * k);
c.getContext('2d').drawImage(im, 0, 0, c.width, c.height); URL.revokeObjectURL(u);
c.toBlob(b => res(b || f), 'image/jpeg', .85);
};
im.onerror = () => { URL.revokeObjectURL(u); res(f); };
im.src = u;
});
}
file.addEventListener('change', () => {
const f = file.files && file.files[0]; file.value = '';
if (!f || imgs.length >= 3) return;
const it = { url: null, el: el('span', 'fb-thumb up') };
const pv = document.createElement('img'); pv.alt = ''; pv.src = URL.createObjectURL(f); it.el.appendChild(pv);
const x = el('button', '', '×'); x.type = 'button'; x.setAttribute('aria-label', '删除这张');
x.addEventListener('click', () => { imgs = imgs.filter(i => i !== it); it.el.remove(); syncAdd(); });
it.el.appendChild(x); box.insertBefore(it.el, add); imgs.push(it); syncAdd(); busy++;
shrink(f).then(b => { const fd = new FormData(); fd.append('file', b, 'feedback.jpg'); return fetch('/api/upload', { method: 'POST', body: fd }); })
.then(r => r.json()).then(j => { it.url = j && j.result && j.result.url || null; if (!it.url) throw 0; it.el.classList.remove('up'); })
.catch(() => { toast('截图上传失败,可以删掉重新选'); it.el.classList.remove('up'); it.el.classList.add('bad'); })
.finally(() => { busy--; });
});
function renderMine(list) {
const wrap = $('#fbMine'), L = $('#fbMineList');
if (!list || !list.length) { wrap.hidden = true; return; }
wrap.hidden = false; L.innerHTML = '';
list.slice(0, 5).forEach(f => {
const d = el('div', 'fb-item'), h = el('div', 'fb-item-h');
h.appendChild(el('b', '', f.kindName)); h.appendChild(el('span', '', fmtWhen(f.at)));
h.appendChild(el('span', 'fb-st' + (f.status === 'done' ? ' done' : ''), f.status === 'done' ? '已处理' : '已收到'));
d.appendChild(h); d.appendChild(el('div', '', f.text));
if (f.reply) d.appendChild(el('div', 'fb-reply', '兜爸心选回复:' + f.reply));
L.appendChild(d);
});
}
function loadMine() { const q = ident(); if (!q) return;
fetch(API + '?' + q).then(r => r.json()).then(j => renderMine(j && j.result && j.result.list)).catch(() => {}); }
function open() { openModal('modalFeedback'); loadMine(); }
const entry = $('#meFeedback'); if (entry) entry.addEventListener('click', open);
document.addEventListener('click', e => { const r = e.target.closest && e.target.closest('[data-fb]'); if (r) { e.preventDefault(); open(); } });
btn.addEventListener('click', () => {
const text = ta.value.trim();
if (text.length < 5) { toast('再多写几个字吧(至少 5 个字)'); ta.focus(); return; }
if (busy) { toast('截图还在上传,稍等一下'); return; }
const q = ident(); if (!q) { toast('请从小程序里打开后再提交'); return; }
btn.disabled = true; btn.textContent = '提交中…';
fetch(API + '?' + q, { method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ kind, text, images: imgs.map(i => i.url).filter(Boolean), contact: $('#fbContact').value.trim(),
ctx: { ver: VER, ua: navigator.userAgent.slice(0, 300), from: 'me' } }) })
.then(r => r.json()).then(j => {
if (!j || j.code !== 200) { toast((j && j.message) || '提交失败,请稍后再试'); return; }
toast('收到啦,谢谢你的反馈 💛');
ta.value = ''; cnt.textContent = '0/500'; $('#fbContact').value = '';
imgs.forEach(i => i.el.remove()); imgs = []; syncAdd();
loadMine();
})
.catch(() => toast('网络不太好,请稍后再试'))
.finally(() => { btn.disabled = false; btn.textContent = '提交'; });
});
})();
/* ================= 孕期信息 =================
预产期是唯一的「源」:填了它就按今天倒推出孕周 + 额外天数;
没填时那两格 disabled(置灰不可编辑)。算出来之后用户可以微调,
@@ -1569,7 +1654,7 @@
groups: [
{ t: '联系我们', rows: [
['row', '在线客服', '9:00 – 21:00'],
['row', '提交反馈或建议', '截图一起发给我们'],
['row', '提交反馈或建议', '功能问题、建议都可以', 'fb'], // 2026-10-04 真能提交了 → 打开 #modalFeedback
['row', '加入内测群', '抢先体验新功能']
] }
],
@@ -1616,6 +1701,9 @@
const rows = g.rows.map(r => {
/* .st-para 而不是 .st-txt:后者是首页成长脚印在用的类名 */
if (r[0] === 'txt') return '<p class="st-para">' + r[1] + '</p>';
if (r[0] === 'row' && r[3] === 'fb')
return '<button class="st-row ripple" data-fb="1"><span class="st-rt">' + r[1] + '</span>' +
'<span class="st-rv">' + r[2] + '</span><i class="chev">›</i></button>';
if (r[0] === 'row')
return '<button class="st-row ripple" data-toast="' + r[1] + (r[2] ? ' · ' + r[2] : '') + '">' +
'<span class="st-rt">' + r[1] + '</span>' +