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