feat(community): 评论改为独立二级页面 + 评论内容安全审核 + 评论数按实发实时纠正
前端: - 新增 pages/comments 独立评论页(列表 + 底部输入条 + 回复),社区卡片点评论改为 navigateTo - 输入条用普通流式布局 + 原生 adjust-position 做键盘避让,聚焦期间不 setData 碰输入框 - 评论只能配 1 张图;底部常驻「审核后公开」提示;发送按钮在途「审核中…」防连点 - 发布结果按状态区分:过审即时展示,其余提示「已提交,审核通过后展示」 - 评论页把已发布评论数回传社区列表,卡片数字实时更新 后端: - CreateComment 文本判为违规时直接拒收(ErrContentRejected),不入库 - 社区列表/用户帖子列表用「已发布评论实时条数」覆盖 comment_count, 修正异步过审/删待审/后台改状态导致的计数漂移 - IDCard 手机号返回完整号码(上一批遗留) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
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' }));
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user