| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041 |
- <template>
- <view class="chat-container">
- <!-- 连接状态指示器 -->
- <view class="connection-status" v-if="!isConnected">
- <text class="status-text">{{ reconnectCount > 0 ? '重连中...' : '连接中...' }}</text>
- </view>
- <!-- 聊天消息列表 -->
- <scroll-view
- class="message-list"
- :scroll-top="scrollTop"
- scroll-y
- :scroll-into-view="scrollIntoView"
- :scroll-with-animation="scrollWithAnimation"
- @scrolltoupper="loadMoreMessages"
- >
- <!-- 加载更多 -->
- <view class="load-more" v-if="showLoadMore" @click="loadMoreMessages">
- <text class="load-more-text">加载更多</text>
- </view>
- <view
- class="message-item"
- v-for="(message, index) in messageList"
- :key="index"
- :id="'message-' + message.id"
- >
- <!-- 左侧消息(商家/其他用户) -->
- <view class="message-left" v-if="!message.isMine && !message.shopId">
- <view class="avatar-wrapper">
- <image class="avatar" :src="message.avatar" mode="aspectFill" />
- </view>
- <view class="message-content-wrapper">
- <view class="username" v-if="isMultiUser">{{ message.username }}</view>
- <view class="message-bubble left-bubble">
- <text class="message-text">{{ message.message }}</text>
- </view>
- <view class="message-time">{{ message.time }}</view>
- </view>
- </view>
- <!-- 右侧消息(我的) -->
- <view class="message-right" v-if="message.isMine || message.shopId">
- <view class="message-content-wrapper">
- <view class="username align-right" v-if="isMultiUser">{{ message.username }}</view>
- <view class="message-bubble right-bubble">
- <text class="message-text">{{ message.message }}</text>
- </view>
- <view class="message-time align-right">{{ message.time }}</view>
- </view>
- <view class="avatar-wrapper">
- <image class="avatar" :src="message.avatar" mode="aspectFill" />
- </view>
- </view>
- </view>
- <!-- 空状态 -->
- <view class="empty-state" v-if="messageList.length === 0">
- <text class="empty-text">暂无聊天记录</text>
- </view>
- </scroll-view>
- <!-- 输入区域 -->
- <view class="input-area" :style="{ 'padding-bottom': keyboardHeight > 0 ? keyboardHeight + 'px' : 'calc(20upx + env(safe-area-inset-bottom))' }">
- <view class="input-wrapper">
- <textarea
- class="message-input"
- v-model="inputText"
- placeholder="请输入消息..."
- :placeholder-style="placeholderStyle"
- @confirm="sendMessage"
- @focus="onInputFocus"
- @blur="onInputBlur"
- @input="onInputChange"
- confirm-type="send"
- :adjust-position="false"
- :auto-height="true"
- :maxlength="500"
- :show-confirm-bar="false"
- :focus="inputFocus"
- />
- <button
- class="send-button"
- :class="{ 'send-active': inputText.trim() }"
- @click="sendMessage"
- :disabled="!inputText.trim()"
- >
- 发送
- </button>
- </view>
- </view>
- </view>
- </template>
- <script>
- import { getChatHistory } from '@/api/chat';
- import { WSSHOST } from '@/config.js';
- export default {
- data() {
- return {
- inputText: '',
- scrollTop: 0,
- scrollIntoView: '',
- placeholderStyle: 'color: #ccc; font-size: 28upx;',
- // 是否多人聊天
- isMultiUser: false,
- // 当前用户信息
- currentUser: {
- userId: 0, //客户登录帐户的id(不是 customId)
- staffId: 0, //门店员工id
- customId: 0, //客户id
- shopId: 0, //门店id
- username: '', //门店名称
- avatar: ''
- },
- // 聊天消息列表
- messageList: [],
- // WebSocket相关
- socket: null,
- isConnected: false,
- room: '',
- reconnectCount: 0,
- maxReconnectCount: 3,
- isManualDisconnect: false, // 添加标志位:是否主动断开连接
- scrollWithAnimation: false, // 控制滚动动画
- // 分页加载
- allMessages: [], // 存储所有从API获取的消息
- initialLoadSize: 50, // 首次加载数量
- loadMoreSize: 100, // 每次加载更多的数量
- showLoadMore: false, // 是否显示“加载更多”
- // 键盘相关
- keyboardHeight: 0, // 键盘高度
- isKeyboardShow: false, // 键盘是否显示
- inputFocus: false, // 输入框焦点
- isInputFocused: false, // 标记输入框是否聚焦
- };
- },
- onLoad(options) {
- console.log('---------- options: ', options);
- // 根据传入参数判断是否多人聊天
- if (options.isMultiUser) {
- this.isMultiUser = options.isMultiUser === 'true';
- }
- if (options.userId) {
- this.currentUser.userId = options.userId;
- }
- if (options.customId) {
- this.currentUser.customId = options.customId;
- }
- if (options.shopId) {
- this.currentUser.shopId = options.shopId;
- }
- if (options.name) {
- this.currentUser.username = options.name;
- }
- if (options.staffId) {
- this.currentUser.staffId = options.staffId;
- }
- if (options.avatar) {
- this.currentUser.avatar = options.avatar;
- }
- // 获取房间号
- if (this.currentUser.customId && this.currentUser.shopId) {
- this.room = 'custom_id-' + this.currentUser.customId + 'AND' + 'shop_id-' + this.currentUser.shopId;
- } else {
- // 如果没有传入房间号,使用默认值或生成一个
- this.room = 'default_room';
- }
- console.log('---------- this.room: ', this.room);
- // 加载聊天记录
- this.loadChatHistory();
- // 连接WebSocket
- this.connectWebSocket();
- },
- onReady() {
- // 页面加载完成后滚动到底部
- this.$nextTick(() => {
- this.scrollToBottom();
- });
-
- // 监听键盘弹起事件
- uni.onKeyboardHeightChange((res) => {
- this.keyboardHeight = res.height;
- this.isKeyboardShow = res.height > 0;
-
- // 键盘弹起时,延迟滚动到底部
- if (this.isKeyboardShow) {
- this.$nextTick(() => {
- setTimeout(() => {
- this.scrollToBottom();
- }, 300);
- });
- }
- });
- },
- onUnload() {
- // 页面卸载时关闭WebSocket连接
- this.isManualDisconnect = true; // 标记为主动断开
- this.disconnectWebSocket();
- },
- onHide() {
- // 页面隐藏时可以选择性地关闭连接(根据业务需求)
- // this.disconnectWebSocket()
- },
- onShow() {
- // 页面显示时重新连接(如果之前断开了)
- // if (!this.isConnected && this.room) {
- // this.connectWebSocket();
- // }
- },
- methods: {
- init() {},
- // 加载聊天历史记录
- loadChatHistory() {
- getChatHistory({
- roomName: this.room,
- userId: this.currentUser.id
- }).then((res) => {
- if (res.code == 1 && res.data) {
- this.allMessages = res.data.map((item, index) => ({
- ...item,
- id: `msg_${index}`, // 为每条消息添加唯一ID
- }));
- if (this.allMessages.length > this.initialLoadSize) {
- this.messageList = this.allMessages.slice(
- this.allMessages.length - this.initialLoadSize
- );
- this.showLoadMore = true;
- } else {
- this.messageList = this.allMessages;
- this.showLoadMore = false;
- }
- this.$nextTick(() => {
- this.scrollToBottom(false); // 初始加载时不使用动画
- });
- }
- });
- },
- // 加载更多历史记录
- loadMoreMessages() {
- if (!this.showLoadMore) {
- return;
- }
- const currentLoadedCount = this.messageList.length;
- const remainingMessagesCount = this.allMessages.length - currentLoadedCount;
- if (remainingMessagesCount <= 0) {
- this.showLoadMore = false;
- return;
- }
- const countToLoad = Math.min(this.loadMoreSize, remainingMessagesCount);
- const moreMessages = this.allMessages.slice(
- remainingMessagesCount - countToLoad,
- remainingMessagesCount
- );
- const oldTopMessageId = this.messageList.length > 0 ? this.messageList[0].id : null;
- this.messageList = [...moreMessages, ...this.messageList];
- if (this.allMessages.length - (currentLoadedCount + countToLoad) <= 0) {
- this.showLoadMore = false;
- }
- if (oldTopMessageId) {
- this.$nextTick(() => {
- this.scrollWithAnimation = false; // 加载更多时不使用动画
- this.scrollIntoView = "message-" + oldTopMessageId;
- });
- }
- },
- // 获取主机地址
- getHost() {
- // #ifdef H5
- return WSSHOST;
- // #endif
- // #ifdef MP-WEIXIN || APP-PLUS
- // 小程序和App环境下,需要配置具体的服务器地址
- //return 'ws://192.168.10.182:8080';
- return WSSHOST; // 替换为实际的服务器地址
- // #endif
- },
- // 连接WebSocket
- connectWebSocket() {
- if (!this.room) {
- console.error('房间号不能为空');
- uni.showToast({
- title: '房间号不能为空',
- icon: 'none'
- });
- return;
- }
- try {
- // 构建WebSocket URL
- const wsUrl = `${this.getHost()}/room`;
- // 构建子协议参数(模拟POST body传输)
- const protocols = this.buildWebSocketProtocols();
- console.log('protocols: ', protocols);
- // 创建WebSocket连接
- this.socket = uni.connectSocket({
- url: wsUrl,
- protocols: protocols,
- success: () => {
- console.log('WebSocket连接创建成功');
- },
- fail: (error) => {
- console.error('WebSocket连接创建失败:', error);
- this.handleConnectionError();
- }
- });
- // 监听WebSocket连接打开
- this.socket.onOpen(() => {
- console.log('WebSocket连接已打开');
- this.isConnected = true;
- this.reconnectCount = 0;
- this.isManualDisconnect = false; // 连接成功时重置标志位
- });
- // 监听WebSocket消息
- this.socket.onMessage((event) => {
- this.handleWebSocketMessage(event.data);
- });
- // 监听WebSocket连接关闭
- this.socket.onClose(() => {
- console.log('WebSocket连接已关闭');
- this.isConnected = false;
- // 只有在非主动断开的情况下才尝试重连
- if (!this.isManualDisconnect && this.reconnectCount < this.maxReconnectCount) {
- setTimeout(() => {
- this.reconnectWebSocket();
- }, 3000);
- }
- });
- // 监听WebSocket错误
- this.socket.onError((error) => {
- console.error('WebSocket连接错误:', error);
- this.isConnected = false;
- this.handleConnectionError();
- });
- } catch (error) {
- console.error('创建WebSocket连接异常:', error);
- this.handleConnectionError();
- }
- },
- // 构建WebSocket子协议参数(模拟POST body传输)
- buildWebSocketProtocols() {
- try {
- const userId = parseInt(this.currentUser.userId); //客户登录帐户的id(不是 customId,是customer登录帐户的userId)
- if (isNaN(userId) || userId <= 0) {
- this.$msg("当前用户的id出错");
- return;
- }
-
- const customId = parseInt(this.currentUser.customId);
- if (isNaN(customId) || customId <= 0) {
- this.$msg("当前用户的customId出错");
- return;
- }
-
- const shopId = parseInt(this.currentUser.shopId);
- if (isNaN(shopId) || shopId <= 0) {
- this.$msg("当前用户的shopId出错");
- return;
- }
- const staffId = parseInt(this.currentUser.staffId);
- if (isNaN(staffId) || staffId <= 0) {
- this.$msg("当前用户的staffId出错");
- return;
- }
- // 创建房间参数对象
- const roomParams = {
- u_id: userId, //不是 customId,是customer的: userId
- c_id: parseInt(this.currentUser.customId),
- s_id: parseInt(this.currentUser.shopId),
- u_name: this.currentUser.username && this.currentUser.username.length > 30 ? this.currentUser.username.substring(0, 30) : this.currentUser.username,
- platform: this.getPlatformInfo(),
- type: 'shop', // 客户端类型:shop:门店端, customer:客户端
- staff_id: staffId, //门店员工id
- };
- // 分段传输(如果参数过长)
- const segments = this.createSegmentedProtocols(roomParams);
- if (segments.length == 0) {
- this.$msg('分段传输失败');
- return;
- }
- // 根据调试选项和长度选择合适的方案
- let protocols;
- if (segments.length > 0) {
- protocols = ['chat', ...segments];
- }
- // 验证协议字符串的合法性
- const isValid = this.validateProtocols(protocols);
- console.log('协议验证结果:', isValid);
- if (!isValid) {
- this.$msg('协议验证失败');
- return;
- }
- return protocols;
- } catch (error) {
- console.error('构建WebSocket protocols失败:', error);
- return ['chat']; // 返回基础协议作为fallback
- }
- },
- // 获取平台信息
- getPlatformInfo() {
- // #ifdef MP-WEIXIN
- return 'MP-WEIXIN';
- // #endif
- // #ifdef APP-PLUS
- return 'APP-PLUS';
- // #endif
- // #ifdef MP-ALIPAY
- return 'MP-ALIPAY';
- // #endif
- // #ifdef H5
- return 'H5';
- // #endif
- return 'UNKNOWN';
- },
- // Base64编码方法(跨平台兼容)
- base64Encode(str) {
- // #ifdef H5
- // H5环境使用浏览器原生方法
- try {
- return btoa(str);
- } catch (error) {
- console.error('H5 Base64编码失败:', error);
- return this.customBase64Encode(str);
- }
- // #endif
- // #ifdef MP-WEIXIN || MP-ALIPAY || APP-PLUS
- // 小程序和App环境使用自定义编码
- return this.customBase64Encode(str);
- // #endif
- },
- // 自定义Base64编码实现
- customBase64Encode(str) {
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
- let output = '';
- let chr1, chr2, chr3, enc1, enc2, enc3, enc4;
- let i = 0;
- // 转换为UTF-8字节
- str = this.utf8Encode(str);
- while (i < str.length) {
- chr1 = str.charCodeAt(i++);
- chr2 = str.charCodeAt(i++);
- chr3 = str.charCodeAt(i++);
- enc1 = chr1 >> 2;
- enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
- enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
- enc4 = chr3 & 63;
- if (isNaN(chr2)) {
- enc3 = enc4 = 64;
- } else if (isNaN(chr3)) {
- enc4 = 64;
- }
- output = output + chars.charAt(enc1) + chars.charAt(enc2) + chars.charAt(enc3) + chars.charAt(enc4);
- }
- return output;
- },
- // UTF-8编码
- utf8Encode(str) {
- str = str.replace(/\r\n/g, '\n');
- let utftext = '';
- for (let n = 0; n < str.length; n++) {
- const c = str.charCodeAt(n);
- if (c < 128) {
- utftext += String.fromCharCode(c);
- } else if (c > 127 && c < 2048) {
- utftext += String.fromCharCode((c >> 6) | 192);
- utftext += String.fromCharCode((c & 63) | 128);
- } else {
- utftext += String.fromCharCode((c >> 12) | 224);
- utftext += String.fromCharCode(((c >> 6) & 63) | 128);
- utftext += String.fromCharCode((c & 63) | 128);
- }
- }
- return utftext;
- },
- // URL安全的Base64编码
- urlSafeBase64Encode(str) {
- // 先进行标准Base64编码
- const base64 = this.base64Encode(str);
- // 转换为URL安全格式:替换 +/= 字符
- return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
- },
- // 创建分段协议(用于长参数)- 每次取两个键值对,将参数分成多个小块
- createSegmentedProtocols(roomParams) {
- const segments = [];
- const MAX_SEGMENT_LENGTH = 64; // 每段最大长度
- try {
- // 将参数分成多个小段
- const paramEntries = Object.entries(roomParams);
- const chunks = [];
- // 每次取两个键值对,将参数分成多个小块
- for (let i = 0; i < paramEntries.length; i += 2) {
- const chunk = Object.fromEntries(paramEntries.slice(i, i + 2));
- chunks.push(chunk);
- }
- chunks.forEach((chunk, index) => {
- const chunkStr = JSON.stringify(chunk);
- const encoded = this.urlSafeBase64Encode(chunkStr);
- if (encoded.length <= MAX_SEGMENT_LENGTH) {
- segments.push(`p${index}-${encoded}`);
- }
- });
- return segments;
- } catch (error) {
- console.error('创建分段协议失败:', error);
- return [];
- }
- },
- // 验证协议字符串的合法性
- validateProtocols(protocols) {
- const MAX_SEGMENT_LENGTH = 64 + 30; // WebSocket 子协议名称按照 RFC 6455 规范,建议长度不超过 64 字符
- if (!protocols || !Array.isArray(protocols)) {
- return false;
- }
- for (const protocol of protocols) {
- // 检查协议名称是否符合规范(RFC 6455)
- // 只允许字母、数字、连字符、下划线、点
- if (!/^[a-zA-Z0-9\-._]+$/.test(protocol)) {
- console.error('协议包含非法字符:', protocol);
- return false;
- }
- // 检查长度限制
- if (protocol.length > MAX_SEGMENT_LENGTH) {
- console.error('协议长度超限:', protocol.length);
- return false;
- }
- console.log('protocal -- ', protocol, protocol.length)
- }
- return true;
- },
- // 断开WebSocket连接
- disconnectWebSocket() {
- if (this.socket) {
- this.isManualDisconnect = true; // 标记为主动断开
- this.socket.close();
- this.socket = null;
- this.isConnected = false;
- }
- },
- // 重连WebSocket
- reconnectWebSocket() {
- if (this.reconnectCount < this.maxReconnectCount) {
- this.reconnectCount++;
- console.log(`正在尝试第${this.reconnectCount}次重连...`);
- // uni.showToast({
- // title: `重连中(${this.reconnectCount}/${this.maxReconnectCount})`,
- // icon: 'loading',
- // duration: 1500
- // });
- this.connectWebSocket();
- } else {
- uni.showToast({
- title: '连接失败,请检查网络',
- icon: 'none',
- duration: 3000
- });
- }
- },
- // 处理 WebSocket 消息
- handleWebSocketMessage(data) {
- console.log('---------- data: ', data);
- try {
- // 尝试解析JSON,如果失败则认为是纯文本消息
- let messageData;
- try {
- const temp = JSON.parse(data);
- const msg = temp.message ? temp.message : temp; // TODO 测试兼容
- messageData = JSON.parse(msg);
- } catch (parseError) {
- // 如果不是JSON格式,可能是纯文本消息,暂时忽略或创建简单消息对象
- console.log('收到非JSON格式消息:', data);
- try {
- messageData = JSON.parse(data);
- } catch (error) {
- console.error('解析WebSocket消息失败:', error, data);
- }
- }
- let isMine = false;
- // 检查是否是本店但是他人发送的消息
- const messageShopId = parseInt(messageData.shopId);
- const currentShopId = parseInt(this.currentUser.shopId);
- const isShopMessage = messageShopId === currentShopId && messageShopId > 0;
- if (isShopMessage) {
- isMine = true;
- }
- // 创建消息对象
- const newMessage = {
- id: 'msg_receive_' + Math.floor(Date.now() / 1000) + '_' + Math.floor(Math.random() * 100000) + 1,
- isMine: isMine, // 标记是否是本店发送的消息
- message: messageData.message || messageData || '',
- username: messageData.name || messageData.username || '未知用户',
- avatar: messageData.avatar || this.$constant.imgUrl + '/retail/default-img.png',
- time: this.getCurrentTime(),
- };
- console.log('收到 ---------- newMessage: ', newMessage);
- // 添加到消息列表
- this.messageList.push(newMessage);
- // 滚动到底部
- this.$nextTick(() => {
- this.scrollToBottom(); // 用户操作时使用动画
- });
- } catch (error) {
- console.error('解析WebSocket消息失败:', error, data);
- }
- },
- // 发送消息
- sendMessage() {
- if (!this.inputText.trim()) {
- return;
- }
- if (!this.isConnected) {
- uni.showToast({
- title: 'WebSocket未连接',
- icon: 'none'
- });
- return;
- }
- const messageContent = this.inputText.trim();
- const avatar = this.currentUser.avatar || this.$constant.imgUrl + '/retail/default-img.png';
- const timestamp = Math.floor(Date.now() / 1000);
- try {
- // 立即在本地显示发送的消息(右侧)
- const myMessage = {
- id: 'msg_local_' + timestamp + '_' + Math.floor(Math.random() * 100000) + 1,
- isMine: true,
- message: messageContent,
- username: this.currentUser.username,
- avatar: avatar,
- time: this.getCurrentTime(),
- };
- // 添加到消息列表
- this.messageList.push(myMessage);
- // 清空输入框
- this.inputText = '';
- // 重新聚焦
- this.$nextTick(() => {
- this.inputFocus = true
- })
- // 滚动到底部
- // this.$nextTick(() => {
- // this.scrollToBottom(); // 用户操作时使用动画
- // });
- // 构建要发送的消息数据
- const messageData = {
- receiver: this.currentUser.customId,
- message: messageContent,
- username: this.currentUser.username,
- avatar: avatar,
- shopId: this.currentUser.shopId, // 发送者:门店使用shopId,客户使用customId
- timestamp: timestamp
- };
- const json = JSON.stringify(messageData);
- console.log('send ---------- json: ', json);
- // 发送到WebSocket服务器
- this.socket.send({
- data: json,
- success: () => {
- console.log('消息发送成功');
- },
- fail: (error) => {
- console.error('消息发送失败:', error);
- uni.showToast({
- title: '发送失败',
- icon: 'none'
- });
- // 发送失败时,可以考虑移除刚添加的消息或标记为失败
- }
- });
- } catch (error) {
- console.error('发送消息异常:', error);
- uni.showToast({
- title: '发送异常',
- icon: 'none'
- });
- }
- },
- // 处理连接错误
- handleConnectionError() {
- uni.showToast({
- title: '连接失败',
- icon: 'none',
- duration: 2000
- });
- },
- // 滚动到底部
- scrollToBottom(enableAnimation = true) {
- this.scrollWithAnimation = enableAnimation;
- if (this.messageList.length > 0) {
- this.scrollIntoView = "message-" + this.messageList[this.messageList.length - 1].id;
- }
- },
- // 获取当前时间
- getCurrentTime() {
- const now = new Date();
- const hours = String(now.getHours()).padStart(2, '0');
- const minutes = String(now.getMinutes()).padStart(2, '0');
- return `${hours}:${minutes}`;
- },
- // 输入框聚焦事件
- onInputFocus() {
- // console.log('输入框聚焦');
- // // 微信小程序中,聚焦时延迟滚动到底部
- // setTimeout(() => {
- // this.scrollToBottom();
- // }, 300);
- this.isInputFocused = true
- this.inputFocus = true
- this.hideExtraPanels() // 聚焦时隐藏表情和更多面板
- },
- // 输入框失焦事件
- onInputBlur() {
- // console.log('输入框失焦');
- this.isInputFocused = false
- this.inputFocus = false
- },
- // 输入内容变化事件
- onInputChange(event) {
- this.inputText = event.detail.value
- // 可以在这里处理输入内容的变化,比如限制行数等
- const value = event.detail.value;
- // 限制最大行数为5行,避免输入框过高
- const lines = value.split('\n');
- if (lines.length > 5) {
- // 截取前5行
- this.inputText = lines.slice(0, 5).join('\n');
- }
- },
- }
- };
- </script>
- <style lang="scss" scoped>
- .chat-container {
- height: 100vh;
- display: flex;
- flex-direction: column;
- background-color: #f5f5f5;
- }
- .connection-status {
- background-color: #ff9800;
- color: #ffffff;
- text-align: center;
- padding: 16upx;
- font-size: 24upx;
- .status-text {
- color: #ffffff;
- }
- }
- .message-list {
- flex: 1;
- padding: 20upx;
- overflow-y: auto;
- }
- .load-more {
- text-align: center;
- padding: 20upx 0;
- .load-more-text {
- color: #007aff;
- font-size: 28upx;
- cursor: pointer;
- }
- }
- .message-item {
- margin-bottom: 30upx;
- &:last-child {
- margin-bottom: 0;
- }
- }
- .message-left {
- display: flex;
- justify-content: flex-start;
- align-items: flex-start;
- }
- .message-right {
- display: flex;
- justify-content: flex-end;
- align-items: flex-start;
- }
- .avatar-wrapper {
- flex-shrink: 0;
- margin: 0 20upx;
- }
- .avatar {
- width: 80upx;
- height: 80upx;
- border-radius: 50%;
- background-color: #e0e0e0;
- }
- .message-content-wrapper {
- max-width: 520upx;
- min-width: 100upx;
- }
- .username {
- font-size: 24upx;
- color: $fontColor3;
- margin-bottom: 8upx;
- &.align-right {
- text-align: right;
- }
- }
- .message-bubble {
- padding: 20upx 24upx;
- border-radius: 16upx;
- position: relative;
- word-wrap: break-word;
- word-break: break-all;
- &.left-bubble {
- background-color: #ffffff;
- margin-left: 0;
- border-top-left-radius: 8upx;
- &::before {
- content: '';
- position: absolute;
- left: -22upx;
- top: 30upx;
- width: 0;
- height: 0;
- border: 12upx solid transparent;
- border-right-color: #ffffff;
- z-index: 999999;
- }
- }
- &.right-bubble {
- background-color: rgb(27, 193, 66);
- margin-right: 0;
- border-top-right-radius: 8upx;
- &::before {
- content: '';
- position: absolute;
- right: -22upx;
- top: 30upx;
- width: 0;
- height: 0;
- border: 12upx solid transparent;
- border-left-color: rgb(27, 193, 66);
- z-index: 999999;
- }
- .message-text {
- color: #ffffff;
- }
- }
- }
- .message-text {
- font-size: 32upx;
- line-height: 1.4;
- color: $fontColorMain;
- }
- .message-time {
- font-size: 20upx;
- color: $fontColor3;
- margin-top: 8upx;
- &.align-right {
- text-align: right;
- }
- }
- .empty-state {
- display: flex;
- justify-content: center;
- align-items: center;
- height: 300upx;
- }
- .empty-text {
- font-size: 32upx;
- color: $fontColor3;
- }
- .input-area {
- background-color: #ffffff;
- padding: 26upx;
- // margin-bottom: 20upx;
- border-top: 1upx solid $borderColor;
- // 适配安全区域和键盘高度(通过内联样式动态设置)
- transition: padding-bottom 0.3s ease;
- }
- .input-wrapper {
- display: flex;
- align-items: center;
- background-color: #f8f8f8;
- border-radius: 40upx;
- padding: 14upx 18upx 14upx 18upx;
- margin-bottom: 30upx;
- }
- .message-input {
- flex: 1;
- font-size: 32upx;
- border: none;
- background-color: transparent;
- outline: none;
- min-height: 28upx;
- max-height: 140upx;
- line-height: 1.4;
- word-wrap: break-word;
- word-break: break-all;
- resize: none;
- &::placeholder {
- color: #ccc;
- }
- }
- .send-button {
- margin-left: 20upx;
- padding: 10upx 30upx;
- background-color: $fontColor4;
- color: #ffffff;
- border: 1px solid #c9c9c9;
- border-radius: 30upx;
- font-size: 28upx;
- transition: all 0.3s ease;
- &.send-active {
- background-color: $mainColor;
- }
- &:disabled {
- background-color: $fontColor4;
- color: #ffffff;
- }
- &::after {
- border: none;
- }
- }
- // 适配不同设备
- /* #ifdef H5 */
- .chat-container {
- height: calc(100vh - 44px); // 减去导航栏高度
- }
- /* #endif */
- /* #ifdef MP-WEIXIN */
- .input-area {
- // 微信小程序底部安全区域
- padding-bottom: calc(20upx + env(safe-area-inset-bottom));
- }
- /* #endif */
- /* #ifdef APP-PLUS */
- .input-area {
- // App端底部安全区域
- padding-bottom: calc(20upx + env(safe-area-inset-bottom));
- }
- /* #endif */
- </style>
|