Files
sundynix-pets/pets-fe/pages/comments/comments.js
T
Blizzard 7a55ee0f2e feat(community): 评论改为独立二级页面 + 评论内容安全审核 + 评论数按实发实时纠正
前端:
- 新增 pages/comments 独立评论页(列表 + 底部输入条 + 回复),社区卡片点评论改为 navigateTo
- 输入条用普通流式布局 + 原生 adjust-position 做键盘避让,聚焦期间不 setData 碰输入框
- 评论只能配 1 张图;底部常驻「审核后公开」提示;发送按钮在途「审核中…」防连点
- 发布结果按状态区分:过审即时展示,其余提示「已提交,审核通过后展示」
- 评论页把已发布评论数回传社区列表,卡片数字实时更新

后端:
- CreateComment 文本判为违规时直接拒收(ErrContentRejected),不入库
- 社区列表/用户帖子列表用「已发布评论实时条数」覆盖 comment_count,
  修正异步过审/删待审/后台改状态导致的计数漂移
- IDCard 手机号返回完整号码(上一批遗留)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 16:04:21 +08:00

174 lines
5.7 KiB
JavaScript

const api = require('../../utils/api.js');
const upload = require('../../utils/upload.js');
// 相对时间:社区里关心「多久以前」,精确到秒没意义
function fmtAgo(iso) {
if (!iso) return '';
const d = new Date(iso);
if (isNaN(d.getTime())) return '';
const min = Math.floor((Date.now() - d.getTime()) / 60000);
if (min < 1) return '刚刚';
if (min < 60) return min + ' 分钟前';
if (min < 60 * 24) return Math.floor(min / 60) + ' 小时前';
if (min < 60 * 24 * 7) return Math.floor(min / 1440) + ' 天前';
const p = (n) => (n < 10 ? '0' + n : '' + n);
const sameYear = d.getFullYear() === new Date().getFullYear();
return (sameYear ? '' : d.getFullYear() + '-') + p(d.getMonth() + 1) + '-' + p(d.getDate());
}
Page({
data: {
postId: '',
total: 0,
comments: [],
commentText: '',
commentImages: [],
replyTo: {},
cmFocus: false,
sendingComment: false,
},
onLoad(query) {
this.setData({ postId: query.postId || '' });
this.loadComments();
},
// 把当前已发布评论数回传给社区列表,卡片上的评论数实时跟着变
syncCountBack() {
const ch = this.getOpenerEventChannel && this.getOpenerEventChannel();
if (ch && ch.emit) ch.emit('commentChanged', { postId: this.data.postId, count: this.data.total });
},
loadComments() {
if (!this.data.postId) return;
api
.listComments(this.data.postId)
.then((page) => {
this.setData({
total: page.total || 0,
comments: (page.list || []).map((c) => ({
...c,
timeText: fmtAgo(c.created_at),
initial: (c.author_name || '?').slice(0, 1),
imgs: Array.isArray(c.images) ? c.images : [],
replies: (c.replies || []).map((r) => ({
...r,
timeText: fmtAgo(r.created_at),
initial: (r.author_name || '?').slice(0, 1),
})),
})),
});
this.syncCountBack();
})
.catch(() => {});
},
onCommentInput(e) {
this.setData({ commentText: e.detail.value });
},
// 点任意评论/回复:底部输入条切到「回复 xxx」并聚焦
onReplyTo(e) {
const { id, name } = e.currentTarget.dataset;
if (!id) return;
this.setData({ replyTo: { id, name }, cmFocus: true });
},
onCancelReply() {
this.setData({ replyTo: {} });
},
// 关键:聚焦期间 JS 绝不 setData 碰输入条(连 cmFocus:false 都不设),
// 否则原生 textarea 会被抖失焦、键盘弹一下就自己收。cmFocus 只在失焦时复位,
// 这样下次点回复 false→true 还能重新拉起键盘。键盘避让全交给原生 adjust-position。
onCommentBlur() {
this.setData({ cmFocus: false });
},
// 配图:评论只允许 1 张
onPickCommentImages() {
if (this.data.commentImages.length >= 1) {
return wx.showToast({ title: '评论只能配 1 张图', icon: 'none' });
}
upload
.chooseAndUploadImages(1)
.then((files) => this.setData({ commentImages: (files || []).slice(0, 1) }))
.catch((e) => {
if (e && e.canceled) return;
wx.showToast({ title: (e && e.message) || '上传失败', icon: 'none' });
});
},
onRemoveCommentImage(e) {
const list = this.data.commentImages.slice();
list.splice(e.currentTarget.dataset.index, 1);
this.setData({ commentImages: list });
},
onPreviewCommentImage(e) {
const { urls, cur } = e.currentTarget.dataset;
if (urls && urls.length) wx.previewImage({ urls, current: cur });
},
async onSendComment() {
if (this.data.sendingComment) return; // 防连点
const text = (this.data.commentText || '').trim();
if (!text) return wx.showToast({ title: '写点什么再发', icon: 'none' });
if (!this.data.postId) return;
this.setData({ sendingComment: true });
wx.showLoading({ title: '审核中…', mask: true });
try {
const res = await api.createComment(
this.data.postId,
text,
this.data.commentImages.map((i) => i.url),
this.data.replyTo.id || '',
);
wx.hideLoading();
this.setData({ commentText: '', commentImages: [], replyTo: {}, cmFocus: false, sendingComment: false });
if (res && res.status === 'published') {
this.loadComments();
wx.showToast({ title: '评论成功', icon: 'success' });
} else {
wx.showToast({ title: '已提交,审核通过后展示', icon: 'none' });
}
} catch (e) {
wx.hideLoading();
this.setData({ sendingComment: false });
wx.showToast({ title: (e && e.message) || '评论失败', icon: 'none' });
}
},
// 长按删除自己的评论
onDeleteComment(e) {
const { id, self } = e.currentTarget.dataset;
if (!id || !self) return;
wx.showModal({
title: '删除评论',
content: '确定删除这条评论?',
confirmColor: '#D9534F',
success: (r) => {
if (!r.confirm) return;
api
.deleteComment(id)
.then(() => this.loadComments())
.catch((err) => wx.showToast({ title: (err && err.message) || '删除失败', icon: 'none' }));
},
});
},
// 一级评论默认只带 3 条回复,点开拉全量
onExpandReplies(e) {
const { id, index } = e.currentTarget.dataset;
api
.listReplies(id)
.then((list) =>
this.setData({
[`comments[${index}].replies`]: (list || []).map((r) => ({
...r,
timeText: fmtAgo(r.created_at),
initial: (r.author_name || '?').slice(0, 1),
})),
}),
)
.catch((err) => wx.showToast({ title: (err && err.message) || '加载失败', icon: 'none' }));
},
});