收藏
回答

求助:小程序内聊天页面bug如何修复?

个人利用workbuddy开发小程序,在苹果、安卓、鸿蒙三端真机调试,聊天页面无法同时实现以下功能:

需求 1:进入聊天页面,输入框区块在底部完整显示且不遮挡最后一条信息

需求 2:最新消息实时刷新显示在输入框上面

需求 3:键盘弹起后,键盘与输入框紧密贴合不重叠

需求 4:点击发送按钮后键盘常驻

需求 5:输入框内容多时换行

目前代码如下

chat.js

var api = require('../../../utils/api');

var app = getApp();


var POLL_INTERVAL = 5000;

var pollTimer = null;


var DEFAULT_AVATAR = '/images/default-avatar.png';


Page({

data: {

convId: '',

toUserId: '',

otherAvatar: DEFAULT_AVATAR,

otherName: '',

myAvatar: DEFAULT_AVATAR,

messages: [],

loading: true,

inputText: '',

inputFocus: false,

scrollTop: 0,


/* === 键盘适配变量 === */

windowHeight: 0,

safeAreaBottom: 0,

keyboardHeight: 0, /* container padding-bottom = keyboardHeight,推动输入栏到键盘上方 */

scrollPadding: 140, /* scroll-view 内部底部留白,防止最后消息被输入栏遮挡 */

inputBarPadBottom: 0, /* 输入栏 padding-bottom,无键盘时含 safe-area */

inputBarHeight: 0, /* 输入栏实际高度(onReady 测量) */

},


/* 内部状态(不进 data) */

_lastKBHeight: -1,

_lastKBTime: 0,

_pendingFocus: false,

_kbFallbackTimer: null,


onLoad: function (options) {

var self = this;


/* 获取屏幕信息 */

var sys = wx.getWindowInfo();

var safeBottom = sys.screenHeight - sys.safeArea.bottom;

var wh = sys.windowHeight;


console.log('[chat] windowHeight=' + wh + ' safeBottom=' + safeBottom);


self.setData({

windowHeight: wh,

safeAreaBottom: safeBottom,

scrollPadding: 140,

inputBarPadBottom: safeBottom,

});


/* 注册全局键盘高度监听(兜底,组件事件可能不触发) */

wx.onKeyboardHeightChange(self._onGlobalKBChange.bind(self));


var convId = options.convId || '';

var toUserId = options.toUserId || '';


var userInfo = app.globalData.userInfo || {};

var myAvatar = self._safeAvatar(userInfo.avatar);


self.setData({

convId: convId,

toUserId: toUserId,

myAvatar: myAvatar,

});


if (convId) {

self.loadConvInfo(convId);

self.loadMessages();

self.startPoll();

} else if (toUserId) {

self.createOrGetConv(toUserId);

}

},


onReady: function () {

var self = this;

/* 测量输入栏实际高度 */

setTimeout(function () {

wx.createSelectorQuery().select('.input-bar').boundingClientRect(function (rect) {

if (rect && rect.height > 0) {

console.log('[chat] inputBar measured height=' + rect.height);

var pad = rect.height + self.data.safeAreaBottom + 10;

self.setData({

inputBarHeight: rect.height,

scrollPadding: Math.max(pad, 140),

});

}

}).exec();

}, 800);

},


onUnload: function () {

this.stopPoll();

this._clearFallback();

wx.offKeyboardHeightChange(this._onGlobalKBChange);

},


onHide: function () {

this.stopPoll();

this._clearFallback();

this._lastKBHeight = -1;

this._lastKBTime = 0;

},


onShow: function () {

if (this.data.convId) {

this.startPoll();

}

},


_clearFallback: function () {

if (this._kbFallbackTimer) {

clearTimeout(this._kbFallbackTimer);

this._kbFallbackTimer = null;

}

this._pendingFocus = false;

},


_safeAvatar: function (avatar) {

if (!avatar) return DEFAULT_AVATAR;

if (avatar.indexOf('data:image') === 0 || avatar.indexOf('data:') === 0) return DEFAULT_AVATAR;

return avatar;

},


/* ========== 键盘高度处理(核心适配逻辑) ========== */


onKeyboardHeightChange: function (e) {

var h = (e.detail && typeof e.detail.height === 'number') ? e.detail.height : -1;

if (h < 0 && typeof e.height === 'number') h = e.height;

this._handleKBChange(h, 'component');

},


_onGlobalKBChange: function (res) {

var h = (typeof res.height === 'number') ? res.height : -1;

if (h < 0 && res.detail && typeof res.detail.height === 'number') h = res.detail.height;

this._handleKBChange(h, 'global');

},


_handleKBChange: function (h, source) {

/* 收到真实事件,取消 fallback */

this._clearFallback();


if (typeof h !== 'number') h = 0;


/* 时间窗口去重:同一高度 300ms 内只处理一次 */

var now = Date.now();

if (h === this._lastKBHeight && now - this._lastKBTime < 300) {

return;

}

this._lastKBHeight = h;

this._lastKBTime = now;


console.log('[chat] KB change (src=' + source + '): height=' + h);


var self = this;

var barHeight = self.data.inputBarHeight || 100;


if (h > 0) {

/* 键盘弹出:container padding-bottom = keyboardHeight,输入栏自然位于键盘上方 */

self.setData({

keyboardHeight: h,

scrollPadding: Math.max(barHeight + 20, 100),

inputBarPadBottom: 0, /* 键盘已覆盖安全区,无需额外 padding */

});

} else {

/* 键盘收起:恢复原始状态 */

var pad = barHeight + self.data.safeAreaBottom + 10;

self.setData({

keyboardHeight: 0,

scrollPadding: Math.max(pad, 140),

inputBarPadBottom: self.data.safeAreaBottom,

});

}


/* 延迟滚动到底部,等布局稳定 */

setTimeout(function () {

self.scrollToBottom();

}, 300);

},


/* ========== 焦点事件 ========== */


onFocus: function (e) {

console.log('[chat] focus');


var self = this;


/* 启动 fallback 计时器:400ms 内无键盘事件则估算 */

self._pendingFocus = true;

self._kbFallbackTimer = setTimeout(function () {

if (self._pendingFocus) {

self._pendingFocus = false;

var estimate = Math.round(self.data.windowHeight * 0.42);

console.warn('[chat] KB height fallback estimate: ' + estimate + 'px (event did not fire)');

self._handleKBChange(estimate, 'fallback');

}

}, 400);


/* 延迟滚动到底部 */

setTimeout(function () {

self.scrollToBottom();

}, 350);

},


onBlur: function () {

console.log('[chat] blur');

this._clearFallback();

},


/* ========== 发送消息(含键盘常驻逻辑) ========== */


sendMessage: function () {

var self = this;

var content = self.data.inputText.trim();

if (!content || !self.data.convId) return;


console.log('[chat] sendMessage:', content);


var tempId = 'temp_' + Date.now();

var tempMsg = {

id: tempId,

senderId: 'me',

content: content,

isMine: true,

createdAt: new Date().toISOString(),

};


var messages = self.data.messages.concat([tempMsg]);

self.setData({ messages: messages, inputText: '' });

self.scrollToBottom();


/* 强制 refocus:false→true 确保键盘常驻 */

self.setData({ inputFocus: false });

setTimeout(function () {

self.setData({ inputFocus: true });

}, 200);


api.request({

url: '/messages/message',

method: 'POST',

data: { conversationId: self.data.convId, content: content },

showLoading: false

}).then(function (res) {

var data = res.data || {};

var msgs = self.data.messages;

for (var i = 0; i < msgs.length; i++) {

if (msgs[i].id === tempId) {

msgs[i].id = data.id;

msgs[i].createdAt = data.createdAt;

break;

}

}

msgs = self.formatMessages(msgs);

self.setData({ messages: msgs });

}).catch(function () { });

},


onConfirm: function () {

this.sendMessage();

},


onInput: function (e) {

this.setData({ inputText: e.detail.value });

},


onLineChange: function (e) {

this.scrollToBottom();

},


/* ========== 消息操作 ========== */


createOrGetConv: function (toUserId, transactionId) {

var self = this;

api.request({

url: '/messages/conversations',

method: 'POST',

data: { toUserId: toUserId, transactionId: transactionId || null },

showLoading: false

}).then(function (res) {

var data = res.data || {};

var conv = data;

var otherAvatar = self._safeAvatar(conv.otherUser && conv.otherUser.avatar);

self.setData({

convId: conv.id,

otherAvatar: otherAvatar,

otherName: (conv.otherUser && conv.otherUser.nickname) || '',

});

wx.setNavigationBarTitle({ title: self.data.otherName || '聊天' });

self.loadMessages();

self.markRead(conv.id);

self.startPoll();

});

},


loadConvInfo: function (convId) {

var self = this;

api.request({

url: '/messages/conversations',

method: 'GET',

data: { page: 1, limit: 100 },

showLoading: false

}).then(function (res) {

var items = (res.data && res.data.items) || [];

for (var i = 0; i < items.length; i++) {

if (items[i].id === convId) {

var conv = items[i];

var otherAvatar = self._safeAvatar(conv.otherUser && conv.otherUser.avatar);

self.setData({

otherAvatar: otherAvatar,

otherName: (conv.otherUser && conv.otherUser.nickname) || '',

});

wx.setNavigationBarTitle({ title: self.data.otherName || '聊天' });

self.markRead(convId);

break;

}

}

});

},


loadMessages: function () {

var self = this;

self.setData({ loading: true });

api.request({

url: '/messages/messages/' + self.data.convId,

method: 'GET',

data: { page: 1, limit: 100 },

showLoading: false

}).then(function (res) {

var items = (res.data && res.data.items) || [];

items = self.formatMessages(items);

self.setData({ messages: items, loading: false });

self.scrollToBottom();

}).catch(function () {

self.setData({ loading: false });

});

},


markRead: function (convId) {

api.request({

url: '/messages/messages/read/' + convId,

method: 'PUT',

data: {},

showLoading: false

});

},


/* ========== 滚动控制 ========== */


scrollToBottom: function () {

this.setData({ scrollTop: 99999 });

},


/* ========== 轮询新消息 ========== */


startPoll: function () {

this.stopPoll();

var self = this;

pollTimer = setInterval(function () {

if (!self.data.convId) return;

api.request({

url: '/messages/messages/' + self.data.convId,

method: 'GET',

data: { page: 1, limit: 100 },

showLoading: false

}).then(function (res) {

var items = (res.data && res.data.items) || [];

items = self.formatMessages(items);

var oldLast = self.data.messages.length > 0 ? self.data.messages[self.data.messages.length - 1].id : '';

var newLast = items.length > 0 ? items[items.length - 1].id : '';

if (items.length !== self.data.messages.length || oldLast !== newLast) {

self.setData({ messages: items });

self.markRead(self.data.convId);

self.scrollToBottom();

}

});

}, POLL_INTERVAL);

},


stopPoll: function () {

if (pollTimer) {

clearInterval(pollTimer);

pollTimer = null;

}

},


formatMessages: function (messages) {

if (!messages || messages.length === 0) return [];

var userId = (app.globalData.userInfo && app.globalData.userInfo.id) || '';

var prevTime = null;

return messages.map(function (msg) {

var isMine = (typeof msg.isMine === 'boolean') ? msg.isMine : (msg.senderId === userId);

var showTime = false;

var timeText = '';

if (msg.createdAt) {

var d = new Date(msg.createdAt);

var key = d.getFullYear() + '-' + d.getMonth() + '-' + d.getDate();

if (key !== prevTime) {

showTime = true;

timeText = formatChatTime(d);

prevTime = key;

}

}

return {

id: msg.id,

senderId: msg.senderId,

content: msg.content,

isMine: isMine,

showTime: showTime,

timeText: timeText,

createdAt: msg.createdAt,

};

});

},


});


function formatChatTime(d) {

var now = new Date();

var today = new Date(now.getFullYear(), now.getMonth(), now.getDate());

var target = new Date(d.getFullYear(), d.getMonth(), d.getDate());


if (target.getTime() === today.getTime()) {

return padZero(d.getHours()) + ':' + padZero(d.getMinutes());

}


var yesterday = new Date(today.getTime() - 86400000);

if (target.getTime() === yesterday.getTime()) {

return '\u6628\u5929 ' + padZero(d.getHours()) + ':' + padZero(d.getMinutes());

}


var diff = today.getTime() - target.getTime();

if (diff < 7 * 86400000) {

var days = ['\u5468\u65e5', '\u5468\u4e00', '\u5468\u4e8c', '\u5468\u4e09', '\u5468\u56db', '\u5468\u4e94', '\u5468\u516d'];

return days[d.getDay()] + ' ' + padZero(d.getHours()) + ':' + padZero(d.getMinutes());

}


return (d.getMonth() + 1) + '-' + d.getDate() + ' ' + padZero(d.getHours()) + ':' + padZero(d.getMinutes());

}


function padZero(n) {

return n < 10 ? '0' + n : String(n);

}

chat.json

{

"usingComponents": {

"t-icon": "tdesign-miniprogram/icon/icon",

"t-loading": "tdesign-miniprogram/loading/loading"

},

"navigationBarTitleText": "聊天"

}


chat.wxml

<view class="container" style="padding-bottom: {{keyboardHeight}}px;">

<scroll-view

class="msg-scroll"

scroll-y

scroll-top="{{scrollTop}}"

scroll-with-animation

enhanced="{{true}}"

bounces="{{false}}"

>

<t-loading wx:if="{{loading}}" theme="circular" size="40rpx" loading style="margin-top: 80rpx;" />


<block wx:for="{{messages}}" wx:key="id">

<view class="time-divider" wx:if="{{item.showTime}}">

<text class="time-text">{{item.timeText}}</text>

</view>


<view class="msg-row {{item.isMine ? 'msg-mine' : 'msg-other'}}">

<image class="msg-avatar avatar-left {{item.isMine ? 'avatar-hidden' : ''}}" src="{{otherAvatar}}" mode="aspectFill" />

<view class="bubble {{item.isMine ? 'bubble-mine' : 'bubble-other'}}">

<text class="bubble-text">{{item.content}}</text>

</view>

<image class="msg-avatar avatar-right {{!item.isMine ? 'avatar-hidden' : ''}}" src="{{myAvatar}}" mode="aspectFill" />

</view>

</block>


<!-- 底部留白:确保最后一条消息不被输入栏遮挡 -->

<view style="height: {{scrollPadding}}px;"></view>

</scroll-view>


<!-- 底部输入栏:在 flex 容器内,container padding-bottom 推动其位于键盘上方 -->

<view class="input-bar" style="padding-bottom: {{inputBarPadBottom}}px;">

<view class="input-wrap">

<textarea

class="msg-textarea"

placeholder="输入消息..."

value="{{inputText}}"

focus="{{inputFocus}}"

hold-keyboard="{{true}}"

adjust-position="{{false}}"

auto-height="{{true}}"

disable-default-padding="{{true}}"

show-confirm-bar="{{false}}"

confirm-type="send"

confirm-hold="{{true}}"

maxlength="-1"

bindinput="onInput"

bindconfirm="onConfirm"

bindkeyboardheightchange="onKeyboardHeightChange"

bindlinechange="onLineChange"

bindblur="onBlur"

bindfocus="onFocus"

/>

</view>

<view class="send-btn {{inputText ? 'send-active' : ''}}" catchtap="sendMessage">

<text class="send-text">发送</text>

</view>

</view>

</view>

chat.wxss

page {

height: 100vh;

overflow: hidden;

}


/* ---- 主容器:flex 纵向 + 动态 padding-bottom ---- */

.container {

display: flex;

flex-direction: column;

height: 100vh;

box-sizing: border-box; /* padding-bottom 计入 height,不撑高 */

background: #F5F5F5;

}


/* ---- 消息滚动区 ---- */

.msg-scroll {

flex: 1;

min-height: 0;

width: 100%;

box-sizing: border-box;

padding: 20rpx 0;

}


/* ---- 时间分隔 ---- */

.time-divider {

display: flex;

justify-content: center;

margin: 24rpx 0;

}


.time-text {

font-size: 22rpx;

color: #BBBBBB;

background: rgba(0, 0, 0, 0.04);

padding: 6rpx 20rpx;

border-radius: 16rpx;

}


/* ---- 消息行 ---- */

.msg-row {

display: flex;

align-items: flex-start;

gap: 16rpx;

margin-bottom: 24rpx;

padding: 0 24rpx;

}


.msg-mine {

justify-content: flex-end;

}


.msg-other {

justify-content: flex-start;

}


.msg-avatar {

width: 72rpx;

height: 72rpx;

border-radius: 50%;

background: #E0E0E0;

flex-shrink: 0;

}


.avatar-hidden {

display: none !important;

}


.bubble {

max-width: calc(100% - 72rpx - 16rpx);

padding: 20rpx 24rpx;

border-radius: 20rpx;

word-break: break-all;

}


.bubble-other {

background: #fff;

border-top-left-radius: 6rpx;

}


.bubble-mine {

background: #C0392B;

border-top-right-radius: 6rpx;

}


.bubble-text {

font-size: 28rpx;

line-height: 1.6;

}


.bubble-other .bubble-text {

color: #333;

}


.bubble-mine .bubble-text {

color: #fff;

}


/* ---- 底部输入栏(在 flex 容器内) ---- */

.input-bar {

display: flex;

align-items: flex-end;

gap: 16rpx;

padding: 16rpx 24rpx;

background: #fff;

border-top: 1rpx solid #F0F0F0;

flex-shrink: 0;

/* padding-bottom 通过 inline style 动态设置(safe-area / 键盘) */

}


.input-wrap {

flex: 1;

background: #F5F5F5;

border-radius: 36rpx;

padding: 16rpx 24rpx;

min-height: 80rpx;

max-height: 240rpx;

overflow: hidden;

box-sizing: border-box;

}


.msg-textarea {

width: 100%;

min-height: 48rpx;

max-height: 200rpx;

font-size: 28rpx;

line-height: 44rpx;

color: #333;

padding: 0;

box-sizing: border-box;

}


.send-btn {

flex-shrink: 0;

padding: 18rpx 40rpx;

background: #E0E0E0;

border-radius: 36rpx;

transition: all 0.2s;

margin-bottom: 4rpx;

}


.send-btn.send-active {

background: #C0392B;

}


.send-btn:active {

opacity: 0.8;

}


.send-text {

font-size: 28rpx;

color: #fff;

font-weight: 500;

}



回答关注问题邀请回答
收藏

2 个回答

登录 后发表内容