| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619 |
- <template>
- <view class="chat-container">
- <!-- 连接状态指示器 -->
- <view class="connection-status" v-if="!isConnected && !isManualDisconnect">
- <text class="status-text">连接已断开,请退出重新进入</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">
- <view class="avatar-wrapper">
- <image class="avatar" :src="formatAvatarUrl(message.avatar)" mode="aspectFill" />
- </view>
- <view v-if="message.messageType == 'text' || message.messageType != 'goods'" class="message-content-wrapper">
- <view class="message-bubble left-bubble">
- <text class="message-text">{{ message.message }}</text>
- </view>
- <view class="message-time">{{ message.time }}</view>
- </view>
- <view v-if="message.messageType == 'goods'" class="goods-content-wrapper">
- <view class="goods-info left-bubble">
- <view class="goods-main" @click="toGoodsDetail(message.goodsInfo)">
- <image class="goods-image" :src="message.goodsInfo.goodsImg" mode="aspectFill"></image>
- <view class="goods-details">
- <view class="goods-name-wrapper">
- <text class="goods-name">{{ message.goodsInfo.goodsName }}</text>
- </view>
- <!-- <view class="sales-info">
- <text class="delivery-time">48小时发</text>
- </view> -->
- <text v-if="message.goodsInfo.goodsPriceType == 1 || message.goodsInfo.goodsPriceType == 2" class="goods-price">¥{{ parseFloat(message.goodsInfo.goodsPrice).toFixed(2) }}</text>
- <text v-if="message.goodsInfo.resourcePrice && message.goodsInfo.resourcePrice != parseFloat(message.goodsInfo.goodsPrice).toFixed(2)" class="goods-price--secondary">原先 ¥{{ message.goodsInfo.resourcePrice }}</text>
- </view>
- </view>
- <view class="goods-footer" v-if="message.goodsInfo.goodsPriceType != 1"> </view>
- <view v-if="message.goodsInfo.goodsPriceType !== 2 && isExpired(message.timestamp)" class="action-buttons">
- <form @submit.prevent="createQrotedPrice(message)">
- <input
- class="price-input"
- type="digit"
- v-model="message.quotedPrice"
- placeholder="输入价格"
- @focus="hideKeyboard"
- @blur="showKeyboard"
- />
- <button class="action-btn buy-btn" form-type="submit">改价</button>
- </form>
- </view>
- <view v-else>
- <text v-if="message.goodsInfo.goodsPriceType == 0">改价窗口期已过</text>
- </view>
- <!-- 改价类型显示重新改价 -->
- <view v-if="message.goodsInfo.goodsPriceType == 2 && isExpired(message.timestamp)" class="action-buttons">
- <form @submit.prevent="createQrotedPrice(message)">
- <!-- <text class="goods-price" v-if="message.goodsInfo.lastPrice || message.goodsInfo.goodsPrice"
- >¥{{ parseFloat(message.goodsInfo.lastPrice || message.goodsInfo.goodsPrice).toFixed(2) }}</text
- > -->
- <input
- class="price-input"
- type="digit"
- v-model="message.quotedPrice"
- placeholder="输入价格"
- @focus="hideKeyboard"
- @blur="showKeyboard"
- />
- <button class="action-btn buy-btn" form-type="submit">重新改价</button>
- </form>
- </view>
- </view>
- <view class="message-time">{{ message.time }}</view>
- </view>
- </view>
- <!-- 右侧消息(我的) -->
- <view class="message-right" v-if="message.isMine">
- <view v-if="message.messageType == 'text' || message.messageType != 'goods'" class="message-content-wrapper">
- <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 v-if="message.messageType == 'goods'" class="goods-content-wrapper">
- <view class="goods-info right-bubble">
- <view class="goods-main" @click="toGoodsDetail(message.goodsInfo)">
- <image class="goods-image" :src="message.goodsInfo.goodsImg" mode="aspectFill" />
- <view class="goods-details">
- <view class="goods-name-wrapper">
- <text class="goods-name">{{ message.goodsInfo.goodsName }}</text>
- </view>
- <text v-if="message.goodsInfo.goodsPriceType == 1 || message.goodsInfo.goodsPriceType == 2" class="goods-price">¥{{ parseFloat(message.goodsInfo.goodsPrice).toFixed(2) }}</text>
- <view class="sales-info">
- <!-- <text class="delivery-time">48小时发</text> -->
- </view>
- <text v-if="message.goodsInfo.goodsPriceType == 1" class="goods-price"
- >¥{{ parseFloat(message.goodsInfo.goodsPrice).toFixed(2) }}</text
- >
- </view>
- </view>
- <!-- <view class="goods-footer">
- <text class="footer-text">破损包退·极速退款·7天无理由退货</text>
- <text class="arrow">></text>
- </view> -->
- <!--<view class="action-buttons">
- <button class="action-btn spec-btn" @click="toGoodsDetail(message.goodsInfo)">查看商品</button>
- <button v-if="message.goodsInfo.goodsPriceType == 1" class="action-btn buy-btn" @click="toBuy">去购买</button>
- //改价类型显示去购买
- <button v-if="message.goodsInfo.goodsPriceType == 2" class="action-btn buy-btn" @click="toBuy">去购买</button>
- </view> -->
- </view>
- <view class="message-time align-right">{{ message.time }}</view>
- </view>
- <view class="avatar-wrapper">
- <image class="avatar" :src="formatAvatarUrl(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 v-show="msgInputShow" 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"
- @input="onInputChange"
- confirm-type="send"
- :adjust-position="false"
- :auto-height="true"
- :maxlength="500"
- :show-confirm-bar="false"
- :focus="inputFocus"
- /><!-- @blur="onInputBlur" -->
- <button class="send-button" :class="{ 'send-active': inputText.trim() }" @click="sendMessage" :disabled="!inputText.trim()">
- <!-- @touchstart.prevent="sendMessage" -->
- 发送
- </button>
- </view>
- </view>
- </view>
- </template>
- <script>
- import { getChatHistory, createQuotedPrice as createQuotedPriceApi, getQuotedPrice } from '@/api/chat';
- import { WSSHOST } from '@/config.js';
- export default {
- data() {
- return {
- inputText: '',
- scrollTop: 0,
- scrollIntoView: '',
- placeholderStyle: 'color: #ccc; font-size: 28upx;',
- // 当前用户信息
- currentUser: {
- userId: 0, //客户登录帐户的id(不是 customId)
- staffId: 0, //门店员工id
- customId: 0, //客户id
- shopId: 0, //门店id
- username: '', //门店名称
- avatar: '' //门店头像
- },
- // 聊天消息列表
- messageList: [],
- today: new Date().toLocaleDateString(),
- // WebSocket相关
- socket: null,
- isConnected: true,
- room: '',
- reconnectCount: 0,
- maxReconnectCount: 5,
- isManualDisconnect: false, // 添加标志位:是否主动断开连接
- scrollWithAnimation: false, // 控制滚动动画
- // 分页加载
- allMessages: [], // 存储所有从API获取的消息
- initialLoadSize: 50, // 首次加载数量
- loadMoreSize: 50, // 每次加载更多的数量
- showLoadMore: false, // 是否显示“加载更多”
- // 键盘相关
- keyboardHeight: 0, // 键盘高度
- isKeyboardShow: false, // 键盘是否显示
- inputFocus: false, // 输入框焦点
- isInputFocused: false, // 标记输入框是否聚焦
- expireTime: 24 * 60 * 60, // 商品改价有效窗口时长 -- 24小时
- msgInputShow: true, // 消息输入框是否显示
- };
- },
- onLoad(options) {
- const eventChannel = this.getOpenerEventChannel();
- eventChannel.on('acceptDataFromList', (data) => {
- this.pageParams = data;
- // console.log("---------- pageParams: ", data);
- this.initializePage(data);
- })
- },
- 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.disconnectWebSocket();
- },
- onHide() {
- // 页面隐藏时关闭 WebSocket连接(根据业务需求)
- this.disconnectWebSocket();
- },
- onShow() {
- // 页面显示时重新连接(如果之前断开了)
- if (!this.isConnected && this.room) {
- this.connectWebSocket();
- }
- },
- methods: {
- init() {},
- initializePage(options) {
- if (options.chatPerson) {
- // 设置聊天人名称
- uni.setNavigationBarTitle({
- title: options.chatPerson
- });
- }
- this.currentUser.userId = this._parseAndValidateId(options.userId, 'userId 出错');
- this.currentUser.customId = this._parseAndValidateId(options.customId, 'customId 出错');
- this.currentUser.shopId = this._parseAndValidateId(options.shopId, 'shopId 出错');
- this.currentUser.staffId = this._parseAndValidateId(options.staffId, 'staffId 出错');
-
- if (options.name) {
- this.currentUser.username = options.name;
- }
- 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.error('房间号为默认房间 ---------- this.room: ', this.room);
- }
- // console.log('---------- this.room: ', this.room);
- // 加载聊天记录
- this.loadChatHistory();
- // 连接WebSocket
- this.connectWebSocket();
- },
- /**
- * 解析并验证ID
- * @param {any} id - 需要解析和验证的ID
- * @param {string} errorMsg - 验证失败时的错误信息
- * @returns {number|null} - 验证通过则返回ID,否则返回null
- */
- _parseAndValidateId(id, errorMsg) {
- if (id === null || id === undefined) {
- return null;
- }
- const parsedId = parseInt(id);
- if (isNaN(parsedId) || parsedId <= 0) {
- console.error(errorMsg + ' ---------- id: ', id);
- // this.$msg(errorMsg); // 根据项目情况,决定是否需要用户提示
- return null;
- }
- return parsedId;
- },
- // 加载聊天历史记录
- loadChatHistory() {
- getChatHistory({
- roomName: this.room
- }).then((res) => {
- if (res.code == 1 && res.data) {
- this.allMessages = res.data.map((item, index) => {
- // 判断是否是我(本店)发送的消息
- const isMine = item && item.shopId ? parseInt(item.shopId) == this.currentUser.shopId : false;
- //console.log("---isMine---: ", isMine);
- const newItem = {
- ...item,
- id: `message-${index}` // 为每条消息添加唯一ID // 'msg_local_' + timestamp + '_' + Math.floor(Math.random() * 100000) + 1,
- };
- //如果消息类型为 'goods',就调用 parseGoodsMessage 并将结果存入新属性 goodsInfo
- if (newItem.messageType === 'goods') {
- newItem.goodsInfo = this.parseGoodsMessage(newItem.message);
- }
- newItem.isMine = isMine;
- // 每6条消息显示一次时间
- if (index % 6 === 0) {
- newItem.time = this.getCurrentTime(newItem.timestamp);
- }
- return newItem;
- });
- 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) {
- uni.showToast({
- title: '房间号不能为空',
- icon: 'none'
- });
- return;
- }
- try {
- const wsUrl = `${this.getHost()}/room`;
- // 构建子协议参数(模拟POST body传输)
- const protocols = this.buildWebSocketProtocols();
- // 创建 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; // 连接成功时重置标志位
- // 启动心跳
- this.startHeartbeat();
- });
- // 监听WebSocket消息
- this.socket.onMessage((event) => {
- this.handleWebSocketMessage(event.data);
- });
- // 监听 WebSocket连接关闭
- this.socket.onClose(() => {
- console.log('WebSocket连接已关闭');
- // 只有在非主动断开的情况下才尝试重连
- if (!this.isManualDisconnect && this.reconnectCount < this.maxReconnectCount) {
- setTimeout(() => {
- if (!this.isManualDisconnect) {
- this.reconnectWebSocket();
- }
- }, 3000);
- }
- this.isConnected = false;
- // 尝试重新连接超过 maxReconnectCount 次才标记为未连接状态
- // if (!this.isManualDisconnect && this.reconnectCount >= this.maxReconnectCount) {
- // this.isConnected = false;
- // }
- });
- // 监听WebSocket错误
- this.socket.onError((error) => {
- console.error('WebSocket连接错误:', error);
- this.isConnected = false;
- this.handleConnectionError();
- });
- } catch (error) {
- console.error('创建WebSocket连接异常:', error);
- this.handleConnectionError();
- }
- },
- /**
- * 启动心跳
- */
- startHeartbeat() {
- if (this.heartbeatTimer) {
- clearInterval(this.heartbeatTimer);
- }
- this.heartbeatTimer = setInterval(() => {
- if (this.isConnected) {
- this.socket.send({
- data: '*hb*',
- success: () => {
- // console.log('发送ping');
- },
- fail: (error) => {
- console.error('发送ping失败:', error);
- }
- });
- }
- }, 30000); // 每30秒发送一次
- },
- /**
- * 停止心跳
- */
- stopHeartbeat() {
- if (this.heartbeatTimer) {
- clearInterval(this.heartbeatTimer);
- this.heartbeatTimer = null;
- }
- },
- // 构建WebSocket子协议参数(模拟POST body传输)
- buildWebSocketProtocols() {
- try {
- const userId = this.currentUser.userId; //客户登录帐户的id(不是 customId,是customer登录帐户的userId)
- if (isNaN(userId) || userId <= 0) {
- this.$msg('当前用户的id出错');
- return;
- }
- const customId = this.currentUser.customId;
- if (isNaN(customId) || customId <= 0) {
- this.$msg('当前用户的customId出错');
- return;
- }
- const shopId = this.currentUser.shopId;
- if (isNaN(shopId) || shopId <= 0) {
- this.$msg('当前用户的shopId出错');
- return;
- }
- const staffId = this.currentUser.staffId;
- if (isNaN(staffId) || staffId <= 0) {
- this.$msg('当前用户的staffId出错');
- return;
- }
- // 创建房间参数对象
- const roomParams = {
- u_id: userId, //不是 customId,是customer的: userId
- c_id: this.currentUser.customId,
- s_id: 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);
- 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
- // #ifndef H5
- // 小程序和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);
- // 增加一个保护
- if (typeof base64 !== 'string') {
- console.error('Base64编码失败,返回非字符串值:', base64);
- return ''; // 返回一个空字符串或其他默认值
- }
- // 转换为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;
- }
- // 停止心跳
- this.stopHeartbeat();
- },
- // 重连WebSocket
- reconnectWebSocket() {
- if (this.reconnectCount < this.maxReconnectCount) {
- this.reconnectCount++;
- console.log(`正在尝试第${this.reconnectCount}次重连...`);
- 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 messageType = messageData.messageType || 'text';
- // console.log('messageType -- ', messageType);
- // 创建消息对象
- 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 || '',
- time: this.getCurrentTime(), // 收到消息时间点
- timestamp: messageData.timestamp, // 消息发送时间点
- messageType: messageType
- };
- if (newMessage.messageType === 'goods') {
- newMessage.goodsInfo = this.parseGoodsMessage(newMessage.message);
- }
- // console.log('收到 ---------- newMessage: ', newMessage);
- // 添加到消息列表
- this.messageList.push(newMessage);
- // 滚动到底部
- // this.$nextTick(() => {
- // this.scrollToBottom(); // 用户操作时使用动画
- // });
- setTimeout(() => {
- this.scrollToBottom();
- }, 100);
- } catch (error) {
- console.error('解析WebSocket消息失败:', error, data);
- }
- },
- // 发送消息
- sendMessage() {
- if (!this.inputText.trim()) {
- return;
- }
- if (!this.isConnected) {
- uni.showToast({
- title: '未连接到网络',
- icon: 'none'
- });
- return;
- }
- const messageContent = this.inputText.trim();
- const avatar = this.currentUser.avatar || '';
- 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(timestamp), // 收到消息时间点
- timestamp: timestamp, // 消息发送时间点
- messageType: 'text'
- };
- // 添加到消息列表
- this.messageList.push(myMessage);
- // 清空输入框
- this.inputText = '';
- // 重新聚焦
- // this.$nextTick(() => {
- // this.inputFocus = false
- // this.$nextTick(() => {
- // this.inputFocus = true
- // })
- // });
- // 构建要发送的消息数据
- const messageData = {
- receiver: this.currentUser.customId,
- message: messageContent,
- username: this.currentUser.username,
- avatar: avatar,
- shopId: this.currentUser.shopId, // 发送者:门店使用shopId,客户使用customId
- timestamp: timestamp,
- messageType: 'text'
- };
- 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'
- });
- // 发送失败时,可以考虑移除刚添加的消息或标记为失败
- }
- });
- // 滚动到底部
- // this.$nextTick(() => {
- // this.scrollToBottom(); // 用户操作时使用动画
- // });
- setTimeout(() => {
- this.scrollToBottom();
- }, 280);
- } catch (error) {
- console.error('发送消息异常:', error);
- uni.showToast({
- title: '发送异常',
- icon: 'none'
- });
- }
- },
- // 处理连接错误
- handleConnectionError() {
- console.log('连接失败--', this.reconnectCount);
- // 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(timestamp = null) {
- let now;
- if (timestamp) {
- now = new Date(timestamp * 1000);
- } else {
- now = new Date();
- }
- const hours = String(now.getHours()).padStart(2, '0');
- const minutes = String(now.getMinutes()).padStart(2, '0');
-
- if (this.today == now.toLocaleDateString()) {
- return `${hours}:${minutes}`;
- } else {
- // return now.toLocaleDateString() + ' ' + `${hours}:${minutes}`;
- return `${now.getMonth() + 1}/${now.getDate()} ${hours}:${minutes}`;
- }
- },
- // 输入框聚焦事件
- onInputFocus() {
- // console.log('输入框聚焦');
- // 微信小程序中,聚焦时延迟滚动到底部
- setTimeout(() => {
- this.scrollToBottom();
- }, 450);
- 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');
- }
- },
- // 解析商品消息
- parseGoodsMessage(messageStr) {
- try {
- // 首先检查 messageStr 是否已经是对象
- if (typeof messageStr === 'object' && messageStr !== null) {
- return messageStr;
- }
- // 如果是字符串,则尝试解析
- if (typeof messageStr === 'string') {
- let parsed = JSON.parse(messageStr);
- if(parsed.goodsPriceType == 0 || parsed.goodsPriceType == 1) { // 请求后端获取修改价格,再根据结果展示
- // 如果没有过期,请求后端获取修改价格
- const now = Math.floor(Date.now()/1000);
- const timeDiff = now - parsed.timestamp;
- const expireTime = 24 * 60 * 60;
- if (timeDiff > expireTime) {
- // console.log("parsed 过期:", parsed);
- } else {
- // console.log("parsed 没价格:", parsed);
- const key = parsed.key || '';
- if (key != '') {
- getQuotedPrice({
- key: key,
- }).then(res => {
- // console.log("res", res)
- if (res.code == 1) {
- if (res.data.price != -1) {
- parsed.resourcePrice = parseFloat(parsed.goodsPrice).toFixed(2);
- parsed.goodsPriceType = 2; // 价格类型
- parsed.goodsPrice = parseFloat(res.data.price).toFixed(2);
- // 通过 parsed.goodsPriceType 判断哪种类型
- }
- }
- }).catch(err => {
- console.log("getQuotedPrice error: ", key, err)
- })
- }
- }
- } else if(parsed.goodsPriceType == 1) {
- // console.log("parsed 有价格:", parsed);
- } else if(parsed.goodsPriceType == 2) {
- // console.log("parsed 重新改价:", parsed);
- } else {
- console.error("parsed 其他类型:", parsed);
- return {};
- }
- return parsed;
- }
- console.error("解析商品消息失败:", messageStr);
- // 如果两者都不是,返回空对象
- return {};
- } catch (error) {
- console.error("解析商品消息失败:", error, messageStr);
- // 在解析失败时返回一个包含默认值的对象,以避免模板渲染错误
- return {
- goodsImg: '',
- goodsName: '商品信息解析失败',
- goodsPrice: '0.00'
- };
- }
- },
- // 发送商品改价信息
- sendMessageOfGoodsPrice(currentGoodsInfo) {
- if (!this.isConnected) {
- uni.showToast({
- title: '未连接到网络',
- icon: 'none'
- });
- return;
- }
- const avatar = this.currentUser.avatar || '';
- const timestamp = Math.floor(Date.now() / 1000);
- // 构建商品信息消息体 -------------------------------- 所看到的消息体 -------------------------------
- const goodsMessage = {
- ...currentGoodsInfo
- };
- const messageContent = JSON.stringify(goodsMessage); // 所看到的消息体
- try {
- // 立即在本地显示发送的消息(右侧)
- const myMessage = {
- id: 'msg_local_' + timestamp + '_' + (Math.floor(Math.random() * 100000) + 1),
- isMine: true,
- // message: `[商品] ${this.goodsInfo.goodsName}`, // 显示简化信息
- message: messageContent,
- username: this.currentUser.username,
- avatar: avatar,
- time: this.getCurrentTime(timestamp),
- messageType: 'goods', // 附加一个字段,用于渲染时区分商品消息
- goodsInfo: goodsMessage
- };
- // 添加到消息列表
- this.messageList.push(myMessage);
- // 滚动到底部
- // this.$nextTick(() => {
- // this.scrollToBottom();
- // });
- setTimeout(() => {
- this.scrollToBottom();
- }, 100);
- // 构建要发送的消息数据
- const messageData = {
- receiver: this.currentUser.customId,
- message: messageContent,
- username: this.currentUser.username,
- avatar: avatar,
- shopId: this.currentUser.shopId, // 发送者:门店使用shopId,客户使用customId
- timestamp: timestamp,
- messageType: 'goods'
- };
- const json = JSON.stringify(messageData);
- // 发送到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'
- });
- }
- },
- createQrotedPrice(message) {
- // console.log("createPrice", message)
- // 表单验证
- if (message.quotedPrice == '') {
- this.$msg('请输入价格');
- return;
- }
- if (isNaN(message.quotedPrice)) {
- this.$msg('价格不对,请输入数字');
- return;
- }
- if (message.quotedPrice < 0.01) {
- this.$msg('价格不能小于0.01');
- return;
- }
- const keyArr = message.goodsInfo.key.split('-');// shopId-userId-goodsId
- if (keyArr.length != 3) {
- //console.log("keyArr.length != 3", keyArr)
- this.$msg('key格式不正确');
- return;
- }
- const shopId = parseInt(keyArr[0]);
- const userId = parseInt(keyArr[1]);
- const goodsId = parseInt(keyArr[2]);
- if (isNaN(shopId) || isNaN(userId) || isNaN(goodsId)) {
- console.error("isNaN(shopId) || isNaN(userId) || isNaN(goodsId)", shopId, userId, goodsId)
- this.$msg('key数据出错');
- return;
- }
- createQuotedPriceApi({
- key: message.goodsInfo.key,
- price: parseFloat(message.quotedPrice).toFixed(2)
- }).then((res) => {
- // console.log("createQuotedPriceApi", res)
- if (res.code == 1) {
- this.$msg('改价成功');
- message.goodsInfo.goodsPriceType = 2;
- message.goodsInfo.goodsPrice = parseFloat(message.quotedPrice).toFixed(2);
- message.goodsInfo.lastPrice = parseFloat(message.quotedPrice).toFixed(2);
- message.quotedPrice = '';
- this.sendMessageOfGoodsPrice(message.goodsInfo);
- } else {
- this.$msg(res.msg);
- }
- }).catch((err) => {
- console.log('createQuotedPrice error', err);
- });
- },
- isExpired(timestamp) {
- if (isNaN(timestamp)) {
- return false;
- }
- const now = Math.floor(Date.now() / 1000);
- const timeDiff = now - timestamp;
- return timeDiff < this.expireTime;
- },
- // 查看商品
- toGoodsDetail(goodsInfo) {
- // console.log("---------- goodsInfo: ", goodsInfo);
- const goodsId = parseInt(goodsInfo.goodsId);
- const categoryId = parseInt(goodsInfo.categoryId);
- const hdId = parseInt(goodsInfo.hdId);
- const account = parseInt(goodsInfo.account);
- if (isNaN(goodsId) || isNaN(categoryId) || isNaN(hdId) || isNaN(account)) {
- this.$msg('商品数据异常');
- return;
- }
- this.$util.pageTo({
- url: '/admin/goods/detail',
- query: {
- // id=2020&categoryId=1402&hdId=229&account=36548&shopId=36548&name=花仙子&avatar=https://img.theflorist.cn/hhb_small.png
- id: goodsId,
- account: goodsInfo.account,
- hdId: goodsInfo.hdId,
- categoryId: categoryId,
- shopId: goodsInfo.shopId,
- name: goodsInfo.name,
- avatar: goodsInfo.avatar
- }
- });
- this.disconnectWebSocket();
- },
- hideKeyboard() {
- this.keyboardHeight = 0;
- this.msgInputShow = false;
- },
- showKeyboard() {
- this.msgInputShow = true;
- },
- // 补全头像地址:若不含 http(s):// 则加上 this.$constant.imgUrl + '/'
- formatAvatarUrl(avatar) {
- try {
- const value = avatar || '';
- if (!value) {
- return this.$constant.imgUrl + '/retail/default-img.png';
- }
- // 已是 http/https 绝对地址
- if (/^https?:\/\//i.test(value)) {
- return value;
- }
-
- const base =this.$constant.imgUrl || '';
- if (!base) {
- return value;
- }
- const normalizedBase = base.replace(/\/+$/, '');
- if (value.startsWith('/')) {
- return normalizedBase + value;
- }
- return normalizedBase + '/' + value;
- } catch (e) {
- return avatar;
- }
- }
- }
- };
- </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;
- }
- .goods-content-wrapper {
- width: 72%;
- }
- .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;
- }
- }
- }
- .goods-info.right-bubble {
- background-color: #fff !important;
- color: #333;
- &::before {
- border-left-color: #fff !important;
- }
- }
- .goods-info {
- background-color: #fff !important;
- color: #333;
- padding: 20rpx;
- border-radius: 16rpx;
- &.right-bubble {
- background-color: #fff !important; // 覆盖默认气泡颜色
- &::before {
- border-left-color: #fff; // 确保箭头颜色与背景一致
- }
- }
- .goods-main {
- display: flex;
- position: relative;
- }
- .goods-image {
- width: 180rpx;
- height: 180rpx;
- border-radius: 8rpx;
- margin-right: 20rpx;
- flex-shrink: 0;
- }
- .goods-details {
- flex: 1;
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- .goods-name-wrapper {
- height: 80rpx;
- }
- .goods-name {
- font-size: 30rpx;
- color: #333;
- font-weight: bold;
- // 多行省略
- text-overflow: -o-ellipsis-lastline;
- overflow: hidden;
- text-overflow: ellipsis;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- line-clamp: 2;
- -webkit-box-orient: vertical;
- }
- .sales-info {
- display: flex;
- align-items: center;
- margin-top: 10rpx;
- }
- .delivery-time {
- background-color: #e8f5ff;
- color: #007aff;
- font-size: 20rpx;
- padding: 4rpx 8rpx;
- border-radius: 4rpx;
- }
- .goods-price {
- font-size: 28rpx;
- color: #ff5050;
- font-weight: bold;
- margin-top: 40rpx;
- }
- .goods-price--secondary {
- font-size: 24rpx;
- color: #999999;
- font-weight: normal;
- }
- }
- .goods-footer {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-top: 20rpx;
- padding-top: 10rpx;
- border-top: 1rpx solid #f5f5f5;
- }
- .action-buttons {
- display: flex;
- justify-content: flex-end;
- align-items: center;
- margin-top: 20rpx;
- form {
- display: flex;
- align-items: center;
- }
- .price-input {
- display: inline-block;
- vertical-align: middle;
- width: 160rpx;
- height: 60rpx;
- border: 1px solid #666666;
- border-radius: 8rpx;
- padding: 0 15rpx;
- font-size: 28rpx;
- text-align: center;
- color: #666666;
- }
- .price-input::placeholder {
- color: #aaaaaa;
- }
- .action-btn {
- display: inline-block;
- vertical-align: middle;
- padding: 10rpx 30rpx;
- border-radius: 40rpx;
- font-size: 28rpx;
- margin-left: 20rpx;
- line-height: 1.5;
- &::after {
- border: none;
- }
- }
- .spec-btn {
- background-color: #f5f5ff;
- color: #333;
- border: 1px solid #eee;
- }
- .buy-btn {
- background-color: #ff9800;
- color: white;
- }
- .goods-price {
- font-size: 28rpx;
- color: green;
- font-weight: bold;
- margin-right: 12rpx;
- }
- }
- }
- .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: #cccccc;
- }
- }
- .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>
|