L***@163.com
L***@163.com
  • 发布:2025-12-09 10:48
  • 更新:2025-12-10 17:51
  • 阅读:37

scroll-view中用video原生组件脱离文档流

分类:uni-app

<template>
<view class="kefu-chat">
<!-- 顶部导航栏 -->
<uv-navbar :title="friendInfo.nickname || merInfo.merchant_name || '客服'" :autoBack="true" :placeholder="true">
<template v-slot:left>
<uv-icon name="arrow-left" size="20" color="#333" @click="goBack" />
</template>
</uv-navbar>

    <!-- 聊天内容区域 -->  
    <scroll-view class="chat-content" scroll-y :scroll-top="scrollTop" :scroll-into-view="scrollIntoViewId"  
            :scroll-with-animation="true" @scrolltoupper="loadMoreMessages" @scroll="onScroll"  
            :enhanced="true"   
            :bounce="true"  
            scroll-anchoring="true"  
            style="transform: translateZ(0); position: relative; z-index: 1;"  
        >  
        <view class="message-list">  
            <view v-for="(message, index) in messageList" :key="index" :id="'' + index"  
                :class="{ 'message-right': message.uid === userInfo.id }" class="message-item">  
                <!-- 用户头像 -->  
                <view class="avatar">  
                    <uv-image  
                        :src="message.uid === userInfo.id ? userInfo.avatar : (friendInfo.avatar || merInfo.brand_logo)"  
                        width="50" height="50" shape="circle"  
                        :errorIcon="getIconPath('https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1208/703ee202512081525532575.png')" />  
                </view>  

                <!-- 消息内容 -->  
                <view class="message-content">  
                    <view class="message-bubble"  
                        :style="message.type !== 4 && message.type !== 5?'background-color: white;padding:20rpx;box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);':''"  
                        :class="{ 'bubble-right': message.uid === userInfo.id }">  
                        <!-- 图片消息 -->  
                        <view v-if="message.type === 4 && message.image_url" class="image-message"  
                            @click="previewImage(message.image_url)">  
                            <uv-image :src="message.image_url" mode="aspectFit"  
                                :style="{width: `min(${message.image_width}rpx)`, height: `min(${message.image_height}rpx, 400rpx)`}"  
                                style="opacity: 1;" :errorIcon="getIconPath('image-error.png')" />  
                        </view>  

                        <!-- 视频消息 -->  
                        <view v-else-if="message.type === 5 && message.video_url" class="video-message">  
                            <video  
                                :src="message.video_url"  
                                class="video-player"  
                                controls  

                                playsinline  
                                webkit-playsinline="true"  

                                x5-playsinline="true"  
                                x5-video-player-type="h5"  
                                x5-video-orientation="portrait"  

                                :show-center-play-btn="true"  
                                :enable-progress-gesture="false"  
                                :vslide-gesture="false"  

                                :app-plus="{  
                                    videoEmbeded: true, // 必加:嵌入WebView  
                                    zindex: 1, // 层级与WebView一致  
                                    renderMode: 'webview', // 强制WebView渲染  
                                    width: '400rpx',   
                                    height: '400rpx'  
                                }"  

                                @click="playVideo(message.video_url)"  
                            ></video>  
                        </view>  

                        <!-- 商品消息 -->  
                        <view v-else-if="message.type === 2 && message.product_info" class="product-message">  
                            <view class="product-message-header">  
                                <text class="product-label">商品信息</text>  
                            </view>  
                            <view class="product-message-content">  
                                <uv-image :src="message.product_info.image" width="60" height="60" mode="aspectFill"  
                                    :errorIcon="getIconPath('https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1208/faa3e202512081724257635.png')" />  
                                <view class="product-message-details">  
                                    <text class="product-message-name">{{ message.product_info.name }}</text>  
                                    <text class="product-message-price">¥{{ message.product_info.price }}</text>  
                                </view>  
                            </view>  
                        </view>  

                        <!-- 服务订单消息 -->  
                        <view v-else-if="message.type === 3 && message.service_order_info"  
                            class="service-order-message">  
                            <view class="service-order-header">  
                                <text class="service-order-label">服务订单信息</text>  
                            </view>  
                            <view class="service-order-content">  
                                <view class="service-order-details">  
                                    <text  
                                        class="service-order-name">{{ message.service_order_info.product_name }}</text>  
                                    <text  
                                        class="service-order-price">¥{{ message.service_order_info.total_price }}</text>  
                                    <text  
                                        class="service-order-time">服务时间:{{ message.service_order_info.service_time }}</text>  
                                    <text  
                                        class="service-order-address">服务地址:{{ message.service_order_info.address_address }}</text>  
                                    <text  
                                        class="service-order-no">订单号:{{ message.service_order_info.order_no }}</text>  
                                </view>  
                            </view>  
                        </view>  

                        <!-- 语音消息 -->  
                        <view v-else-if="message.type === 6 && message.voice_url" class="voice-message">  
                            <view class="voice-length">{{ message.voice_url }}</view>  
                        </view>  

                        <!-- 普通消息 -->  
                        <text v-else class="message-text">{{ message.content }}</text>  

                        <!-- 商品消息 -->  
                        <!-- <view class="orderListContent"  v-else v-for="(item, index) in orderList.shop" :key="index">  
                            <view class="orderListContentTitle">  
                                <view class="orderListContentTitleLeft">  
                                    <uv-avatar :src="item.image" shape="square" size="24"></uv-avatar>  
                                    <text class="orderListContentTitleLeftName">{{ item.shopName }}</text>  
                                    <uv-icon name="arrow-right" color="#111111" size="16"></uv-icon>  
                                </view>  
                                <text class="orderListContentTitleRight">{{ item.status }}</text>  
                            </view>  
                            <view class="orderListContentCenter">  
                                <view class="orderListContentcenterLeft">  
                                    <uv-image :src="item.image" width="80" height="80" radius="10"  
                                        class="orderListContentcenterLeftImage" mode="aspectFill" />  
                                    <view class="product-details">  
                                        <view class="productTitle">  
                                            <text class="productTitleName">{{ item.title }}</text>  
                                        </view>  
                                        <view class="eproductSpecifications">  
                                            <text class="eproductSpecificationsName">{{ item.spec }}</text>  
                                        </view>  
                                    </view>  
                                </view>  
                                <view class="orderListContentCenterRight">  
                                    <view class="orderListContentCenterPrice">  
                                        <text class="orderListContentCenterPriceValue">¥{{ item.price }}</text>  
                                    </view>  
                                    <text class="eproductSpecificationsNumberValue">×{{ item.number }}</text>  
                                </view>  
                            </view>  
                        </view> -->  

                        <!-- 日常家具 -->  
                        <!-- <view class="dailyFurnitureCleaning" v-else v-for="(item,index) in orderList.dailyFurniture">  
                            <view class="dailyFurniture">  
                                <view class="dailyFurnitureTitle">  
                                    <Text class="dailyFurnitureTitleName">{{item.cleaningName}}</Text>  
                                    <Text class="dailyFurnitureTitleValue">{{item.status}}</Text>  
                                </view>  
                                <view class="dailyFurnitureContent">  
                                    <view class="dailyFurnitureContentList" :style="idx===0?'margin-top:0rpx':''" v-for="(itm,idx) in item.content">  
                                        <Text class="dailyFurnitureContentName">{{itm.name}}</Text>  
                                        <Text class="dailyFurnitureContentTime">{{itm.value}}</Text>  
                                        <uv-image  
                                            src="https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1204/4f518202512041129434253.png"  
                                            width="20" height="20" mode="aspectFill" v-if="itm.reward"/>  
                                    </view>  
                                </view>  
                            </view>  
                        </view> -->  

                        <!-- 平台外卖 -->  
                        <!-- <view class="platformTakeaway" v-else v-for="(item,index) in orderList.takeout">  
                            <view class="takeout">  
                                <view class="takeoutTitle">  
                                    <Text class="takeoutTitleName" style="color: #2D68EC;background-color: #DBEAFE;">{{item.takeoutName}}</Text>  
                                    <Text class="takeoutTitleValue">{{item.status}}</Text>  
                                </view>  
                                <view class="takeoutContent" v-for="(itm,idx) in item.content">  
                                    <view class="takeoutList">  
                                        <uv-image  
                                            :src="itm.img"  
                                            width="28" height="28" mode="aspectFill" />  
                                        <view class="takeoutListDetails">  
                                            <view>  
                                                <Text class="takeoutListDetailsName">{{itm.name}}</Text>  
                                            </view>  
                                            <view>  
                                                <Text class="takeoutListDetailsValue">姓名 {{itm.value}}</Text>  
                                            </view>  
                                        </view>   
                                    </view>  
                                </view>  
                            </view>  
                        </view> -->  

                        <!-- 宠物服务 -->  
                        <!-- <view class="platformTakeaway" v-else v-for="(item,index) in orderList.petServices">  
                            <view class="takeout">  
                                <view class="takeoutTitle">  
                                    <Text class="takeoutTitleName" style="color: #3DA316; background-color: #E3FACB;">{{item.takeoutName}}</Text>  
                                    <Text class="takeoutTitleValue">{{item.status}}</Text>  
                                </view>  
                                <view class="takeoutContent" v-for="(itm,idx) in item.content">  
                                    <view class="takeoutList">  
                                        <uv-image  
                                            :src="itm.img"  
                                            width="28" height="28" mode="aspectFill" />  
                                        <view class="takeoutListDetails">  
                                            <view>  
                                                <Text class="takeoutListDetailsName">{{itm.name}}</Text>  
                                            </view>  
                                            <view>  
                                                <Text class="takeoutListDetailsValue">姓名 {{itm.value}}</Text>  
                                            </view>  
                                        </view>   
                                    </view>  
                                </view>  
                            </view>  
                        </view> -->  

                    </view>  
                    <view class="message-time">  
                        <text>{{ formatTime(message.created_at) }}</text>  
                    </view>  
                </view>  
            </view>  
        </view>  
        <!-- 底部占位,避免被输入区遮挡 -->  
        <view class="chat-bottom-spacer"></view>  
    </scroll-view>  
    <!-- 商品信息卡片 -->  
    <view class="product-card" v-if="productInfo">  
        <!-- 关闭按钮 -->  
        <view class="product-close" @click="productInfo = null">×</view>  
        <view class="product-info">  
            <uv-image :src="productInfo.image" width="80" height="80" mode="aspectFill"  
                :errorIcon="getIconPath('avatar.png')" />  
            <view class="product-details">  
                <text class="product-name">{{ productInfo.name }}</text>  
                <text class="product-price">¥{{ productInfo.price }}</text>  
            </view>  
        </view>  
        <view class="product-actions">  
            <button class="send-product-btn" @click="sendProductMessage">  
                发送商品  
            </button>  
        </view>  
    </view>  

    <!-- 服务订单信息卡片 -->  
    <view class="service-order-card" v-if="serviceOrderInfo">  
        <!-- 关闭按钮 -->  
        <view class="service-order-close" @click="serviceOrderInfo = null">×</view>  
        <view class="service-order-info">  
            <view class="service-order-details">  
                <text class="service-order-name">{{ serviceOrderInfo.product_name }}</text>  
                <text class="service-order-price">¥{{ serviceOrderInfo.total_price }}</text>  
                <text class="service-order-time">服务时间:{{ serviceOrderInfo.service_time }}</text>  
                <text class="service-order-address">服务地址:{{ serviceOrderInfo.address_address }}</text>  
                <text class="service-order-no">订单号:{{ serviceOrderInfo.order_no }}</text>  
            </view>  
        </view>  
        <view class="service-order-actions">  
            <button class="send-service-order-btn" @click="sendServiceOrderMessage">  
                发送服务订单  
            </button>  
        </view>  
    </view>  

    <!-- 底部输入区域 -->  
    <view class="input-area">  
        <view class="input-container">  
            <uv-image :src="switchVoiceTextStatus" width="36" height="36" mode="aspectFill"  
                @click="switchVoiceText()" />  
            <view class='input-box' :class="{ 'input-box-focus': inputFocus }" @touchstart="startRecord"  
                @touchend="stopRecord" @touchcancel="stopRecord">  
                <input v-model="inputMessage" :disabled="switchStatus === 1?false:true"  
                    :class="switchStatus === 1 ?'message-input':'message-input-status'"  
                    :placeholder="switchStatus === 1 ? '请输入您想要咨询的内容...' : '按住说话'" @confirm="sendMessage"  
                    @blur="inputFocus = false" @focus="onInputFocus" confirm-type="send" />  
                <uv-image v-if="switchStatus===1"  
                    src="https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1202/11a04202512021005249142.png"  
                    width="24" height="24" mode="aspectFill" @click="toggleEmojiPanel()" />  
            </view>  
            <uv-image  
                src="https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1202/7ed38202512021601429234.png"  
                width="36" height="36" mode="aspectFill" @click="orderShow()" />  
            <uv-image :src="moreFeaturesStatus" width="36" height="36" mode="aspectFill" @click="moreFeatures()" />  
        </view>  
    </view>  
    <!-- 原生表情面板 -->  
    <scroll-view scroll-y :style="{height: '518rpx'}" v-if="showEmojiPanel">  
        <view class="emoji-panel" style="position: relative; z-index: 9999;">  
            <!-- 表情列表:每个表情项加@click.stop -->  
            <view class="native-emoji-list">  
                <view class="native-emoji-item" v-for="(emoji, index) in nativeEmojiList" :key="index"  
                    @click.stop="selectNativeEmoji(emoji)">  
                    {{ emoji }}  
                </view>  
            </view>  
            <view class="send-content">  
                <uv-image class="emojiDetele"  
                    src="https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1206/60b41202512060905184516.png"  
                    width="32" height="32" radius="8rpx" mode="aspectFill" @click="clearInputMessage" />  
                <view class="send-btn"  
                    :class="{ 'send-btn-active': inputMessage.trim(), 'send-btn-disabled': !inputMessage.trim() }"  
                    @click="sendMessage">  
                    <text class="send-text">发送</text>  
                </view>  
            </view>  
        </view>  
    </scroll-view>  
    <!-- 更多服务 -->  
    <view class="more-serve" v-if="moreServe">  
        <view class="more-serve-list" key="index" v-for="(item,index) in moreFunction"  
            @click="index===0?selectFromAlbum():(index===1?takeVideo():(index === 2?orderShow():''))">  
            <uv-image :src="item.img" width="64" height="64" mode="aspectFill" class="more-serve-list-img" />  
            <Text class="more-serve-list-text">{{item.name}}</Text>  
        </view>  
    </view>  
    <!-- 订单列表 -->  
    <view class="orderList" v-if="showMask">  
        <view class="orderListTitle">  
            <uv-icon name="close" size="20" class="iconClose" color="#CDCDCD" @click="closed()" />  
            <Text class="orderListTitleText">{{orderTitle}}</Text>  
            <view></view>  
        </view>  
        <!-- <view class="orderSelect">  
            <uv-tabs :list="orderSelect" :lineColor="color_red" :activeStyle="{color: color_red}"  
                :inactiveStyle="{color: color_black}" :current="active" @change="selectOrderShop" />  
        </view> -->  
        <view v-if="orderShopId === 0">  
            <view class="orderSearch">  
                <uv-icon name="search" size="25" color="#CDCDCD" @click="closed()" />  
                <input type="text" class="orderSearchInput" placeholder="搜索我的订单" />  
            </view>  

            <!-- 可滚动区域 -->  
            <scroll-view scroll-y class="orderListScroll" :style="{height: '600rpx'}">  

                <!-- 订单商品 -->  
                <view class="orderListContent" v-for="(item, index) in orderList.shop" :key="index">  
                    <view class="orderListContentTitle">  
                        <view class="orderListContentTitleLeft">  
                            <uv-avatar :src="item.image" shape="square" size="24"></uv-avatar>  
                            <text class="orderListContentTitleLeftName">{{ item.shopName }}</text>  
                            <uv-icon name="arrow-right" color="#111111" size="16"></uv-icon>  
                        </view>  
                        <text class="orderListContentTitleRight">{{ item.status }}</text>  
                    </view>  
                    <view class="orderListContentCenter">  
                        <view class="orderListContentcenterLeft">  
                            <uv-image :src="item.image" width="80" height="80" radius="10"  
                                class="orderListContentcenterLeftImage" mode="aspectFill" />  
                            <view class="product-details">  
                                <view class="productTitle">  
                                    <text class="productTitleName">{{ item.title }}</text>  
                                </view>  
                                <view class="eproductSpecifications">  
                                    <text class="eproductSpecificationsName">{{ item.spec }}</text>  
                                </view>  
                            </view>  
                        </view>  
                        <view class="orderListContentCenterRight">  
                            <view class="orderListContentCenterPrice">  
                                <text class="orderListContentCenterPriceValue">¥{{ item.price }}</text>  
                            </view>  
                            <text class="eproductSpecificationsNumberValue">×{{ item.number }}</text>  
                        </view>  
                    </view>  
                    <view class="consult">  
                        <text class="consultValue">咨询</text>  
                    </view>  
                    <view class="underline"></view>  
                    <!-- 底部奖励标识 -->  
                    <!-- <view class="orderRewards" v-if="item.reward">  
                        <uv-image  
                            src="https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1203/dcbc7202512031011377196.png"  
                            width="24" height="24" class="orderRewardsImg" mode="aspectFill" />  
                        <view class="orderRewardsName">  
                            <text class="orderRewardsValue">订单奖励已发放</text>  
                        </view>  
                    </view> -->  
                </view>  

                <!-- 日常家具保洁 -->  
                <view class="dailyFurnitureCleaning" v-for="(item,index) in orderList.dailyFurniture">  
                    <view class="dailyFurniture">  
                        <view class="dailyFurnitureTitle">  
                            <Text class="dailyFurnitureTitleName">{{item.cleaningName}}</Text>  
                            <Text class="dailyFurnitureTitleValue">{{item.status}}</Text>  
                        </view>  
                        <view class="dailyFurnitureContent">  
                            <view class="dailyFurnitureContentList" :style="idx===0?'margin-top:0rpx':''"  
                                v-for="(itm,idx) in item.content">  
                                <Text class="dailyFurnitureContentName">{{itm.name}}</Text>  
                                <Text class="dailyFurnitureContentTime">{{itm.value}}</Text>  
                                <uv-image  
                                    src="https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1204/4f518202512041129434253.png"  
                                    width="20" height="20" mode="aspectFill" v-if="itm.reward" />  
                            </view>  
                        </view>  
                    </view>  
                    <view class="consult">  
                        <text class="consultValue">咨询</text>  
                    </view>  
                    <view class="underline"></view>  
                </view>  

                <!-- 平台外卖 -->  
                <view class="platformTakeaway" v-for="(item,index) in orderList.takeout">  
                    <view class="takeout">  
                        <view class="takeoutTitle">  
                            <Text class="takeoutTitleName"  
                                style="color: #2D68EC;background-color: #DBEAFE;">{{item.takeoutName}}</Text>  
                            <Text class="takeoutTitleValue">{{item.status}}</Text>  
                        </view>  
                        <view class="takeoutContent" v-for="(itm,idx) in item.content">  
                            <view class="takeoutList">  
                                <uv-image :src="itm.img" width="28" height="28" mode="aspectFill" />  
                                <view class="takeoutListDetails">  
                                    <view>  
                                        <Text class="takeoutListDetailsName">{{itm.name}}</Text>  
                                    </view>  
                                    <view>  
                                        <Text class="takeoutListDetailsValue">姓名 {{itm.value}}</Text>  
                                    </view>  
                                </view>  
                            </view>  
                        </view>  
                    </view>  
                    <view class="consult">  
                        <text class="consultValue">咨询</text>  
                    </view>  
                    <view class="underline"></view>  
                </view>  

                <!-- 宠物服务 -->  
                <view class="platformTakeaway" v-for="(item,index) in orderList.petServices">  
                    <view class="takeout">  
                        <view class="takeoutTitle">  
                            <Text class="takeoutTitleName"  
                                style="color: #3DA316; background-color: #E3FACB;">{{item.takeoutName}}</Text>  
                            <Text class="takeoutTitleValue">{{item.status}}</Text>  
                        </view>  
                        <view class="takeoutContent" v-for="(itm,idx) in item.content">  
                            <view class="takeoutList">  
                                <uv-image :src="itm.img" width="28" height="28" mode="aspectFill" />  
                                <view class="takeoutListDetails">  
                                    <view>  
                                        <Text class="takeoutListDetailsName">{{itm.name}}</Text>  
                                    </view>  
                                    <view>  
                                        <Text class="takeoutListDetailsValue">姓名 {{itm.value}}</Text>  
                                    </view>  
                                </view>  
                            </view>  
                        </view>  
                    </view>  
                    <view class="consult">  
                        <text class="consultValue">咨询</text>  
                    </view>  
                    <view class="underline"></view>  
                </view>  
            </scroll-view>  
        </view>  

        <scroll-view class="orderListScroll" scroll-y="true" :style="{height: '600rpx'}" v-if="orderShopId===1">  
            <view v-for="(item,index) in productList" :key="index" class="orderListContentCenter"  
                style="margin-bottom: 56rpx;">  
                <view class="shopListContentcenterLeft">  
                    <uv-image :src="item.image" width="80" height="80" radius="10"  
                        class="orderListContentcenterLeftImage" mode="aspectFill" />  
                    <view>  
                        <view class="productTitle">  
                            <text class="productTitleName">{{ item.title }}</text>  
                        </view>  
                    </view>  
                </view>  

                <view class="orderListContentCenterRight">  
                    <view class="orderListContentCenterPrice">  
                        <text class="orderListContentCenterPriceValue">¥{{ item.price }}</text>  
                    </view>  
                    <view class="consult">  
                        <text class="consultValue">发送</text>  
                    </view>  
                </view>  
            </view>  
        </scroll-view>  
        <view class="notOrderProductInquiry" @click="closed()">  
            <Text class="notOrderProductInquiryValue">非订单/商品咨询</Text>  
        </view>  
    </view>  
</view>  
<view class="mask" v-if="showMask" @click="closed()"></view>  

</template>

<script setup>
import {
ref,
reactive,
onMounted,
onUnmounted,
nextTick
} from 'vue'
import {
onLoad,
onShow
} from '@dcloudio/uni-app'
import {
bindCustomer,
sendMessage as apiSendMessage,
getChatLists,
getChatList,
addOfficial,
videoUplaod,
audioUplaod,
getFileTaskResult
} from '@/api/kefu.js'
import {
getUserInfo
} from '@/api/index.js'
import {
storeToRefs
} from 'pinia'
import {
useWebSocketStore
} from '@/store/MessageWebsocket.js'
import {
useUserStore
} from '@/store'
import {
uploadImage,
videoUpload
} from '@/api/index.js'

// 用户状态管理  
const userStore = useUserStore()  
const {  
    token,  
    userInfo: storeUserInfo  
} = storeToRefs(userStore)  
const wsStore = useWebSocketStore()  

// 响应式数据  
const userInfo = ref({})  
const merInfo = ref({})  
const friendInfo = ref({})  
const messageList = ref([])  
const inputMessage = ref('')  
const inputFocus = ref(false)  
const scrollTop = ref(0)  
const scrollIntoViewId = ref('')  
const loading = ref(false)  
const page = ref(1)  
const hasMore = ref(true)  
const productInfoInput = ref(false)  
const switchVoiceTextStatus = ref(  
    'https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1202/b601a202512020937515649.png');  
const moreFeaturesStatus = ref(  
    'https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1202/5831a202512020940542856.png');  
const switchStatus = ref(1);  
const showEmojiPanel = ref(false) // 表情面板显示状态  
const moreServe = ref(false);  
const showMask = ref(false);  
const videoUrl = ref('');  

// 状态定义  
const isRecording = ref(false)  
const currentPlayingVoice = ref('')  
// 录音相关实例(区分环境)  
const recorderManager = ref(null) // 小程序录音管理器  
const h5Recorder = ref(null) // H5 MediaRecorder实例  
const audioStream = ref(null) // H5音频流  
const innerAudioContext = ref(null) // 音频播放上下文  

// 判断是否为H5环境  
const isH5 = () => {  
    // #ifdef H5  
    return true  
    // #endif  
    return false  
}  

const nativeEmojiList = ref([  
    '?', '?', '?', '?', '?', '?', '?',  
    '?', '?', '?', '?', '?', '?', '?',  
    '?', '?', '?', '?', '?', '?', '?',  
    '?', '?', '?', '?', '?', '?', '?',  
    '?', '?', '?', '?', '?', '?', '?',  
    '?', '?', '?', '?', '?', '?', '?',  
    '?', '?', '?', '?', '?', '☹️', '?',  
    '?', '?', '?', '?', '?', '?', '?',  
    '?', '?', '?', '?', '?', '?', '?' // 确保都是基础emoji  
])  

const moreFunction = ref([{  
        id: 0,  
        name: '相册',  
        img: 'https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1202/7408e202512021104333458.png'  
    },  
    {  
        id: 1,  
        name: '拍摄',  
        img: 'https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1202/3331b202512021106431887.png'  
    },  
    {  
        id: 2,  
        name: '订单',  
        img: 'https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1202/d03ab202512021107236046.png'  
    },  
    {  
        id: 3,  
        name: '服务评价',  
        img: 'https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1202/c27ec202512021109092650.png'  
    },  
]);  
const color_red = uni.$config.color.red  
const orderTitle = ref('为更快解决您的问题,请选择要咨询的内容');  
const orderShopId = ref(0);  
const orderSelect = ref([{  
        id: 0,  
        name: '订单',  
    },  
    {  
        id: 1,  
        name: '商品',  
    }  
]);  

const orderList = ref({  
    shop: [{  
        shopName: "依利安达家具旗舰店",  
        status: "完成",  
        image: "https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1203/42567202512030927251507.png",  
        title: "商品标题商品标题商品标题商品标题",  
        spec: "产品规格",  
        number: 1,  
        price: 19999.9,  
        reward: true  
    }],  
    dailyFurniture: [{  
        cleaningName: '日常家具保洁2小时',  
        status: '待服务',  
        content: [{  
                name: '服务时间',  
                value: '2025-12-12 12:00',  
                reward: false  
            },  
            {  
                name: '服务地点',  
                value: '盛世御龙湾-3号楼一单元402',  
                reward: false  
            },  
            {  
                name: '服务报价',  
                value: '¥19999.9',  
                reward: false  
            },  
            {  
                name: '订单号',  
                value: '202020448476474748484848',  
                reward: true  
            }  
        ]  
    }],  
    takeout: [{  
        takeoutName: '待取外卖',  
        status: '已取消',  
        content: [{  
                img: 'https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1204/1f1ec202512041157416797.png',  
                name: '详细地址',  
                value: '12034809810'  
            },  
            {  
                img: 'https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1204/9a939202512041334109414.png',  
                name: '详细地址',  
                value: '12034809810'  
            }  
        ]  
    }],  
    petServices: [{  
        takeoutName: '宠物待遛',  
        status: '待服务',  
        content: [{  
            img: 'https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1204/4731a202512041345189036.png',  
            name: '详细地址',  
            value: '12034809810'  
        }]  
    }]  
})  

const productList = ref([{  
        image: "https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1203/42567202512030927251507.png",  
        title: "商品标题商品标题商品标题商品标题商品标题商品标题",  
        price: 19999.9  
    },  
    {  
        image: "https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1203/42567202512030927251507.png",  
        title: "第二个商品标题商品标题",  
        price: 299.9  
    },  
    {  
        image: "https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1203/42567202512030927251507.png",  
        title: "第三个商品标题",  
        price: 88  
    },  
    {  
        image: "https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1203/42567202512030927251507.png",  
        title: "第三个商品标题",  
        price: 88  
    },  
    {  
        image: "https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1203/42567202512030927251507.png",  
        title: "第三个商品标题",  
        price: 88  
    }  
])  
// WebSocket相关  
const chatId = ref('');  
let clientId = ''  
let merchantId = 0  
let goodsId = 0  

// 页面参数  
const pageParams = ref({})  
const productInfo = ref(null)  
const serviceOrderInfo = ref(null)  

const getCustomerInformation = async () => {  
    const res = await addOfficial();  
    console.log(res.result.friend_info);  
    if (res.code === 200) {  
        friendInfo.value = res.result.friend_info;  
        userInfo.value = res.result.user_info;  
        chatId.value = res.result.chat_id;  
    }  

}  

// 页面加载  
onLoad((options) => {  
    getCustomerInformation();  
})  

// 页面显示  
onShow(async () => {  
    await getCustomerInformation();  
    await initPage()  
})  

// WebSocket回调函数引用  
let wsCallback = null  

// 添加WebSocket消息监听器  
const addWebSocketListener = () => {  
    wsCallback = async (data) => {  
        console.log('wsCallback', data)  
        // 检查是否是发送者自己的消息(避免重复显示)  
        if (data.from === userInfo.value.id) {  
            // 检查是否已经存在相同的消息(避免重复)  
            const messageExists = messageList.value.some(msg =>  
                msg.content === data.content &&  
                msg.created_at === data.time &&  
                msg.uid === data.from  
            )  

            if (messageExists) {  
                return // 消息已存在,忽略  
            }  
        }  

        // 检查是否是当前聊天的消息  
        if (data.chat_id && data.chat_id !== chatId.value) {  
            // 不是当前聊天的消息,忽略  
            return  
        }  

        // 对于自己的消息,已经在上面检查过了,这里只检查非自己的消息  
        if (data.from !== userInfo.value.id) {  
            const messageExists = messageList.value.some(msg =>  
                msg.content === data.content &&  
                msg.created_at === data.time &&  
                msg.uid === data.from  
            )  

            if (messageExists) {  
                return // 消息已存在,忽略  
            }  
        }  

        // 解析消息内容  
        let messageContent = data.content  
        let messageType = data.type || 1  
        let productInfo = null  
        let serviceOrderInfo = null  

        // 添加新消息到列表  
        messageList.value.push({  
            uid: data.from,  
            content: messageContent,  
            created_at: data.time || Math.floor(Date.now() / 1000), // 使用秒级时间戳  
            type: messageType,  
            status: 0,  
            product_info: productInfo,  
            service_order_info: serviceOrderInfo  
        })  

        // 使用锚点滚动到最新消息,避免被输入框遮挡  
        nextTick(() => {  
            scrollIntoViewId.value = '' + (messageList.value.length - 1)  
        })  
    }  

    wsStore.subscribe('chat', wsCallback)  
}  

// 移除WebSocket消息监听器  
const removeWebSocketListener = () => {  
    if (wsCallback) {  
        wsStore.unsubscribe('chat', wsCallback)  
        wsCallback = null  
    }  
}  

// 初始化页面  
const initPage = async () => {  
    try {  
        console.log('chatId:', chatId.value); // 检查 chatId  
        // 检查chat_id参数  
        if (!chatId.value) {  
            uni.showToast({  
                title: '聊天ID无效',  
                icon: 'none'  
            });  
            return;  
        }  

        console.log('storeUserInfo:', storeUserInfo.value); // 打印 storeUserInfo  
        // 使用store中的用户信息  
        userInfo.value = storeUserInfo.value || {};  

        // 如果store中没有用户信息,尝试获取  
        if (!userInfo.value.id) {  
            const userRes = await getUserInfo();  
            userInfo.value = userRes.data || {};  
        }  

        // 确保WebSocket已初始化  
        console.log('WebSocket initialized:', wsStore.ws); // 检查 WebSocket  
        if (!wsStore.ws) {  
            wsStore.init();  
        }  

        // 添加WebSocket消息监听器  
        addWebSocketListener();  

        // 加载聊天记录  
        await loadMessages();  

    } catch (error) {  
        console.error('Initialization failed:', error); // 打印异常  
        uni.showToast({  
            title: '初始化失败',  
            icon: 'none'  
        });  
    }  
}  

// 监听 WebSocket 消息  
const handleChatMessage = (payload) => {  
    console.log('--handleChatMessage--payload--', payload)  
}  

// 点击切换语音还是文字按钮  
const switchVoiceText = () => {  
    if (switchVoiceTextStatus.value ===  
        'https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1202/b601a202512020937515649.png') {  
        switchVoiceTextStatus.value =  
            'https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1202/ea63f202512020949292692.png';  
        switchStatus.value = 2;  
    } else {  
        switchVoiceTextStatus.value =  
            'https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1202/b601a202512020937515649.png';  
        switchStatus.value = 1;  
    }  
    showEmojiPanel.value = false;  
    moreServe.value = false;  
    moreFeaturesStatus.value =  
        'https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1202/5831a202512020940542856.png';  

}  

const toggleEmojiPanel = () => {  
    showEmojiPanel.value = !showEmojiPanel.value;  
    inputFocus.value = false;  
    moreServe.value = false;  
    moreFeaturesStatus.value =  
        'https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1202/5831a202512020940542856.png';  
}  

const selectNativeEmoji = (emoji) => {  
    inputMessage.value += emoji;  
}  

const moreFeatures = () => {  
    if (moreFeaturesStatus.value ===  
        'https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1202/5831a202512020940542856.png') {  

        moreFeaturesStatus.value =  
            'https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1202/00b00202512021539127978.png';  

    } else {  
        moreFeaturesStatus.value =  
            'https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1202/5831a202512020940542856.png';  
    }  

    moreServe.value = !moreServe.value;  
    showEmojiPanel.value = false;  
}  

const orderShow = () => {  
    showMask.value = true;  
    showEmojiPanel.value = false;  
    moreServe.value = false;  
    moreFeaturesStatus.value =  
        'https://linshitong.oss-cn-shenzhen.aliyuncs.com/dev/image/2025/1202/5831a202512020940542856.png';  
}  

const closed = () => {  
    showMask.value = false;  
}  

const selectOrderShop = (e) => {  
    console.log(e.index);  
    orderShopId.value = e.index;  
}  

// 从相册选择图片  
const selectFromAlbum = () => {  
    uni.chooseImage({  
        count: 1,  
        sourceType: ['album'],  
        sizeType: ['original', 'compressed'],  
        success: async (res) => {  
            console.log('图片上传成功', res);  
            const tempFile = res.tempFiles[0];  
            await uploadImages(tempFile);  
        },  
        fail: (err) => {  
            console.error('选择图片失败:', err)  
            uni.showToast({  
                title: '选择图片失败',  
                icon: 'none'  
            })  
        }  
    })  
};  

// 上传图片到服务器  
const uploadImages = async (tempFile) => {  
    console.log(tempFile);  
    uni.showLoading({  
        title: '上传中...'  
    });  
    try {  
        // 调试上传请求  
        console.log('开始上传', tempFile);  

        const result = await uploadImage({  
            name: 'file',  
            filePath: tempFile.path  
        });  

        console.log('上传结果', result); // 查看上传结果  
        if (result.code === 0) {  
            await sendMediaMessage({  
                type: 4, // 图片消息类型  
                content: JSON.stringify({  
                    image_url: result.data,  
                })  
            })  
        }  

        uni.showToast({  
            title: '上传成功',  
            icon: 'success'  
        });  

    } catch (err) {  
        console.error('上传失败', err);  
        uni.showToast({  
            title: '上传失败',  
            icon: 'none'  
        });  
    } finally {  
        // 确保始终隐藏加载状态  
        uni.hideLoading();  
    }  
};  

// 预览图片  
const previewImage = (imageUrl) => {  
    uni.previewImage({  
        current: imageUrl,  
        urls: [imageUrl]  
    })  
}  

// 拍摄视频  
const takeVideo = () => {  
    uni.chooseVideo({  
        sourceType: ['camera'],  
        maxDuration: 60, // 最大拍摄时长60秒  
        camera: 'back',  
        success: async (res) => {  
            console.log('拍摄视频成功', res);  
            await uploadVideo(res.tempFilePath, res.tempThumbPath)  
        },  
        fail: () => {  
            uni.showToast({  
                title: '拍摄视频失败',  
                icon: 'none'  
            })  
        }  
    })  
}  

// 上传视频到服务器  
const uploadVideo = async (tempVideoPath, tempCoverPath) => {  
    console.log(tempVideoPath);  
    uni.showLoading({  
        title: '视频上传中...'  
    })  

    try {  

        // 获取视频信息(支持 H5 / APP / 小程序)  
        const videoInfo = (videoPath) => {  
            console.log(videoPath);  
            return new Promise((resolve, reject) => {  
                // APP / 小程序原生支持  
                //#ifdef APP || MP-WEIXIN  
                uni.getVideoInfo({  
                    src: videoPath,  
                    success: resolve,  
                    fail: (err) => reject(new Error(`uni.getVideoInfo 失败:${err.errMsg}`))  
                })  
                //#endif  

                // H5:使用 <video> 对象获取  
                //#ifdef H5  
                try {  
                    const video = document.createElement('video')  
                    video.src = videoPath  
                    video.crossOrigin = 'anonymous' // 如果跨域,必须加  

                    video.onloadedmetadata = () => {  
                        resolve({  
                            width: video.videoWidth || 280,  
                            height: video.videoHeight || 350,  
                            duration: video.duration || 0,  
                            orientation: video.videoWidth > video.videoHeight ?  
                                'landscape' : 'portrait'  
                        })  
                    }  

                    video.onerror = () => {  
                        reject(new Error('H5 获取视频信息失败'))  
                    }  
                } catch (e) {  
                    reject(new Error('H5 获取视频信息异常:' + e.message))  
                }  
                //#endif  
            })  
        }  

        // 2. 计算显示尺寸  
        let width = videoInfo.width > 0 ? videoInfo.width : 280  
        let height = videoInfo.height > 0 ? videoInfo.height : 350  
        const scale = Math.min(280 / width, 350 / height, 1)  
        width = Math.floor(width * scale)  
        height = Math.floor(height * scale)  

        const res = await videoUpload({  
            name: 'file',  
            filePath: tempVideoPath  
        })  

        // 5. 接口响应处理  
        console.log('视频上传接口响应:', res)  

        if (res.code !== 0) {  
            uni.showToast({  
                title: '上传文件过大',  
                icon: 'none'  
            });  
            return  
        }  

        if (res.code === 0 && res?.data) {  
            await sendMediaMessage({  
                type: 5,  
                content: JSON.stringify({  
                    video_url: res.data.url || res.data,  
                    cover_url: res.data.cover_url || tempCoverPath,  
                    width,  
                    height,  
                    duration: Math.floor(videoInfo.duration)  
                }),  
                video_url: res.data.url || res.data,  
                cover_url: res.data.cover_url || tempCoverPath,  
                video_width: width,  
                video_height: height  
            })  
            uni.showToast({  
                title: '视频上传成功',  
                icon: 'success'  
            })  
        } else {  
            throw new Error(res.msg || '视频上传失败:未返回视频URL')  
        }  
    } catch (error) {  
        console.error('视频上传完整错误:', error)  
        uni.showToast({  
            title: error.message || '视频上传失败',  
            icon: 'none',  
            duration: 2000  
        })  
    } finally {  
        uni.hideLoading()  
    }  
}  

// 播放视频  
const playVideo = (url) => {  
    videoUrl.value = url;  
}  

// ==================== 小程序录音初始化 ====================  
const initMpRecorder = () => {  
    recorderManager.value = uni.getRecorderManager()  
    // 小程序录音开始回调  
    recorderManager.value.onStart(() => {  
        isRecording.value = true  
        uni.showToast({  
            title: '开始录音...',  
            icon: 'none',  
            duration: 1000  
        })  
    })  

    // 小程序录音停止回调  
    recorderManager.value.onStop(async (res) => {  
        isRecording.value = false  
        if (res.duration < 1000) {  
            uni.showToast({  
                title: '录音时间太短',  
                icon: 'none'  
            })  
            return  
        }  
        await uploadVoice(res.tempFilePath, res.duration)  
    })  

    // 小程序录音错误回调  
    recorderManager.value.onError((err) => {  
        isRecording.value = false  
        console.error('小程序录音失败:', err)  
        uni.showToast({  
            title: '录音失败',  
            icon: 'none'  
        })  
    })  
}  

// ==================== H5录音初始化 ====================  
const initH5Recorder = async () => {  
    try {  
        // 检测浏览器支持性  
        if (!window.MediaRecorder || !navigator.mediaDevices?.getUserMedia) {  
            uni.showToast({  
                title: '当前浏览器不支持录音功能',  
                icon: 'none'  
            })  
            return false  
        }  

        alert('支持录音')  

        // 浏览器不支持强制设置sampleRate/bitrate,仅保留channelCount  
        audioStream.value = await navigator.mediaDevices.getUserMedia({  
            audio: {  
                channelCount: 1, // 仅保留单声道(部分浏览器支持)  
                echoCancellation: true, // 降噪(提升体验)  
                noiseSuppression: true  
            }  
        })  

        // ========== 修复2:适配不同浏览器的编码格式 ==========  
        const isWechat = /MicroMessenger/i.test(navigator.userAgent);  
        const isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent);  
        let mimeType = '';  

        // 优先级:微信iOS → 微信安卓 → Chrome → 兜底  
        if (isWechat && isIOS) {  
            // iOS微信:优先mp4(mpeg4aac)  
            mimeType = MediaRecorder.isTypeSupported('audio/mp4') ?  
                'audio/mp4' :  
                MediaRecorder.isTypeSupported('audio/wav') ? 'audio/wav' : '';  
        } else if (isWechat) {  
            // 安卓微信:优先webm  
            mimeType = MediaRecorder.isTypeSupported('audio/webm') ?  
                'audio/webm' :  
                MediaRecorder.isTypeSupported('audio/wav') ? 'audio/wav' : '';  
        } else {  
            // Chrome:优先webm  
            mimeType = MediaRecorder.isTypeSupported('audio/webm') ?  
                'audio/webm' :  
                MediaRecorder.isTypeSupported('audio/mp3') ? 'audio/mp3' : '';  
        }  

        // 无可用编码格式  
        if (!mimeType) {  
            uni.showToast({  
                title: '当前浏览器不支持音频编码',  
                icon: 'none'  
            })  
            return false;  
        }  

        // 创建MediaRecorder实例(使用适配后的编码)  
        h5Recorder.value = new MediaRecorder(audioStream.value, {  
            mimeType  
        });  
        const audioChunks = []  

        // H5录音数据收集  
        h5Recorder.value.ondataavailable = (e) => {  
            if (e.data.size > 0) {  
                audioChunks.push(e.data)  
            }  
        }  

        // H5录音停止回调  
        h5Recorder.value.onstop = async () => {  
            isRecording.value = false  
            const duration = h5Recorder.value?.startTime ?  
                Date.now() - h5Recorder.value.startTime :  
                0  

            if (duration < 1000) {  
                uni.showToast({  
                    title: '录音时间太短',  
                    icon: 'none'  
                })  
                audioChunks.length = 0  
                return  
            }  

            // ========== 修复3:根据实际编码设置文件名后缀 ==========  
            const ext = mimeType.split('/')[1]; // webm/mp4/wav/mp3  
            const audioBlob = new Blob(audioChunks, {  
                type: mimeType  
            })  
            const tempFilePath = URL.createObjectURL(audioBlob)  

            await uploadVoice(tempFilePath, duration, audioBlob, ext) // 传递后缀  
            audioChunks.length = 0  
        }  

        h5Recorder.value.onerror = (err) => {  
            isRecording.value = false  
            console.error('H5录音失败:', err)  
            uni.showToast({  
                title: '录音失败',  
                icon: 'none'  
            })  
        }  

        // ========== 修复4:微信浏览器权限引导 ==========  
        if (isWechat) {  
            uni.showToast({  
                title: '请允许麦克风权限后录音',  
                icon: 'none',  
                duration: 2000  
            })  
        }  

        return true  
    } catch (err) {  
        console.error('H5录音初始化失败:', err)  
        // ========== 修复5:区分权限失败和其他错误 ==========  
        if (err.name === 'NotAllowedError') {  
            // 权限被拒绝  
            if (/MicroMessenger/i.test(navigator.userAgent)) {  
                uni.showToast({  
                    title: '请点击右上角···→允许使用麦克风',  
                    icon: 'none',  
                    duration: 3000  
                })  
            } else {  
                uni.showToast({  
                    title: '麦克风权限被拒绝,请手动授权',  
                    icon: 'none',  
                    duration: 3000  
                })  
            }  
        } else if (err.name === 'NotFoundError') {  
            uni.showToast({  
                title: '未检测到麦克风设备',  
                icon: 'none'  
            })  
        } else {  
            uni.showToast({  
                title: '获取麦克风权限失败',  
                icon: 'none'  
            })  
        }  
        return false  
    }  
}  

// ==================== 统一录音初始化入口 ====================  
const initRecorderManager = async () => {  
    if (isH5()) {  
        // H5环境初始化录音  
        await initH5Recorder()  
    } else {  
        // 小程序环境初始化录音  
        initMpRecorder()  
    }  
}  

// ==================== 音频播放初始化(兼容H5) ====================  
const initInnerAudioContext = () => {  
    if (isH5()) {  
        // H5使用HTML5 Audio  
        innerAudioContext.value = new Audio()  
        innerAudioContext.value.onplay = () => {  
            currentPlayingVoice.value = innerAudioContext.value.src  
        }  
        innerAudioContext.value.onended = () => {  
            currentPlayingVoice.value = ''  
        }  
        innerAudioContext.value.onerror = () => {  
            currentPlayingVoice.value = ''  
            uni.showToast({  
                title: '播放失败',  
                icon: 'none'  
            })  
        }  
    } else {  
        // 小程序使用内置音频上下文  
        innerAudioContext.value = uni.createInnerAudioContext()  
        innerAudioContext.value.onPlay(() => {  
            currentPlayingVoice.value = innerAudioContext.value.src  
        })  
        innerAudioContext.value.onEnded(() => {  
            currentPlayingVoice.value = ''  
        })  
        innerAudioContext.value.onError(() => {  
            currentPlayingVoice.value = ''  
            uni.showToast({  
                title: '播放失败',  
                icon: 'none'  
            })  
        })  
    }  
}  

// ==================== 开始录音(统一接口) ====================  
const startRecord = async () => {  
    // 非语音模式不录音  
    if (switchStatus.value === 1) return  

    // H5环境  
    if (isH5()) {  
        if (!h5Recorder.value) {  
            const initSuccess = await initH5Recorder()  
            if (!initSuccess) return  
        }  
        // 开始H5录音  
        h5Recorder.value.start()  
        h5Recorder.value.startTime = Date.now() // 记录开始时间(计算时长)  
        isRecording.value = true  
        uni.showToast({  
            title: '开始录音...',  
            icon: 'none',  
            duration: 1000  
        })  
    } else {  
        // 小程序环境  
        recorderManager.value.start({  
            sampleRate: 16000,  
            numberOfChannels: 1,  
            format: 'mp3',  
            bitRate: 48000  
        })  
    }  
}  

// ==================== 停止录音(统一接口) ====================  
const stopRecord = () => {  
    if (!isRecording.value) return  

    if (isH5()) {  
        // H5停止录音  
        if (h5Recorder.value?.state === 'recording') {  
            h5Recorder.value.stop()  
        }  
        // 释放音频流  
        if (audioStream.value) {  
            audioStream.value.getTracks().forEach(track => track.stop())  
        }  
    } else {  
        // 小程序停止录音  
        recorderManager.value.stop()  
    }  
}  

// ==================== 上传语音(兼容H5文件格式) ====================  
const uploadVoice = async (tempFilePath, duration, audioBlob = null) => {  
    console.log(tempFilePath);  
    uni.showLoading({  
        title: '语音上传中...'  
    })  
    try {  
        const msg = await videoUpload({  
            name: 'file',  
            filePath: tempFilePath  
        })  

        console.log('msg',msg);  
        const result = await audioUplaod({  
            file_url: msg.data.url  
        })  
        console.log("上传结果:", result)  

        if (result.code !== 200) {  
            throw new Error('上传失败:audioUpload 返回错误')  
        }  

        const res = await getFileTaskResult({  
            task_id: result.result.task_id  
        })  

        console.log("任务结果:", res)  

        if (res.code !== 200) {  
            throw new Error('上传失败:getFileTaskResult 返回错误')  
        }  

        // 上传成功,发送语音  
        await sendMediaMessage({  
            type: 6,  
            content: JSON.stringify({  
                voice_url: res.result.full_text,  
                duration: Math.floor(duration / 1000)  
            }),  
            voice_url: res.result.voice_url,  
            voice_duration: Math.floor(duration / 1000)  
        })  

    } catch (error) {  
        console.error('语音上传失败:', error)  
        uni.showToast({  
            title: '语音上传失败',  
            icon: 'none'  
        })  
    } finally {  
        uni.hideLoading()  
        if (isH5() && tempFilePath.startsWith('blob:')) {  
            URL.revokeObjectURL(tempFilePath)  
        }  
    }  
}  

// ==================== 播放语音(兼容H5) ====================  
const playVoice = (voiceUrl) => {  
    if (currentPlayingVoice.value === voiceUrl) {  
        // 暂停当前播放  
        if (isH5()) {  
            innerAudioContext.value.pause()  
            innerAudioContext.value.currentTime = 0  
        } else {  
            innerAudioContext.value.stop()  
        }  
        currentPlayingVoice.value = ''  
        return  
    }  

    // 播放新语音  
    if (isH5()) {  
        innerAudioContext.value.src = voiceUrl  
        innerAudioContext.value.play().catch(err => {  
            console.error('H5播放失败:', err)  
            uni.showToast({  
                title: '播放失败',  
                icon: 'none'  
            })  
        })  
    } else {  
        innerAudioContext.value.stop()  
        innerAudioContext.value.src = voiceUrl  
        innerAudioContext.value.play()  
    }  
}  

// ==================== 生命周期清理 ====================  
const cleanupRecorder = () => {  
    // 停止录音  
    if (isRecording.value) {  
        stopRecord()  
    }  
    // 释放H5资源  
    if (isH5()) {  
        if (audioStream.value) {  
            audioStream.value.getTracks().forEach(track => track.stop())  
        }  
        h5Recorder.value = null  
        audioStream.value = null  
        // 停止播放  
        if (innerAudioContext.value) {  
            innerAudioContext.value.pause()  
            innerAudioContext.value.src = ''  
        }  
    } else {  
        // 小程序清理  
        if (innerAudioContext.value) {  
            innerAudioContext.value.destroy()  
        }  
    }  
}  

// 发送媒体消息(图片/视频/语音)  
const sendMediaMessage = async (messageData) => {  
    console.log('131454', JSON.parse(messageData.content));  
    try {  
        // 调用发送消息接口  
        await apiSendMessage({  
            friend_id: friendInfo.value.id,  
            content: messageData.content,  
            type: messageData.type,  
            chat_id: chatId.value  
        })  

        // 添加到本地列表  
        messageList.value.push({  
            uid: userInfo.value.id,  
            content: messageData.type === 4 ? '[图片]' : messageData.type === 5 ? '[视频]' :(messageData.type === 6?'[语音]':'') ,  
            created_at: Math.floor(Date.now() / 1000),  
            type: messageData.type,  
            status: 0,  
            image_url: JSON.parse(messageData.content).image_url || '',  
            image_width: JSON.parse(messageData.content).image_width || 0,  
            image_height: JSON.parse(messageData.content).image_height || 0,  
            video_url: messageData.video_url || '',  
            cover_url: messageData.cover_url || '',  
            // video_width: messageData.video_width || 0,  
            // video_height: messageData.video_height || 0,  
            voice_url: JSON.parse(messageData.content).voice_url || '',  
            voice_duration: messageData.content.voice_duration || 0  
        })  

        // 滚动到最新消息  
        nextTick(() => {  
            scrollIntoViewId.value = '' + (messageList.value.length - 1)  
            setTimeout(() => scrollToBottom(), 200)  
        })  
    } catch (error) {  
        console.error('发送媒体消息失败:', error)  
        uni.showToast({  
            title: '发送失败',  
            icon: 'none'  
        })  
    }  
}  

// 加载聊天记录  
const loadMessages = async (previousScrollHeight = 0, previousScrollTop = 0) => {  
    if (loading.value || !hasMore.value) return  

    loading.value = true  

    try {  
        const res = await getChatLists({  
            chat_id: chatId.value,  
            page: page.value,  
            limit: 50  
        })  

        if (res.code === 200) {  
            const messages = res.result || [] // 处理消息,解析商品信息和服务订单信息  
            const processedMessages = messages.map(message => {  

                // 处理图片消息  
                if (message.type === 4) {  
                    try {  
                        const parsed = JSON.parse(message.content)  
                        return {  
                            ...message,  
                            image_url: parsed.image_url,  
                            image_width: parsed.width || 400,  
                            image_height: parsed.height || 400  
                        }  
                    } catch (e) {  
                        return message  
                    }  
                }  
                // 处理视频消息  
                else if (message.type === 5) {  
                    try {  
                        const parsed = JSON.parse(message.content)  
                        return {  
                            ...message,  
                            video_url: parsed.video_url,  
                            cover_url: parsed.cover_url || '/static/images/bgUrl.png',  
                            video_width: 400,  
                            video_height: 400  
                        }  
                    } catch (e) {  
                        return message  
                    }  
                }  
                // 处理语音消息  
                else if (message.type === 6) {  
                    try {  
                        const parsed = JSON.parse(message.content)  
                        return {  
                            ...message,  
                            voice_url: parsed.voice_url,  
                            voice_duration: parsed.duration || 0  
                        }  
                    } catch (e) {  
                        return message  
                    }  
                }  

                if (message.type === 2) {  
                    // 商品消息,解析content中的JSON  
                    try {  
                        const productData = JSON.parse(message.content)  
                        return {  
                            ...message,  
                            product_info: {  
                                id: productData.product_id,  
                                image: productData.product_image,  
                                price: productData.product_price,  
                                name: productData.product_name  
                            }  
                        }  
                    } catch (error) {  
                        console.error('解析商品消息失败:', error)  
                        return message  
                    }  
                } else if (message.type === 3) {  
                    // 服务订单消息,解析content中的JSON  
                    try {  
                        const serviceOrderData = JSON.parse(message.content)  
                        return {  
                            ...message,  
                            service_order_info: {  
                                order_id: serviceOrderData.order_id,  
                                order_no: serviceOrderData.order_no,  
                                product_name: serviceOrderData.product_name,  
                                service_time: serviceOrderData.service_time,  
                                address_address: serviceOrderData.address_address,  
                                total_price: serviceOrderData.total_price,  
                                status: serviceOrderData.status  
                            }  
                        }  
                    } catch (error) {  
                        console.error('解析服务订单消息失败:', error)  
                        return message  
                    }  
                }  
                return message  
            })  

            if (page.value === 1) {  
                // 第一页:显示最新消息,直接替换  
                messageList.value = processedMessages  

                nextTick(() => {  
                    // 立即尝试滚动  
                    scrollToBottom()  

                    // 延迟滚动确保渲染完成  
                    setTimeout(() => {  
                        scrollToBottom()  
                    }, 100)  

                    // 再次延迟确保滚动到底部  
                    setTimeout(() => {  
                        scrollToBottom()  
                    }, 300)  
                })  
            } else {  
                // 后续页:静默加载历史消息,添加到前面  
                messageList.value = [...processedMessages, ...messageList.value]  

                if (previousScrollHeight > 0) {  
                    nextTick(() => {  
                        const query = uni.createSelectorQuery()  
                        query.select('.chat-content').scrollOffset()  
                        query.exec((res) => {  
                            if (res[0]) {  
                                const newScrollHeight = res[0].scrollHeight  
                                console.log(newScrollHeight);  
                                const newScrollTop = newScrollHeight - previousScrollHeight +  
                                    previousScrollTop  
                                scrollTop.value = newScrollTop  
                            }  
                        })  
                    })  
                }  
            }  

            // 检查是否还有更多数据  
            hasMore.value = messages.length === 50  
        }  

    } catch (error) {  
        // 加载失败,忽略  
    } finally {  
        loading.value = false  
    }  
}  

// 加载更多消息(向上滑动加载历史消息)  
const loadMoreMessages = () => {  
    if (hasMore.value && !loading.value) {  
        // 同步记录当前滚动高度和位置  
        const query = uni.createSelectorQuery()  
        query.select('.chat-content').scrollOffset()  
        query.exec((res) => {  
            if (res[0]) {  
                const previousScrollHeight = res[0].scrollHeight  
                const previousScrollTop = res[0].scrollTop  
                page.value++  
                loadMessages(previousScrollHeight, previousScrollTop)  
            }  
        })  
    }  
}  

// 滚动事件处理  
const onScroll = (e) => {  
    // 静默处理,不做任何操作  
}  

// 发送商品消息  
const sendProductMessage = async () => {  
    if (!productInfo.value) return  

    try {  
        // 发送商品消息到服务器  
        const res = await apiSendMessage({  
            friend_id: friendInfo.value.id,  
            content: {  
                product_id: productInfo.value.id,  
                product_image: productInfo.value.image,  
                product_price: productInfo.value.price,  
                product_name: productInfo.value.name  
            },  
            type: 2, // 传递消息类型给后端  
            chat_id: chatId.value  
        })  
        console.log(res);  

        // 添加到本地列表(发送者自己的消息)  
        messageList.value.push({  
            uid: userInfo.value.id,  
            content: `[商品] ${productInfo.value.name}`,  
            created_at: Date.now(),  
            type: 2, // 商品消息类型  
            status: 0,  
            product_info: productInfo.value  
        })  

        // 立即隐藏商品卡片  
        productInfo.value = null  

        // 立即滚动到新消息  
        nextTick(() => {  
            setTimeout(() => {  
                scrollIntoViewId.value = '' + (messageList.value.length - 1)  
                // 备用滚动方案  
                setTimeout(() => {  
                    scrollTop.value = 999999  
                }, 50)  
            }, 50)  
        })  

    } catch (error) {  
        uni.showToast({  
            title: '发送失败',  
            icon: 'none'  
        })  
    }  
}  

// 发送消息  
const sendMessage = async () => {  
    const message = inputMessage.value.trim();  
    console.log(message);  
    if (!message) {  
        uni.showToast({  
            title: '请输入消息内容',  
            icon: 'none',  
            duration: 1500  
        })  
        return  
    }  

    inputMessage.value = '';  
    console.log(friendInfo.value.id);  
    console.log(message);  
    console.log(chatId.value);  
    try {  
        // 发送到服务器  
        await apiSendMessage({  
            friend_id: friendInfo.value.id,  
            content: message, // 直接发送消息内容  
            type: 1, // 传递消息类型给后端  
            chat_id: chatId.value  
        })  

        // 添加到本地列表(发送者自己的消息)  
        messageList.value.push({  
            uid: userInfo.value.id,  
            content: message,  
            created_at: Date.now(),  
            type: 1,  
            status: 0  
        })  

        // 立即滚动到新消息  
        nextTick(() => {  
            setTimeout(() => {  
                scrollIntoViewId.value = '' + (messageList.value.length - 1)  
                // 备用滚动方案  
                setTimeout(() => {  
                    scrollTop.value = 999999  
                }, 50)  
            }, 50)  
        })  

    } catch (error) {  
        uni.showToast({  
            title: '发送失败',  
            icon: 'none'  
        })  
    }  
}  

const clearInputMessage = () => {  
    if (!inputMessage.value) return; // 空值直接返回  

    // 关键:按 Unicode 码点分割,避免拆分 Emoji  
    const codePoints = Array.from(inputMessage.value);  
    // 删除最后一个码点(完整删除一个Emoji/文字)  
    codePoints.pop();  
    // 重新拼接字符串  
    inputMessage.value = codePoints.join('');  
}  

// 发送服务订单信息  
const sendServiceOrderMessage = async () => {  
    if (!serviceOrderInfo.value) return  

    try {  
        // 发送服务订单信息到服务器  
        await apiSendMessage({  
            friend_id: friendInfo.value.id,  
            content: JSON.stringify(serviceOrderInfo.value),  
            type: 3, // 服务订单消息类型  
            chat_id: chatId.value  
        })  

        // 添加到本地列表(发送者自己的消息)  
        messageList.value.push({  
            uid: userInfo.value.id,  
            content: `[服务订单] ${serviceOrderInfo.value.product_name}`,  
            created_at: Date.now(),  
            type: 3, // 服务订单消息类型  
            status: 0,  
            service_order_info: serviceOrderInfo.value  
        })  

        // 立即隐藏服务订单卡片  
        serviceOrderInfo.value = null  

        // 立即滚动到新消息  
        nextTick(() => {  
            setTimeout(() => {  
                scrollIntoViewId.value = '' + (messageList.value.length - 1)  
                // 备用滚动方案  
                setTimeout(() => {  
                    scrollTop.value = 999999  
                }, 50)  
            }, 50)  
        })  

    } catch (error) {  
        uni.showToast({  
            title: '发送失败',  
            icon: 'none'  
        })  
    }  
}  

const scrollToBottom = () => {  
    // 使用nextTick确保DOM更新完成  
    nextTick(() => {  
        // 方案1:使用scroll-into-view滚动到最后一个消息  
        if (messageList.value.length > 0) {  
            scrollIntoViewId.value = '' + (messageList.value.length - 1)  
        }  

        // 方案2:使用scrollTop作为备用方案  
        setTimeout(() => {  
            scrollTop.value = 999999  
        }, 10)  

        // 方案3:再次确保滚动到底部  
        setTimeout(() => {  
            scrollTop.value = 999999  
        }, 50)  

        // 方案
2025-12-09 10:48 负责人:无 分享
已邀请:
爱豆豆

爱豆豆 - 办法总比困难多

你好 运行的那个端?可以发一个能直接运行的demo吗?

要回复问题请先登录注册