调整日记评论面板跟随键盘并保留输入草稿
This commit is contained in:
@@ -12,6 +12,12 @@ Page(withDataCache({
|
|||||||
hasMoreComments: true,
|
hasMoreComments: true,
|
||||||
showCommentInput: false,
|
showCommentInput: false,
|
||||||
commentText: '',
|
commentText: '',
|
||||||
|
commentInitialText: '',
|
||||||
|
commentLength: 0,
|
||||||
|
commentFocus: false,
|
||||||
|
commentKeyboardHeight: 0,
|
||||||
|
commentEditorHeight: 56,
|
||||||
|
commentSending: false,
|
||||||
replyTo: null,
|
replyTo: null,
|
||||||
coverSwiperIndex: 0,
|
coverSwiperIndex: 0,
|
||||||
previewImages: [],
|
previewImages: [],
|
||||||
@@ -119,28 +125,49 @@ Page(withDataCache({
|
|||||||
onPreviewSwiperChange: function(e) { this.setData({ previewIndex: e.detail.current }); },
|
onPreviewSwiperChange: function(e) { this.setData({ previewIndex: e.detail.current }); },
|
||||||
|
|
||||||
onCommentTap: function() {
|
onCommentTap: function() {
|
||||||
this.setData({ showCommentInput: true, replyTo: null, commentText: '' });
|
this.openComment(null);
|
||||||
},
|
},
|
||||||
onReplyTap: function(e) {
|
onReplyTap: function(e) {
|
||||||
var item = e.currentTarget.dataset.item;
|
var item = e.currentTarget.dataset.item;
|
||||||
this.setData({
|
this.openComment({ id: item.id, user_id: item.user_id, nickname: item.nickname });
|
||||||
showCommentInput: true,
|
},
|
||||||
replyTo: { id: item.id, user_id: item.user_id, nickname: item.nickname },
|
openComment: function(replyTo) {
|
||||||
commentText: ''
|
var sameTarget = (!replyTo && !this.data.replyTo) ||
|
||||||
});
|
(replyTo && this.data.replyTo && replyTo.id === this.data.replyTo.id);
|
||||||
|
var text = sameTarget ? this.data.commentText : '';
|
||||||
|
this.setData({ showCommentInput: true, replyTo: replyTo, commentText: text,
|
||||||
|
commentInitialText: text, commentLength: text.length, commentFocus: true,
|
||||||
|
commentKeyboardHeight: 0, commentEditorHeight: 56 });
|
||||||
},
|
},
|
||||||
|
|
||||||
onCommentInput: function(e) {
|
onCommentInput: function(e) {
|
||||||
this.setData({ commentText: e.detail.value });
|
// 不把正文反复写回原生输入框,避免中文输入时光标跳动。
|
||||||
|
this.data.commentText = e.detail.value || '';
|
||||||
|
this.setData({ commentLength: this.data.commentText.length });
|
||||||
|
},
|
||||||
|
onCommentKeyboardHeight: function(e) {
|
||||||
|
var height = Math.max(0, Number(e.detail.height) || 0);
|
||||||
|
if (this.data.showCommentInput) this.setData({ commentKeyboardHeight: height });
|
||||||
|
},
|
||||||
|
onCommentLineChange: function(e) {
|
||||||
|
this.setData({ commentEditorHeight: Math.min(160, Math.max(56, Number(e.detail.height) || 56)) });
|
||||||
|
},
|
||||||
|
onCommentBlur: function() {
|
||||||
|
this.setData({ commentFocus: false });
|
||||||
},
|
},
|
||||||
|
|
||||||
cancelComment: function() {
|
cancelComment: function() {
|
||||||
this.setData({ showCommentInput: false, replyTo: null, commentText: '' });
|
if (this.data.commentSending) return;
|
||||||
|
this.setData({ showCommentInput: false, commentFocus: false, commentKeyboardHeight: 0 });
|
||||||
|
wx.hideKeyboard();
|
||||||
|
},
|
||||||
|
onHide: function() {
|
||||||
|
this.setData({ showCommentInput: false, commentFocus: false, commentKeyboardHeight: 0 });
|
||||||
},
|
},
|
||||||
|
|
||||||
submitComment: async function() {
|
submitComment: async function() {
|
||||||
|
if (this.data.commentSending) return;
|
||||||
var text = this.data.commentText.trim();
|
var text = this.data.commentText.trim();
|
||||||
if (!text) { wx.showToast({ title: '请输入评论内容', icon: 'none' }); return; }
|
if (!text) { wx.showToast({ title: '请输入评论内容', icon: 'none' }); return; }
|
||||||
|
this.setData({ commentSending: true });
|
||||||
try {
|
try {
|
||||||
var params = { diary_id: this.data.diaryId, content: text };
|
var params = { diary_id: this.data.diaryId, content: text };
|
||||||
if (this.data.replyTo) {
|
if (this.data.replyTo) {
|
||||||
@@ -166,12 +193,19 @@ Page(withDataCache({
|
|||||||
showCommentInput: false,
|
showCommentInput: false,
|
||||||
replyTo: null,
|
replyTo: null,
|
||||||
commentText: '',
|
commentText: '',
|
||||||
|
commentInitialText: '',
|
||||||
|
commentLength: 0,
|
||||||
|
commentFocus: false,
|
||||||
|
commentKeyboardHeight: 0,
|
||||||
'diary.comment_count': (this.data.diary.comment_count || 0) + 1
|
'diary.comment_count': (this.data.diary.comment_count || 0) + 1
|
||||||
});
|
});
|
||||||
|
wx.hideKeyboard();
|
||||||
wx.showToast({ title: '评论成功', icon: 'success' });
|
wx.showToast({ title: '评论成功', icon: 'success' });
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
wx.showToast({ title: e.error || '评论失败', icon: 'none' });
|
wx.showToast({ title: e.error || '评论失败', icon: 'none' });
|
||||||
|
} finally {
|
||||||
|
this.setData({ commentSending: false });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -71,15 +71,14 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 评论输入弹窗 -->
|
<!-- 评论面板自行跟随键盘,禁用原生页面上推,避免双重位移。 -->
|
||||||
<view class="modal-mask" wx:if="{{showCommentInput}}" catchtap="cancelComment">
|
<view class="comment-compose-mask" wx:if="{{showCommentInput}}" catchtap="cancelComment" catchtouchmove="noop">
|
||||||
<view class="modal-box" catchtap="noop">
|
<view class="comment-compose {{commentKeyboardHeight > 0 ? 'comment-compose--keyboard' : ''}}" style="bottom:{{commentKeyboardHeight}}px;" catchtap="noop">
|
||||||
<view class="modal-top">
|
<view class="comment-compose-heading"><text>{{replyTo ? '回复 @' + replyTo.nickname : '写下你的留言'}}</text><text class="comment-compose-close" catchtap="cancelComment">收起</text></view>
|
||||||
<text class="modal-cancel" catchtap="cancelComment">取消</text>
|
<view class="comment-compose-field">
|
||||||
<text class="modal-title">{{replyTo ? '回复 @' + replyTo.nickname : '发表评论'}}</text>
|
<textarea placeholder-class="handwritten-placeholder" class="comment-compose-input" style="height:{{commentEditorHeight}}px;" placeholder="说点什么,让 TA 知道你的心情…" value="{{commentInitialText}}" bindinput="onCommentInput" bindlinechange="onCommentLineChange" bindkeyboardheightchange="onCommentKeyboardHeight" bindblur="onCommentBlur" focus="{{commentFocus}}" fixed="{{true}}" adjust-position="{{false}}" hold-keyboard="{{true}}" show-confirm-bar="{{false}}" cursor-spacing="0" maxlength="500" disabled="{{commentSending}}" />
|
||||||
<text class="modal-send" catchtap="submitComment">发送</text>
|
|
||||||
</view>
|
</view>
|
||||||
<textarea placeholder-class="handwritten-placeholder" class="modal-input" placeholder="说点什么..." value="{{commentText}}" bindinput="onCommentInput" auto-focus maxlength="500" />
|
<view class="comment-compose-toolbar"><text class="comment-compose-count">{{commentLength}}/500</text><button class="comment-compose-send" catchtap="submitComment" disabled="{{commentLength === 0 || commentSending}}" loading="{{commentSending}}" hover-class="none">发送</button></view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,19 @@
|
|||||||
.comment-box { margin-top:38rpx; }.comment-head { padding-bottom:20rpx; border-bottom:1rpx solid #E9DDD0; }.comment-head-text { color:#352E2A; font-size:28rpx; font-weight:600; }.comment-empty { padding:44rpx 8rpx; text-align:center; font-size:24rpx; line-height:1.7; color:#85776D; }
|
.comment-box { margin-top:38rpx; }.comment-head { padding-bottom:20rpx; border-bottom:1rpx solid #E9DDD0; }.comment-head-text { color:#352E2A; font-size:28rpx; font-weight:600; }.comment-empty { padding:44rpx 8rpx; text-align:center; font-size:24rpx; line-height:1.7; color:#85776D; }
|
||||||
.comment-item { display:flex; align-items:flex-start; gap:14rpx; padding:24rpx 0; border-bottom:1rpx solid #E9DDD0; }.comment-item:last-child { border:0; }.comment-avatar { width:48rpx; height:48rpx; border-radius:50%; flex-shrink:0; }.comment-body { flex:1; min-width:0; }.comment-top { display:flex; justify-content:space-between; gap:12rpx; flex-wrap:wrap; margin-bottom:12rpx; }.comment-name { font-size:24rpx; color:#352E2A; font-weight:500; }.comment-date { font-size:20rpx; color:#A39386; }.comment-content { display:block; font-size:26rpx; line-height:1.65; color:#65594F; overflow-wrap:break-word; }.comment-at { color:#B74768; }
|
.comment-item { display:flex; align-items:flex-start; gap:14rpx; padding:24rpx 0; border-bottom:1rpx solid #E9DDD0; }.comment-item:last-child { border:0; }.comment-avatar { width:48rpx; height:48rpx; border-radius:50%; flex-shrink:0; }.comment-body { flex:1; min-width:0; }.comment-top { display:flex; justify-content:space-between; gap:12rpx; flex-wrap:wrap; margin-bottom:12rpx; }.comment-name { font-size:24rpx; color:#352E2A; font-weight:500; }.comment-date { font-size:20rpx; color:#A39386; }.comment-content { display:block; font-size:26rpx; line-height:1.65; color:#65594F; overflow-wrap:break-word; }.comment-at { color:#B74768; }
|
||||||
.bottom-bar { position:fixed; left:0; right:0; bottom:0; z-index:20; background:#FFFDF8; border-top:1rpx solid #E9DDD0; padding:16rpx 44rpx calc(16rpx + env(safe-area-inset-bottom)); height:calc(100rpx + env(safe-area-inset-bottom)); box-sizing:border-box; }.bottom-input { display:flex; align-items:center; gap:14rpx; height:66rpx; border:1rpx solid #E9DDD0; background:#FAF5EE; padding:0 20rpx; border-radius:16rpx; }.bottom-input-icon { width:30rpx; height:30rpx; }.bottom-input-text { color:#85776D; font-size:25rpx; }
|
.bottom-bar { position:fixed; left:0; right:0; bottom:0; z-index:20; background:#FFFDF8; border-top:1rpx solid #E9DDD0; padding:16rpx 44rpx calc(16rpx + env(safe-area-inset-bottom)); height:calc(100rpx + env(safe-area-inset-bottom)); box-sizing:border-box; }.bottom-input { display:flex; align-items:center; gap:14rpx; height:66rpx; border:1rpx solid #E9DDD0; background:#FAF5EE; padding:0 20rpx; border-radius:16rpx; }.bottom-input-icon { width:30rpx; height:30rpx; }.bottom-input-text { color:#85776D; font-size:25rpx; }
|
||||||
.modal-mask { position:fixed; top:0; left:0; right:0; bottom:0; z-index:1200; display:flex; align-items:flex-end; }.modal-box { width:100%; border-radius:24rpx 24rpx 0 0!important; padding:28rpx 32rpx calc(28rpx + env(safe-area-inset-bottom)); }.modal-top { display:flex; align-items:center; justify-content:space-between; gap:16rpx; margin-bottom:24rpx; }.modal-cancel,.modal-send { padding:12rpx 0; font-size:26rpx; color:#85776D; }.modal-send { color:#B74768; }.modal-title { font-size:28rpx; font-weight:500; color:#352E2A; }.modal-input { width:100%; height:200rpx; background:#FAF5EE; border:1rpx solid #E9DDD0; border-radius:16rpx; padding:20rpx; font-size:27rpx; box-sizing:border-box; }
|
.comment-compose-mask { position:fixed; inset:0; z-index:1200; background:rgba(53,46,42,.24); }
|
||||||
|
.comment-compose { position:fixed; left:0; right:0; padding:22rpx 28rpx calc(20rpx + env(safe-area-inset-bottom)); box-sizing:border-box; border-radius:28rpx 28rpx 0 0; background:#FFFDF8; }
|
||||||
|
.comment-compose--keyboard { padding-bottom:16rpx; }
|
||||||
|
.comment-compose-heading { display:flex; align-items:center; justify-content:space-between; gap:20rpx; margin-bottom:16rpx; font-size:23rpx; line-height:1.5; color:#85776D; }
|
||||||
|
.comment-compose-heading > text:first-child { flex:1; min-width:0; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
|
||||||
|
.comment-compose-close { flex-shrink:0; padding:8rpx 4rpx; color:#B74768; }
|
||||||
|
.comment-compose-field { background:#FFF0F5; border-radius:22rpx; padding:20rpx 24rpx; }
|
||||||
|
.comment-compose-input { display:block; width:100%; font-size:28rpx; line-height:1.6; color:#352E2A; }
|
||||||
|
.comment-compose-toolbar { display:flex; align-items:center; justify-content:space-between; margin-top:14rpx; }
|
||||||
|
.comment-compose-count { font-size:21rpx; color:#A39386; }
|
||||||
|
.comment-compose-send { margin:0; min-width:124rpx; height:64rpx; padding:0 28rpx; border-radius:32rpx; background:#F47FA5; color:#FFF; font-size:25rpx; line-height:64rpx; }
|
||||||
|
.comment-compose-send::after { border:0; }
|
||||||
|
.comment-compose-send[disabled] { background:#F4E6EB; color:#A39386; }
|
||||||
.preview-mask { position:fixed; top:0; left:0; right:0; bottom:0; z-index:9999; background:rgba(0,0,0,.95); }.preview-swiper { width:100%; height:100%; }.preview-slide { position:relative; }.preview-img { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:100%; display:block; }.preview-num { position:fixed; bottom:calc(44rpx + env(safe-area-inset-bottom)); left:0; right:0; text-align:center; color:#FFFDF8; font-size:25rpx; }.preview-close { position:fixed; top:var(--nav-height,64px); right:24rpx; width:72rpx; height:72rpx; display:flex; align-items:center; justify-content:center; }.preview-close-icon { width:34rpx; height:34rpx; filter:brightness(0) invert(1); }
|
.preview-mask { position:fixed; top:0; left:0; right:0; bottom:0; z-index:9999; background:rgba(0,0,0,.95); }.preview-swiper { width:100%; height:100%; }.preview-slide { position:relative; }.preview-img { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:100%; display:block; }.preview-num { position:fixed; bottom:calc(44rpx + env(safe-area-inset-bottom)); left:0; right:0; text-align:center; color:#FFFDF8; font-size:25rpx; }.preview-close { position:fixed; top:var(--nav-height,64px); right:24rpx; width:72rpx; height:72rpx; display:flex; align-items:center; justify-content:center; }.preview-close-icon { width:34rpx; height:34rpx; filter:brightness(0) invert(1); }
|
||||||
@media (max-width:350px) { .read-page { padding-left:32rpx; padding-right:32rpx; }.bottom-bar { padding-left:32rpx; padding-right:32rpx; } }
|
@media (max-width:350px) { .read-page { padding-left:32rpx; padding-right:32rpx; }.bottom-bar { padding-left:32rpx; padding-right:32rpx; } }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const path = require('node:path');
|
||||||
|
const createLoader = require('./load-module.cjs');
|
||||||
|
function setup(api = {}) {
|
||||||
|
let definition; const patches = []; let hidden = 0;
|
||||||
|
createLoader({ Page: value => definition = value, wx: { hideKeyboard: () => hidden++, showToast() {} }, console, Date, Promise }, {
|
||||||
|
'../../utils/page-cache': { withDataCache: value => value },
|
||||||
|
'../../utils/layout': { getLayout: () => ({}) },
|
||||||
|
'../../utils/request': { api, getImageUrl: value => value }
|
||||||
|
})(path.join(__dirname, '../pages/diary_detail/diary_detail.js'));
|
||||||
|
const page = { ...definition, data: { ...definition.data, diaryId: 1, diary: { comment_count: 0 } },
|
||||||
|
setData(patch) { patches.push(patch); for (const [key, value] of Object.entries(patch)) {
|
||||||
|
if (key === 'diary.comment_count') this.data.diary.comment_count = value;
|
||||||
|
else this.data[key] = value;
|
||||||
|
} } };
|
||||||
|
return { page, patches, hidden: () => hidden };
|
||||||
|
}
|
||||||
|
test('composer follows keyboard without rewriting text, and caps long input height', () => {
|
||||||
|
const { page, patches } = setup(); page.onCommentTap(); patches.length = 0;
|
||||||
|
page.onCommentInput({ detail: { value: '字'.repeat(300) } });
|
||||||
|
page.onCommentKeyboardHeight({ detail: { height: 360 } });
|
||||||
|
page.onCommentLineChange({ detail: { height: 400 } });
|
||||||
|
assert.equal(page.data.commentKeyboardHeight, 360);
|
||||||
|
assert.equal(page.data.commentEditorHeight, 160);
|
||||||
|
assert.equal(page.data.commentText.length, 300);
|
||||||
|
assert.ok(patches.every(p => !('commentInitialText' in p) && !('commentText' in p)));
|
||||||
|
page.onCommentKeyboardHeight({ detail: { height: 0 } });
|
||||||
|
assert.equal(page.data.commentKeyboardHeight, 0);
|
||||||
|
});
|
||||||
|
test('closing and reopening preserves draft while changing reply target resets it', () => {
|
||||||
|
const { page } = setup(); page.onCommentTap(); page.onCommentInput({ detail: { value: '未发送' } });
|
||||||
|
page.cancelComment(); page.onCommentTap(); assert.equal(page.data.commentInitialText, '未发送');
|
||||||
|
page.onReplyTap({ currentTarget: { dataset: { item: { id: 2, user_id: 3, nickname: 'TA' } } } });
|
||||||
|
assert.equal(page.data.commentText, ''); assert.equal(page.data.replyTo.id, 2);
|
||||||
|
page.onHide(); assert.equal(page.data.commentKeyboardHeight, 0); assert.equal(page.data.commentFocus, false);
|
||||||
|
});
|
||||||
|
test('failed send preserves draft and allows retry; concurrent send is ignored', async () => {
|
||||||
|
let release; let calls = 0;
|
||||||
|
const { page } = setup({ diaryCommentAdd: () => { calls++; return new Promise((resolve, reject) => release = reject); } });
|
||||||
|
page.onCommentTap(); page.onCommentInput({ detail: { value: '保留这句话' } });
|
||||||
|
const pending = page.submitComment(); await page.submitComment(); assert.equal(calls, 1);
|
||||||
|
release({ error: '网络错误' }); await pending;
|
||||||
|
assert.equal(page.data.commentSending, false); assert.equal(page.data.commentText, '保留这句话');
|
||||||
|
assert.equal(page.data.showCommentInput, true);
|
||||||
|
});
|
||||||
|
test('successful reply sends reply metadata and clears composer', async () => {
|
||||||
|
let params;
|
||||||
|
const { page, hidden } = setup({ diaryCommentAdd: async value => { params = value; return { success: true, comment: { id: 5 } }; } });
|
||||||
|
page.onReplyTap({ currentTarget: { dataset: { item: { id: 2, user_id: 3, nickname: 'TA' } } } });
|
||||||
|
page.onCommentInput({ detail: { value: '回复内容' } }); await page.submitComment();
|
||||||
|
assert.equal(params.parent_id, 2); assert.equal(params.reply_to_user_id, 3);
|
||||||
|
assert.equal(page.data.commentText, ''); assert.equal(page.data.commentKeyboardHeight, 0);
|
||||||
|
assert.equal(page.data.showCommentInput, false); assert.equal(page.data.diary.comment_count, 1);
|
||||||
|
assert.equal(hidden(), 1);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user