From 7a55ee0f2ed8f4d6dc38c7f9d2f9ec28b9f46073 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Mon, 3 Aug 2026 16:04:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(community):=20=E8=AF=84=E8=AE=BA=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E7=8B=AC=E7=AB=8B=E4=BA=8C=E7=BA=A7=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=20+=20=E8=AF=84=E8=AE=BA=E5=86=85=E5=AE=B9=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E5=AE=A1=E6=A0=B8=20+=20=E8=AF=84=E8=AE=BA=E6=95=B0=E6=8C=89?= =?UTF-8?q?=E5=AE=9E=E5=8F=91=E5=AE=9E=E6=97=B6=E7=BA=A0=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 前端: - 新增 pages/comments 独立评论页(列表 + 底部输入条 + 回复),社区卡片点评论改为 navigateTo - 输入条用普通流式布局 + 原生 adjust-position 做键盘避让,聚焦期间不 setData 碰输入框 - 评论只能配 1 张图;底部常驻「审核后公开」提示;发送按钮在途「审核中…」防连点 - 发布结果按状态区分:过审即时展示,其余提示「已提交,审核通过后展示」 - 评论页把已发布评论数回传社区列表,卡片数字实时更新 后端: - CreateComment 文本判为违规时直接拒收(ErrContentRejected),不入库 - 社区列表/用户帖子列表用「已发布评论实时条数」覆盖 comment_count, 修正异步过审/删待审/后台改状态导致的计数漂移 - IDCard 手机号返回完整号码(上一批遗留) Co-Authored-By: Claude Opus 5 --- pets-be/internal/service/community.go | 34 ++++ pets-be/internal/service/service.go | 3 + pets-be/internal/service/user_profile.go | 1 + pets-fe/app.json | 1 + .../components/bottom-sheet/bottom-sheet.js | 65 ++++--- .../components/bottom-sheet/bottom-sheet.wxml | 34 ++-- .../components/bottom-sheet/bottom-sheet.wxss | 10 +- pets-fe/pages/comments/comments.js | 173 ++++++++++++++++++ pets-fe/pages/comments/comments.json | 6 + pets-fe/pages/comments/comments.wxml | 82 +++++++++ pets-fe/pages/comments/comments.wxss | 82 +++++++++ pets-fe/pages/community/community.js | 16 +- 12 files changed, 461 insertions(+), 46 deletions(-) create mode 100644 pets-fe/pages/comments/comments.js create mode 100644 pets-fe/pages/comments/comments.json create mode 100644 pets-fe/pages/comments/comments.wxml create mode 100644 pets-fe/pages/comments/comments.wxss diff --git a/pets-be/internal/service/community.go b/pets-be/internal/service/community.go index 38d750e..597b741 100644 --- a/pets-be/internal/service/community.go +++ b/pets-be/internal/service/community.go @@ -44,6 +44,35 @@ func (s *Service) attachPostImages(posts []model.Post) { } } +// attachCommentCounts 用「已发布评论」的实时条数覆盖 comment_count。 +// 维护型计数器在这些路径上会漂移:图片评论异步过审后没补计数、删除待审评论时误减、 +// 后台审核改状态没同步。直接按实际已发布条数算,永远和评论弹层「共 X 条」一致。 +func (s *Service) attachCommentCounts(posts []model.Post) { + if len(posts) == 0 { + return + } + ids := make([]string, len(posts)) + for i := range posts { + ids[i] = posts[i].ID + } + type row struct { + PostID string + N int + } + var rows []row + s.db.Model(&model.Comment{}). + Select("post_id, count(*) as n"). + Where("post_id IN ? AND status = ?", ids, model.PostPublished). + Group("post_id").Scan(&rows) + cnt := make(map[string]int, len(rows)) + for _, r := range rows { + cnt[r.PostID] = r.N + } + for i := range posts { + posts[i].CommentCount = cnt[posts[i].ID] + } +} + // tabTag 将 feed tab 映射为标签过滤(空表示不过滤) func tabTag(tab string) string { switch tab { @@ -81,6 +110,7 @@ func (s *Service) ListPosts(userID, tab string, offset, limit int) ([]model.Post return nil, 0, err } s.attachPostImages(posts) + s.attachCommentCounts(posts) s.markFollowed(userID, posts) return posts, total, nil } @@ -299,6 +329,10 @@ func (s *Service) CreateComment(userID, postID string, content string, images [] // 评论也过内容安全(scene=2 评论)。图片这里已经是可访问 URL status, _ := s.moderateInitial(content, user.OpenID, 2, len(images) > 0) + // 文本被判违规:直接拒绝、不入库,让用户知道发不出去 + if status == model.PostRejected { + return nil, ErrContentRejected + } comment := model.Comment{ PostID: postID, UserID: userID, AuthorName: name, Content: content, diff --git a/pets-be/internal/service/service.go b/pets-be/internal/service/service.go index 3a37440..83b92c8 100644 --- a/pets-be/internal/service/service.go +++ b/pets-be/internal/service/service.go @@ -24,6 +24,9 @@ var ErrForbidden = errors.New("无权操作") // ErrInvalidParam 参数不合法(handler 据此返回 40000) var ErrInvalidParam = errors.New("参数不合法") +// ErrContentRejected 内容安全判定为违规,直接拒绝发送(不入库) +var ErrContentRejected = errors.New("内容包含违规信息,无法发布") + // Service 业务逻辑聚合,方法按领域分散在各文件 type Service struct { db *gorm.DB diff --git a/pets-be/internal/service/user_profile.go b/pets-be/internal/service/user_profile.go index e330f2f..e75b418 100644 --- a/pets-be/internal/service/user_profile.go +++ b/pets-be/internal/service/user_profile.go @@ -215,6 +215,7 @@ func (s *Service) ListUserPosts(viewerID, userID string, offset, limit int) ([]m return nil, 0, err } s.attachPostImages(posts) + s.attachCommentCounts(posts) s.markFollowed(viewerID, posts) return posts, total, nil } diff --git a/pets-fe/app.json b/pets-fe/app.json index 678ee94..50ed886 100644 --- a/pets-fe/app.json +++ b/pets-fe/app.json @@ -11,6 +11,7 @@ "pages/idcard/idcard", "pages/report/report", "pages/community/community", + "pages/comments/comments", "pages/learn/learn", "pages/article/article", "pages/mine/mine", diff --git a/pets-fe/components/bottom-sheet/bottom-sheet.js b/pets-fe/components/bottom-sheet/bottom-sheet.js index 9fe6f36..6541fd1 100644 --- a/pets-fe/components/bottom-sheet/bottom-sheet.js +++ b/pets-fe/components/bottom-sheet/bottom-sheet.js @@ -82,6 +82,7 @@ Component({ replyTo: {}, cmFocus: false, composing: false, // 评论输入面板是否展开(收起时只是一条细条) + sendingComment: false, // 发送+审核在途,防连点 kbHeight: 0, total: 0, }, @@ -120,6 +121,7 @@ Component({ patch.replyTo = {}; patch.cmFocus = false; patch.composing = false; + patch.sendingComment = false; patch.kbHeight = 0; patch.saving = false; } @@ -655,32 +657,49 @@ Component({ // 评论 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.close(); + this.setData({ sendingComment: true }); + wx.showLoading({ title: '审核中…', mask: true }); try { - await api.createComment( + const res = await api.createComment( this.data.postId, text, this.data.commentImages.map((i) => i.url), this.data.replyTo.id || '', ); - this.setData({ commentText: '', commentImages: [], replyTo: {}, composing: false, cmFocus: false }); - this.loadComments(); - this.triggerEvent('commented'); + wx.hideLoading(); + this.setData({ + commentText: '', commentImages: [], replyTo: {}, + composing: false, cmFocus: false, kbHeight: 0, sendingComment: false, + }); + // 过审即时展示;否则挂在人工/异步图片审核,通过后才出现 + if (res && res.status === 'published') { + this.loadComments(); + this.triggerEvent('commented'); + wx.showToast({ title: '评论成功', icon: 'success' }); + } else { + wx.showToast({ title: '已提交,审核通过后展示', icon: 'none' }); + } } catch (e) { - wx.showToast({ title: e.message || '评论失败', icon: 'none' }); + wx.hideLoading(); + this.setData({ sendingComment: false }); + // 违规内容后端直接拒收,这里把原因透出来 + wx.showToast({ title: (e && e.message) || '评论失败', icon: 'none' }); } }, - // 评论配图,最多 3 张 + // 评论配图:只允许 1 张 onPickCommentImages() { - const left = 3 - this.data.commentImages.length; - if (left <= 0) return wx.showToast({ title: '最多 3 张', icon: 'none' }); + if (this.data.commentImages.length >= 1) { + return wx.showToast({ title: '评论只能配 1 张图', icon: 'none' }); + } upload - .chooseAndUploadImages(left) - .then((files) => this.setData({ commentImages: this.data.commentImages.concat(files) })) + .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' }); @@ -692,27 +711,21 @@ Component({ onReplyTo(e) { const { id, name } = e.currentTarget.dataset; if (!id) return; - // 点任意评论直接展开面板并预填回复对象 - this.setData({ replyTo: { id, name }, composing: true, cmFocus: true }); + // 输入框常驻、属性不变,直接置 focus 拉键盘即可;只填「回复对象」这一个横幅 + this.setData({ replyTo: { id, name }, cmFocus: true }); }, - // 点细条:展开大输入面板并聚焦(拉起键盘) - onStartCompose() { - this.setData({ composing: true, cmFocus: true }); + onCommentFocus() { + // 只复位一次性 focus 开关。键盘高度交给 onKbChange,用 transform 平移弹层来避让 + this.setData({ cmFocus: false }); }, - // 收起面板,草稿(文字/图片)留着,下次点开接着写 - onCloseCompose() { - this.setData({ composing: false, cmFocus: false, kbHeight: 0 }); - }, - onCommentFocus(e) { - // adjust-position=false,键盘高度自己接管:弹层是 fixed 定位, - // 交给系统顶会把整块推出屏幕 + // 键盘高度变化(真机可靠来源)。把高度写进 data, + // wxml 里只用它算 transform 平移量 —— 纯合成层操作,不 reflow、不抖焦点 + onKbChange(e) { this.setData({ kbHeight: (e.detail && e.detail.height) || 0 }); }, onCommentBlur() { - // 失焦先放下键盘。有草稿就保持展开(否则点「发送」会因面板收起而丢掉这一下点击); - // 空内容才收回细条 - const empty = !(this.data.commentText || '').trim() && !this.data.commentImages.length; - this.setData({ kbHeight: 0, cmFocus: false, composing: empty ? false : this.data.composing }); + // 失焦不主动清 kbHeight:键盘真正收起时 onKbChange 会回调 0,避免和收键盘动画抢 + this.setData({ cmFocus: false }); }, onCancelReply() { this.setData({ replyTo: {} }); diff --git a/pets-fe/components/bottom-sheet/bottom-sheet.wxml b/pets-fe/components/bottom-sheet/bottom-sheet.wxml index 96e5314..efbf0c5 100644 --- a/pets-fe/components/bottom-sheet/bottom-sheet.wxml +++ b/pets-fe/components/bottom-sheet/bottom-sheet.wxml @@ -7,7 +7,11 @@ module.exports.sel = function (map, key, index, def) { - + + - - {{commentText || '说点什么…'}} - - - - - - - {{replyTo.name ? '回复 ' + replyTo.name : '写评论'}} - + + + + 回复 {{replyTo.name}} + - + adjust-position="{{false}}" cursor-spacing="16"> @@ -92,10 +93,11 @@ module.exports.sel = function (map, key, index, def) { - + {{commentText.length}}/500 - 发送 + {{sendingComment ? '审核中…' : '发送'}} + 评论通过安全审核后才会公开显示,配图最多 1 张 diff --git a/pets-fe/components/bottom-sheet/bottom-sheet.wxss b/pets-fe/components/bottom-sheet/bottom-sheet.wxss index 547e849..e63f16a 100644 --- a/pets-fe/components/bottom-sheet/bottom-sheet.wxss +++ b/pets-fe/components/bottom-sheet/bottom-sheet.wxss @@ -107,6 +107,8 @@ padding:var(--sp-3) var(--sp-5) calc(20rpx + env(safe-area-inset-bottom)); transition:padding-bottom .2s ease; } +/* 展开态:给顶部一点投影,和列表分层 */ +.cmp-dock.open{box-shadow:0 -8rpx 24rpx rgba(0,0,0,.05)} .cm{display:flex;gap:var(--sp-3);padding:var(--sp-3) 0} .cm-av{ @@ -155,9 +157,9 @@ .cm-dock-title{font-size:var(--fs-md);font-weight:var(--fw-b);color:var(--text)} .cm-dock-close{display:flex;align-items:center;color:var(--muted);padding:var(--sp-1)} .cm-ta{ - width:100%;box-sizing:border-box;min-height:200rpx;max-height:520rpx;height:auto; - padding:var(--sp-4);background:var(--surface-2);border-radius:var(--r-md); - font-size:var(--fs-lg);line-height:1.5; + width:100%;box-sizing:border-box;min-height:96rpx;max-height:360rpx;height:auto; + padding:var(--sp-3) var(--sp-4);background:var(--surface-2);border-radius:var(--r-md); + font-size:var(--fs-md);line-height:1.5; } .cm-toolbar{display:flex;align-items:center;gap:var(--sp-3);margin-top:var(--sp-3)} .cm-pic{flex:none;display:flex;align-items:center;color:var(--muted)} @@ -168,3 +170,5 @@ background:var(--line);color:#fff;font-size:var(--fs-md);font-weight:var(--fw-b);transition:.16s ease; } .cm-send.on{background:var(--cta)} +.cm-pic.disabled{opacity:.35} +.cm-tip{margin-top:var(--sp-2);color:var(--muted2);font-size:var(--fs-cap);line-height:1.5} diff --git a/pets-fe/pages/comments/comments.js b/pets-fe/pages/comments/comments.js new file mode 100644 index 0000000..a0477ef --- /dev/null +++ b/pets-fe/pages/comments/comments.js @@ -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' })); + }, +}); diff --git a/pets-fe/pages/comments/comments.json b/pets-fe/pages/comments/comments.json new file mode 100644 index 0000000..6594cd8 --- /dev/null +++ b/pets-fe/pages/comments/comments.json @@ -0,0 +1,6 @@ +{ + "usingComponents": { + "nav-bar": "/components/nav-bar/nav-bar", + "pt-icon": "/components/pt-icon/index" + } +} diff --git a/pets-fe/pages/comments/comments.wxml b/pets-fe/pages/comments/comments.wxml new file mode 100644 index 0000000..c682909 --- /dev/null +++ b/pets-fe/pages/comments/comments.wxml @@ -0,0 +1,82 @@ + + + + 共 {{total}} 条评论 + + + {{item.initial}} + + {{item.author_name}} + {{item.content}} + + + + + {{item.timeText}} + 回复 + 删除 + + + + + {{r.initial}} + + + {{r.author_name}} + 回复 {{r.reply_to_name}} + + {{r.content}} + + {{r.timeText}} + 回复 + 删除 + + + + + — 展开全部 {{item.reply_n}} 条回复 + + + + + + + + 还没有评论 + 来抢个沙发,说点什么吧~ + + + + + + + + 回复 {{replyTo.name}} + + + + + + + + + + + + {{sendingComment ? '审核中…' : '发送'}} + + 评论通过安全审核后才会公开显示,配图最多 1 张 + diff --git a/pets-fe/pages/comments/comments.wxss b/pets-fe/pages/comments/comments.wxss new file mode 100644 index 0000000..65741f8 --- /dev/null +++ b/pets-fe/pages/comments/comments.wxss @@ -0,0 +1,82 @@ +/* flex 列 + 整页 overflow:hidden:页面本身不滚,列表在内层 scroll-view 里滚。 + 这样聚焦时 adjust-position 只能均匀上移整屏,不会过度上滚露出大空白 */ +page{display:flex;flex-direction:column;height:100vh;overflow:hidden;background:var(--bg)} + +.cm-scroll{flex:1;min-height:0;padding:0 var(--sp-5)} +.cm-count-head{ + padding:var(--sp-3) 0;font-size:var(--fs-md);font-weight:var(--fw-b); + color:var(--text);border-bottom:1rpx solid var(--line); +} + +/* 空态 */ +.cmp-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:160rpx 0;color:var(--muted)} +.cmp-empty-ic{ + width:140rpx;height:140rpx;border-radius:50%;margin-bottom:var(--sp-4); + display:flex;align-items:center;justify-content:center; + background:var(--primary-soft);color:var(--primary); +} +.cmp-empty-t{font-size:var(--fs-lg);font-weight:var(--fw-b);color:var(--text-2)} +.cmp-empty-p{margin-top:var(--sp-1);font-size:var(--fs-sm);color:var(--muted2)} + +/* 评论行 */ +.cm{display:flex;gap:var(--sp-3);padding:var(--sp-3) 0} +.cm-av{ + width:64rpx;height:64rpx;border-radius:50%;flex:none; + display:flex;align-items:center;justify-content:center; + background:var(--primary-soft);color:var(--primary-ink); + font-size:var(--fs-md);font-weight:var(--fw-b); +} +.cm-av-sm{width:48rpx;height:48rpx;font-size:var(--fs-sm)} +.cm-body{flex:1;min-width:0} +.cm-name{font-size:var(--fs-sm);color:var(--muted)} +.cm-at{color:var(--muted2)} +.cm-text{margin-top:6rpx;font-size:var(--fs-md);line-height:1.55;word-break:break-word} +.cm-imgs{display:flex;flex-wrap:wrap;gap:var(--sp-1);margin-top:var(--sp-2)} +.cm-img{width:150rpx;height:150rpx;border-radius:var(--r-sm)} +.cm-meta{display:flex;align-items:center;gap:var(--sp-4);margin-top:var(--sp-2);font-size:var(--fs-cap);color:var(--muted2)} +.cm-reply-btn{color:var(--muted)} +.cm-del{color:var(--red-ink)} +.cm-replies{margin-top:var(--sp-2)} +.cm-reply{display:flex;gap:var(--sp-2);padding:var(--sp-2) 0} +.cm-more{margin-top:4rpx;font-size:var(--fs-cap);color:var(--muted)} +.mine-tag{ + margin-left:var(--sp-1);font-size:var(--fs-cap);font-weight:var(--fw-b); + color:var(--primary-ink);background:var(--primary-soft);padding:2rpx var(--sp-1);border-radius:var(--r-full); +} + +/* 底部输入条:flex 列里最后一行,不 fixed。整屏靠 adjust-position 均匀上移避让键盘 */ +.cm-bar{ + flex:none;background:#fff;border-top:1rpx solid var(--line); + padding:var(--sp-3) var(--sp-5) calc(16rpx + env(safe-area-inset-bottom)); +} +.cm-replying{ + display:inline-flex;align-items:center;gap:var(--sp-2);margin-bottom:var(--sp-2); + padding:4rpx var(--sp-3);border-radius:var(--r-full); + background:var(--primary-soft);color:var(--primary-ink);font-size:var(--fs-cap); +} +.cm-cancel{display:flex;align-items:center} +.cm-bar-row{display:flex;align-items:flex-end;gap:var(--sp-3)} +.cm-ta{ + flex:1;min-width:0;box-sizing:border-box;min-height:72rpx;max-height:300rpx;height:auto; + padding:var(--sp-3) var(--sp-4);background:var(--surface-2);border-radius:var(--r-md); + font-size:var(--fs-md);line-height:1.5; +} +.cm-pic{flex:none;display:flex;align-items:center;color:var(--muted);height:72rpx} +.cm-pic.disabled{opacity:.35} +.cm-send{ + flex:none;height:72rpx;padding:0 var(--sp-6);border-radius:var(--r-full); + display:flex;align-items:center;justify-content:center; + background:var(--line);color:#fff;font-size:var(--fs-md);font-weight:var(--fw-b);transition:.16s ease; +} +.cm-send.on{background:var(--cta)} +.cm-tip{margin-top:var(--sp-2);color:var(--muted2);font-size:var(--fs-cap);line-height:1.5} + +/* 配图预览 */ +.img-picker{display:flex;flex-wrap:wrap;gap:var(--sp-3);margin-bottom:var(--sp-2)} +.img-thumb{position:relative;width:160rpx;height:160rpx;border-radius:var(--r-sm);overflow:hidden} +.img-thumb image{width:100%;height:100%} +.img-del{ + position:absolute;top:0;right:0;width:44rpx;height:44rpx; + display:flex;align-items:center;justify-content:center; + background:rgba(0,0,0,.55);color:#fff;border-radius:0 var(--r-sm) 0 var(--r-sm); +} diff --git a/pets-fe/pages/community/community.js b/pets-fe/pages/community/community.js index c487a37..08c755a 100644 --- a/pets-fe/pages/community/community.js +++ b/pets-fe/pages/community/community.js @@ -105,7 +105,21 @@ Page({ .catch((e) => toastErr(e, '点赞失败')); }, openComments(e) { - this.setData({ sheetPostId: e.currentTarget.dataset.id, sheetType: 'comments', sheetShow: true }); + const id = e.currentTarget.dataset.id; + if (!id) return; + // 评论改成独立二级页面。原来套在 fixed 弹层里,输入框被键盘遮挡且无法可靠避让; + // 普通页面里的 fixed 输入条用 transform 平移就能稳稳浮在键盘上方 + wx.navigateTo({ + url: `/pages/comments/comments?postId=${id}`, + events: { + // 评论页把「已发布评论数」回传,卡片上的数字实时更新 + commentChanged: (d) => { + if (!d || !d.postId) return; + const idx = this.data.posts.findIndex((p) => p.id === d.postId); + if (idx >= 0) this.setData({ [`posts[${idx}].comment_count`]: d.count }); + }, + }, + }); }, openSheet(e) { this.setData({ sheetType: e.currentTarget.dataset.type, sheetPostId: '', sheetShow: true });