<template>
<view class="chatIndex">
<custom-navbar title="WONSOU" :showBack="true" :background="'#F2F2F2'">
<template #left>
<view class="flexCC">
<image src="/static/images/Back@2x.png" class="back-icon" @click="onLeftClick"></image>
<!-- -->
</view>
</template>
<!-- 自定义右侧 slot -->
<template #right>
<image src="/static/images/Dialogue@2x.png" class="icon-more" @click="agrenchatInfo"></image>
</template>
</custom-navbar>
<u-skeleton :loading="loadingskeleton" :animate="true" rows="5" :rowsHeight="200"></u-skeleton>
<scroll-view style="flex:1;padding:24rpx 32rpx 100rpx 32rpx;height: calc(100vh - 450rpx);position: relative;"
:style="inputType === 'text'?'':'height: calc(100vh - 400rpx);'" scroll-y :show-scrollbar="false"
:scroll-top="scrollTop" refresher-enabled :refresher-triggered="refresherTriggered"
:refresher-background="'transparent'" @refresherrefresh="refresherrefresh" @scrolltolower="scrolltolower"
@scroll="scroll">
<view class="flex">
<image src="/static/images/chatindexHeader.png" mode="" class="chatIndexLogo"></image>
<view class="chatIndexLogoText">
<view class="chatIndexLogoTextHi">
hi,
</view>
<view class="chatIndexLogoText2">
我是小W
</view>
</view>
</view>
<view class="chatIndex_card" v-if="getchatUnreadCountInfo.messages.length > 0">
<view class="flexCAB">
<view class="chatIndex_card_tips">
{{getchatUnreadCountInfo.title}}
</view>
<view class="flexC" @click="getchatUnreadCount">
<image src="/static/images/changechatindex.png" mode="" class="changechatindex_icon"
:class="{ spinning: isSpinning }" @click="uploadUnreadCountInfo"></image>
<view class="changechatindex_text" @click="uploadUnreadCountInfo">
换一换
</view>
</view>
</view>
<view class="cardItem flex" v-for="(item,index) in getchatUnreadCountInfo.messages" :key="index"
@click="getNoice(item.content)">
<image :src="item.icon?'https://www.wonsouai.com/static/'+item.icon:'/static/images/new@2x.png'"
mode="widthFix" class="cardItem_img"></image>
<view class="cardItem_text">
{{item.content}}
</view>
</view>
</view>
<view class="" v-for="(item,index) in chatList" :key="index">
<styleTemplate :chatInfo="item" @add-shop-cat="addShopCatInfo" @refresh-answe="RefreshAnsweClick"
:makedowType="makedowType" :isLastData="index==chatList.length-1?true:false"
@delChatList="delChatList" @erragrenChatinfo="erragrenChatinfo" :openLoading="loading"
@ascenrClick="ascenrClick">
</styleTemplate>
</view>
</scroll-view>
<view class="sublimt flexCCC" :style="isFocus?`bottom:` + inputBottom + 'rpx;background:transparent':''">
<image src="/static/images/Down@3x.png" mode="" class="downloadScooltop" @click="scrollTopDown"
v-if="isShowDownload" :style="inputType === 'text'?'':'top:-80rpx'"></image>
<view class="shopcat flexCCC" v-if="inputType === 'text'">
<view class="shopcatNumInfo">
<image src="/static/images/Shopping_cart@2x.png" class="shopcat_img" mode=""></image>
<view class="shopcatNum" v-if="shopCatNum>0">
{{shopCatNum>99?'99+':shopCatNum}}
</view>
</view>
<view class="shopcat_text" @click="jumpShopcat()">
购物车
</view>
</view>
<view class="SandTypeInfo" :style="isCancel?'color:#F2525E;':'color:#333333'" v-if="videoSandType">
{{isCancel?'松手取消':'松手发送,上移取消'}}
</view>
<view class="index_button"
:style="videoSandType&&!isCancel ? 'background: linear-gradient( 94deg, #8128FF 2%, #2866F6 57%);' :(videoSandType&&isCancel?'background: #F2525E;':'')">
<!-- 文本输入 -->
<view class="flexCAB" v-if="inputType === 'text'">
<image src="/static/images/voice@2x.png" @click="changeVlaueType" class="index_button_img" />
<input type="text" v-model="value" class="inputValue" @focus="isFocus = true"
@blur="isFocus = false" :adjust-position="false" @keyboardheightchange="keyboardheightchange"
maxlength="500" />{{RefreshId.value}}
<view class="flexC">
<!-- <image :src="RefreshId.value!==''?'/static/images/send@2x.png':'/static/images/send@3x.png'"
class="index_button_img" @click="pushchatList('text')" /> -->
<image :src="RefreshId==''?'/static/images/send@2x.png':'/static/images/send@3x.png'"
class="index_button_img" @click="pushchatList('text')" />
</view>
</view>
<!-- 语音输入 -->
<view class="flexCAB viodeinfo" v-if="inputType === 'video'">
<!-- 切换键盘图标 -->
<image v-if="!videoSandType" src="/static/images/Keyboard@2x.png" @click="changeVlaueType"
class="index_button_img" />
<!-- 提示文字 -->
<view class="video_tips" @touchstart.stop.prevent="onLongPress" @touchend.stop.prevent="onTouchEnd"
@touchmove.stop.prevent="touchcancel">
{{videoSandType?'':'按住说话'}}
<voiceAnother v-show="videoSandType&&countdown > 10" :startColor="startColor"
:endColor="endColor" style=" pointer-events: none" />
<view class=""
style="font-family: PingFang SC, PingFang SC;font-weight: 500;font-size: 28rpx;color: #fff;"
v-show="videoSandType&&countdown < 10">
{{countdown}}秒后将停止录音
</view>
</view>
<!-- 发送按钮 -->
<view class="flexC" v-if="!videoSandType">
<image :src="RefreshId==''?'/static/images/send@2x.png':'/static/images/send@3x.png'"
class="index_button_img" @click="pushchatList('text')" />
</view>
</view>
</view>
</view>
<guidePageChatIndex v-if="guidePageChatIndexShow" @sublimtGuidechatIndex="sublimtGuidechatIndex"
@sublimtOne="sublimtOne">
</guidePageChatIndex>
</view>
<gao-ChatSSEClient ref="chatSSEClientRef" @onOpen="openCore" @onError="errorCore" @onMessage="messageCore"
@onFinish="finishCore" />
</template>
<script lang="ts" setup>
import { ref, nextTick, onMounted, onUnmounted, getCurrentInstance } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import CustomNavbar from '@/components/CustomNavbar.vue'
import styleTemplate from '@/components/styleTemplate.vue'
import voiceAnother from '@/components/voiceAnother'
import guidePageChatIndex from '@/components/guidePageChatIndex'
import { generateRandomString, openSystemSettings, throttle, getToken, md5Encrypt, processString } from '@/utils/public'
import { ChatAddMessage, chatChatMessage, productAddAction, productItemList, chatStopMessage, chatReGenerate, chatUnreadCount, verificationGetNlsToken, chatGetVoiceUrl, productList } from '@/utils/api.ts'
import { usepublicStore } from '@/store/publicStore'
const guidePageChatIndexShow = ref(false)
const usepublic = usepublicStore()
const isFocus = ref(false)
const refresherTriggered = ref(false)
const isShowDownload = ref(false)
const shopCatNum = ref(0)//购物车的数量
const startColor = ref('#92EEF9')
const endColor = ref('#3AD7FE')
const value = ref('')
const videoSandType = ref(false)
const scrollTop = ref(0)
const messageId = ref(0)
const inputType = ref('text')//输出的是文本还是语音
const name32Potison = ref('')
const chatList : any = ref([
])
const loadingskeleton = ref(true)
const getchatUnreadCountInfo : any = ref('')
const inputBottom = ref(0)
const keyboardheightchange = (e) => {
console.log('event', e)
if (e.detail.height == 0) {
inputBottom.value = 0
} else {
inputBottom.value = (e.detail.height - 20) * 2
}
}
const scrollTopDown = () => {
console.log('点击了')
scrollTop.value = Math.floor(Math.random() * 9000000000) + 1000000000;
}
const scroll = (e : any) => {
const { scrollTop, scrollHeight, clientHeight } = e.detail
// 判断是否接近底部(预留20px缓冲防抖)
// console.log('scrollHeight - scrollTop - clientHeight', scrollHeight, scrollTop, clientHeight)
if ((scrollHeight - scrollTop) < 700) return
isShowDownload.value = true
}
const scrolltolower = () => {
console.log('到底了')
isShowDownload.value = false
}
const isSpinning = ref(false)
const uploadUnreadCountInfo = async () => {
if (isSpinning.value) return; // 防止重复触发
isSpinning.value = true
}
const sublimtGuidechatIndex = () => {
const initData = {
guideIndexTypeOne: true,//首页第一个引导页
guideIndexTypeTwo: true,//首页第二个引导页
guideChatIndexTypeOne: true,//聊天第一个引导页
guideChatIndexTypeTwo: true,//聊天第二个引导页
}
uni.setStorageSync('userGuideType', initData)
guidePageChatIndexShow.value = false
}
const getGuideType = () => {
// 如果 chatId 已有值或输入框有内容,直接隐藏引导
console.log('chatId.value', chatId.value, value.value)
if (chatId.value > 0 || value.value !== '') {
guidePageChatIndexShow.value = false
return
}
// 获取缓存
const guideTypeData = uni.getStorageSync('userGuideType')
console.log('guideTypeDataguideTypeDataguideTypeDataguideTypeData', guideTypeData)
// 缓存存在且为对象,判断引导状态
if (guideTypeData && typeof guideTypeData === 'object') {
if (guideTypeData.guideChatIndexTypeOne === false || guideTypeData.guideChatIndexTypeTwo === false) {
guidePageChatIndexShow.value = true
}
return
} else {
// 如果缓存不存在或格式不对,可以考虑初始化缓存
guidePageChatIndexShow.value = true
}
}
const sublimtOne = (data) => {
console.log(data)
if (data == 'guideChatIndexTypeOne') {
const initData = {
guideIndexTypeOne: true,//首页第一个引导页
guideIndexTypeTwo: true,//首页第二个引导页
guideChatIndexTypeOne: true,//聊天第一个引导页
guideChatIndexTypeTwo: false,//聊天第二个引导页
}
uni.setStorageSync('userGuideType', initData)
} else if (data == 'guideChatIndexTypeTwo') {
const initData = {
guideIndexTypeOne: true,//首页第一个引导页
guideIndexTypeTwo: true,//首页第二个引导页
guideChatIndexTypeOne: true,//聊天第一个引导页
guideChatIndexTypeTwo: true,//聊天第二个引导页
}
uni.setStorageSync('userGuideType', initData)
guidePageChatIndexShow.value = false
}
}
const onLeftClick = () => {
//需要判断输入框是否存在
let data = {
chatId: chatId.value,
value: value.value
}
// 取出本地存储,如果不存在就初始化为空数组
let chatValueInput = uni.getStorageSync('chatValueInputList') || [];
// 查找是否已经存在相同 chatId
if (data.chatId !== 0) {
const index = chatValueInput.findIndex(item => item.chatId === data.chatId);
if (index !== -1) {
// 已存在,更新 value
chatValueInput[index].value = data.value;
} else {
// 不存在,直接 push
chatValueInput.push(data);
}
// 保存回本地存储
uni.setStorageSync('chatValueInputList', chatValueInput);
}
uni.navigateBack({
delta: 1
})
}
const getproductList = async () => {
let data = {
pageIndex: 1,
pageSize: 10,
platform: '',
type: 1
}
const res : any = await productList(data)
console.log('productList', res)
shopCatNum.value = res.total
}
const agrenchatInfo = () => {
//
if (chatList.value.length > 0) {
return uni.navigateTo({
url: '/pages/chat/index'
})
} else {
return uni.showToast({
title: '当前已是最新对话',
icon: "none"
})
}
}
const changeVlaueType = () => {
inputType.value = inputType.value == 'text' ? 'video' : 'text'
}
const chatId = ref(0)
const isfwqerr = ref(false)
const isfwqerrChatValueIndex = ref(0)//服务器错误重新生成的数据id
const isfwqerrAnswersIndex = ref(0)//对应chatvlaue下的答案数组的小标
const erragrenChatinfo = (itema : any) => {//数据服务器错误进行刷新操作
console.log('itemerragrenChatinfo', itema.answersInfo.id)//整个问题的id
const chatValueIndex = chatList.value.findIndex(item => item.id == itema.id)//那条信息的idindex
const answersIndex = chatList.value[chatValueIndex].answers.findIndex(item => item.id == itema.answersInfo.id)
console.log('answersIndexanswersIndex', chatValueIndex, answersIndex)
//数据边控
isfwqerr.value = true
// chatList.value[chatValueIndex].answers[answersIndex].errmessage = ''
// chatList.value[chatValueIndex].answersInfo.errmessage = ''
isfwqerrChatValueIndex.value = chatValueIndex
isfwqerrAnswersIndex.value = answersIndex
stop()
setTimeout(() => {
start(chatList.value[chatValueIndex].answers[answersIndex].id)
}, 1000)
}
const delChatList = (data : any) => {//删除对应的聊天详情里面的数据
console.log('data', data, chatList.value)
const arr = chatList.value.filter(item => item.id !== data);
chatList.value = arr
RefreshId.value = ''
messageId.value = 0
chatList.value.length == 0 ? isShowDownload.value = false : ''
}
const ascenrClick = (data : any) => {
console.log('dataascenrClick', data)
value.value = data
pushchatList('text')
}
const pushchatList = throttle(async (type : any) => {//发送消息
console.log('value.value', value.value, name32Potison.value)
if (RefreshId.value !== '') {
return
}
if (type == 'text') {
if (value.value == '') {
return uni.showToast({
title: '发送内容为空',
icon: "none"
})
}
}
stop()
uni.showLoading({ title: '加载中...' })
let data = {
chatId: chatId.value,//,没有就是0 有就是对应的id
message: type == 'text' ? value.value : '',
voiceName: type == 'voice' ? name32Potison.value : ''
}
const res = await ChatAddMessage(data)
console.log('resresresresres', res)
if (Array.isArray(res.answers)) {
res.answers = res.answers.map(item => ({
...item,
noteArr: [],
sceneArr: [],
sceneArrIndex: -1,
isBotAsked: false,
isPreferences: '',
asOtherQuestArr: [],
disclaimerText: '',
errmessage: ""
}))
}
chatId.value = res.pid
res.answersInfo = {
message: '',
productDetails: '',
noteArr: [],//通知的数据数组
sceneArr: [],//询问场景,通勤,运动这些
sceneArrIndex: -1,
isBotAsked: false,//不想被问的按钮显示,false不显示,true显示
isPreferences: '',//将信息填写到我得喜好按钮 空就不显示,不空就显示
asOtherQuestArr: [],//你可能还想问的数据结构
disclaimerText: '',//免责说明
errmessage: "",//错误消息
}
chatList.value.push(res)
uni.hideLoading()
console.log('chatList.value', chatList.value)
makedowType.value = "wait"
setTimeout(() => {
start(res.answers[0].id)
}, 1000)
nextTick(() => {
doScroll()
})
value.value = ''
name32Potison.value = ''
})
const getNoice = throttle((content : any) => {
value.value = content
RefreshId.value = ''
pushchatList('text')
})
const refresherrefresh = () => {
refresherTriggered.value = true
messageId.value = chatList.value.length > 0 ? chatList.value[0].id : 0
console.log('出发了')
chatChatMessageList('refresher')
}
const chatChatMessageList = async (type : any) => {//获取数据,进行时数据修改
let data = {
chatId: Number(chatId.value),
questionId: messageId.value,
pageSize: 10
}
console.log('datachatChatMessageList', data)
let res = await chatChatMessage(data)
const result = res.map(item => {
item.answers = item.answers.map(itema => {
if (itema.status == -100) {
itema.errmessage = itema.error
} else {
itema.errmessage = ''
}
// ★ 关键修复:exMessage 在 itema 内!
const ex = itema.exMessage || [];
itema.sceneArr = ex.find(c => c.type === 'OPTIONS')?.body?.split(',') || [];
itema.sceneArrIndex = -1;
itema.noteArr = [];
itema.asOtherQuestArr = ex.filter(c => c.type === 'QUESTION').map(c => c.body);
itema.disclaimerText = ex.find(c => c.type === 'STATEMENT')?.body || '';
itema.isPreferences = ex.find(c => c.type === 'USERPROFILE')?.body || '';
return itema;
});
return item;
});
res = result;
console.log('chatChatMessageres', res)
// noteArr: [],//通知的数据数组
// sceneArr: [],//询问场景,通勤,运动这些
// sceneArrIndex: -1,
// isBotAsked: false,//不想被问的按钮显示,false不显示,true显示
// isPreferences: '',//将信息填写到我得喜好按钮 空就不显示,不空就显示
// asOtherQuestArr: [],//你可能还想问的数据结构
// disclaimerText: ''//免责说明
res.map(item => {
item.answersInfo = item.answers[0]
item.form = 'detail'
item.answersIndex = 0
})
console.log('reschatChatMessage', res, refresherTriggered.value)
if (res.length > 0 && res) {
chatList.value = res.reverse().concat(chatList.value)
}
console.log(res.reverse().concat(chatList.value))
// chatList.value.concat(newChatList)
//res.reverse().concat(chatList.value)
console.log('chatList.value', chatList.value)
processData()
if (type == 'refresher') {
nextTick(() => {
refresherTriggered.value = false
})
return
} else {
}
if (chatList.value > 0) {
isShowDownload.value = true
} else {
isShowDownload.value = false
}
setTimeout(() => { doScroll() }, 100)
setTimeout(() => { loadingskeleton.value = false }, 1000)
// 取本地存储,如果不存在就空数组
const chatValueInput = uni.getStorageSync('chatValueInputList') || [];
// 查找第一个匹配 chatId 的项
const matchedItem = chatValueInput.find(item => item.chatId == chatId.value);
// console.log('matchedItem', matchedItem)
// // 如果找到就赋值 value,否则为空字符串
value.value = matchedItem ? matchedItem.value : '';
// //判断最后一条数据的status
// // 1. 取最后一条聊天记录
// // 1. 取最后一条聊天记录
// // 1. 获取最后一条
// const lastIndex = chatList.value.length - 1;
// const lastItem = chatList.value[lastIndex];
// if (!lastItem?.answers?.length) return;
// // 2. 获取最后一条 answer
// const answerIndex = lastItem.answers.length - 1;
// const lastAnswer = lastItem.answers[answerIndex];
// // 3. 状态为 1(正常),清空 message
// if (lastAnswer.status === 0) {
// console.log('lastAnswer.id', lastAnswer.id);
// // 修改 message
// lastAnswer.message = "";
// // 👉 关键:深拷贝触发响应式(重要)
// chatList.value = JSON.parse(JSON.stringify(chatList.value));
// // 然后你可以执行 start()
// start(lastAnswer.id);
// }
}
const getchatUnreadCount = async () => {//获取提问的信息
let data = {
}
const res : any = await chatUnreadCount(data)
console.log('resgetchatUnreadCount', res)
getchatUnreadCountInfo.value = res
isSpinning.value = false
setTimeout(() => { loadingskeleton.value = false }, 1000)
}
const RefreshAnsweClick = async (itema : any) => {//重新生成对应的额回答
console.log('itema', itema)
let data = {
questionId: itema.id
}
const res : any = await chatReGenerate(data)
console.log('RefreshAnswe', res)
RefreshId.value = itema.id
const index = chatList.value.findIndex(item => item.id == itema.id)
console.log('index', index)
console.log('chatList.value[index].answers', chatList.value[index])
chatList.value[index].answers.push({
message: '',
productDetails: '',
id: res.id,
noteArr: [],//通知的数据数组
sceneArr: [],//询问场景,通勤,运动这些
sceneArrIndex: -1,
isBotAsked: false,//不想被问的按钮显示,false不显示,true显示
isPreferences: '',//将信息填写到我得喜好按钮 空就不显示,不空就显示
asOtherQuestArr: [],//你可能还想问的数据结构
disclaimerText: '',//免责说明
errmessage: '',//错误信息
})
chatList.value[index].answersInfo = {
message: '',
productDetails: '',
id: res.id,
noteArr: [],//通知的数据数组
sceneArr: [],//询问场景,通勤,运动这些
sceneArrIndex: -1,
isBotAsked: false,//不想被问的按钮显示,false不显示,true显示
isPreferences: '',//将信息填写到我得喜好按钮 空就不显示,不空就显示
asOtherQuestArr: [],//你可能还想问的数据结构
disclaimerText: '',//免责说明
errmessage: '',//错误信息
}
console.log('chatList.value[index]', chatList.value[index])
setTimeout(() => { start(res.id) }, 100)
}
const jumpShopcat = async () => {
uni.navigateTo({
url: '/pages/shop/shopCart'
})
}
const addShopCatInfo = async (item) => {
let data = {
productId: item.productId,
action: 1
}
const res : any = await productAddAction(data)
console.log('res', res)
uni.showToast({
title: '添加成功',
icon: "none"
})
getproductList()
}
const openPhone = () => {
uni.chooseImage({
count: 6, //默认9
sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有
sourceType: ['album', 'camera'], //从相册选择
success: function (res) {
console.log(JSON.stringify(res.tempFilePaths));
uni.navigateTo({
url: '/pages/chat/sanPhone?phone=' + res.tempFilePaths
})
}
});
}
//长按语音输入
const onLongPress = () => {
console.log('11111', 111)
// uni.showToast({
// title: '长按事件触发',
// icon: 'none'
// });
const RECORDType = plus.navigator.checkPermission('RECORD')
if (RECORDType !== 'authorized') {
videoSandType.value = false
uni.showModal({
title: '提示',
content: '录音服务当前可能尚未打开,请去设置打开!',
success: function (res) {
if (res.confirm) {
videoSandType.value = false
openSystemSettings('record')
} else if (res.cancel) {
console.log('用户点击取消');
}
}
});
return
}
// const appAuthorizeSetting = uni.getAppAuthorizeSetting()
// console.log('appAuthorizeSetting', appAuthorizeSetting)
// if (appAuthorizeSetting.microphoneAuthorized !== 'authorized') {
// uni.showModal({
// title: '提示',
// content: '请授权打开麦克风权限,用于发送语音消息!',
// success: function (res) {
// if (res.confirm) {
// recorderManager = null
// recorderManager = uni.getRecorderManager();
// recorderManager.start();
// // recorderManager.stop();
// } else if (res.cancel) {
// console.log('用户点击取消');
// }
// }
// })
// return
// }
if (RefreshId.value !== '') {
return
}
startRecording()
//videoSandType = true
}
const onTouchEnd = () => {
console.log('2222', 222)
inputType.value = 'video'
videoSandType.value = false
stopRecording()
// stopAliSpeech()
}
const processData = () => {//处理异常的数据
//后端返回的数据为空时候
console.log('processDataChatValue', chatList.value)
makedowType.value = ''
chatList.value.map(item => {
item.answersInfo.message ? '' : item.answersInfo.message = '服务器有点小拥挤,等下再来试试吧~'
item.answersInfo.status = 1
})
}
const isCancel = ref(false) // 是否滑出区域取消
const voiceSendType = ref(false)
const touchcancel = (e : TouchEvent) => {
const touch = e.touches[0]
const y = touch.clientY
// 在 App 端获取元素位置信息
const query = uni.createSelectorQuery()
query.select('.viodeinfo').boundingClientRect((data : any) => {
if (!data) return
const { top, bottom } = data
// 判断是否在范围内
if (y < top || y > bottom) {
if (!isCancel.value) {
isCancel.value = true
console.log('手指移出按钮区域,准备取消')
startColor.value = '#fff'
endColor.value = '#fff'
//如果在这里放弃的直接就是取消
voiceSendType.value = true
}
} else {
if (isCancel.value) {
isCancel.value = false
startColor.value = '#92EEF9'
endColor.value = '#3AD7FE'
voiceSendType.value = false
console.log('回到按钮区域,继续录音', startColor.value, endColor.value)
}
}
})
query.exec()
}
const getchatGetVoiceUrl = async () => {
let data = {
name: name32Potison.value
}
console.log('name32Potison.value', name32Potison.value)
const res : any = await chatGetVoiceUrl(data)
console.log('chatGetVoiceUrlres', res)
return res
}
const countdown = ref(57) // 当前剩余秒数
const timer : any = ref<number | null>(null)
// 开始倒计时
const startCountdown = () => {
if (timer.value) clearInterval(timer.value)
countdown.value = 57
timer.value = setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
clearInterval(timer.value!)
timer.value = null
countdown.value = 57
console.log('倒计时结束')
stopRecording()
}
}, 1000)
}
// 停止倒计时
const stopCountdown = () => {
if (timer.value) {
clearInterval(timer.value)
timer.value = null
countdown.value = 57
recorderManager = null
}
}
//语音
let recorderManager = null;
// ✅ 在 plusReady 之后执行,确保 plus.io 可用
function startRecording() {
// const appAuthorizeSetting = uni.getAppAuthorizeSetting()
// console.log('appAuthorizeSetting', appAuthorizeSetting)
recorderManager = uni.getRecorderManager();
recorderManager.onStart(() => {
console.log("录音开始");
videoSandType.value = true
name32Potison.value = generateRandomString() + '.mp3'
startCountdown()
});
recorderManager.onError((err) => {
console.error("录音错误", err);
videoSandType.value = false
stopCountdown()
});
recorderManager.onStop(async (res) => {
console.log("录音停止", res);
// res.tempFilePath 是录音生成的文件路径
// 你可以用这个文件去做上传或者转码
// voiceSendType.value = false
// isCancel.value = false
// stopCountdown()
if (voiceSendType.value) {
voiceSendType.value = false
isCancel.value = false
stopCountdown()
return
}
// if (voiceSendType.value) {
// voiceSendType.value = false
// isCancel.value = false
// stopCountdown()
// return
// }
const formData = await getchatGetVoiceUrl()
console.log('formData', formData)
// const hasNetwork = await getUserNetWork()
// if (!hasNetwork) {
// return uni.showToast({
// title: '语音发送失败,网络异常!',
// icon: "none"
// })
// }
uni.uploadFile({
url: 'https://voice-1371047959.cos.ap-shanghai.myqcloud.com', //仅为示例,非真实的接口地址
filePath: res.tempFilePath,
name: 'file',
formData: formData,
success: (uploadFileRes) => {
console.log('uploadFileRes.data', uploadFileRes, formData);
// sendAudioToAli(uploadFileRes.data);
stopCountdown()
pushchatList('voice')
}
});
//
});
recorderManager.start({
duration: 57000, // 最长录音时间 1 分钟
format: 'mp3', // 安卓端建议使用 'pcm' 或 'mp3'
sampleRate: 16000, // 采样率 16kHz
numberOfChannels: 1, // 单声道
encodeBitRate: 16 * 1000,// PCM16
});
}
// 停止录音
function stopRecording() {
if (recorderManager) {
recorderManager.stop();
videoSandType.value = false
recorderManager = null
}
}
// const autoScroll = ref(true);//是否允许自动滚动
const TrackingOointData = ref('')
onLoad((option : any) => {
console.log('optionchatIndex', option)
chatId.value = option.chatId ? option.chatId : 0
inputType.value = option.type ? option.type : 'text'
value.value = option.value ? option.value : ''
// chatChatMessageList()
if (chatId.value !== 0) {
chatChatMessageList()
}
if (value.value !== '') {
pushchatList('text')
}
TrackingOointData.value = {
"event": "ChatPage",
startTime: Date.now(),
id: chatId.value,
endTime: ''
}
})
onShow(() => {
getGuideType()
getproductList()
getchatUnreadCount()
})
onMounted(() => {
uni.onUserCaptureScreen((path) => {
console.log("用户截屏了", path);
uni.showToast({
title: "检测到截屏!",
icon: "none"
});
});
})
// 页面关闭或退出时关闭 gRPC
onUnmounted(() => {
// wsClient.close();
uni.offUserCaptureScreen();
stop()
TrackingOointData.value.endTime = Date.now()
usepublic.changeTrackingOointValue(TrackingOointData.value)
console.log('trackingOointValue', usepublic.trackingOointValue)
})
//开启sse
const chatSSEClientRef = ref(null);
const loading = ref(false);
const openLoading = ref(false);
const openCore = (response) => {
openLoading.value = false;
console.log("open sse:", response);
}
const errorCore = (err) => {
console.log("error sse:", err);
}
const safeMarkdownContent = (msg) => {
console.log(msg)
}
interface ProductItemListRequest {
ids : number[] | null;
}
// 监听用户滚动事件(scroll-view)
const handleScroll = (e : any) => {
// console.log('出发了')
// autoScroll.value = false
};
const RefreshId = ref('')
const productItemLists = async () => {//普通的请求数据图片
console.log('productChatArr.value', productChatArr.value);
if (!productChatArr.value) {
RefreshId.value = ''
return
};
const data : ProductItemListRequest = {
ids: productChatArr.value,
};
const res : any = await productItemList(data);
// ------------ 关键逻辑:根据 RefreshId 判断 ------------
const last = chatList.value[chatList.value.length - 1];
last.answers[0].productDetails = res;
last.answersInfo.productDetails = res;
productChatArr.value = '';
console.log('获取到的商品信息(普通)', res);
processData();
doScroll();
// ------------ 逻辑完全结束后再清空 RefreshId ------------
};
const productItemListsrealse = async () => {//重新生成的商品图片
console.log('productChatArr.value', productChatArr.value);
if (!productChatArr.value) {
RefreshId.value = ''
return
};
const data : ProductItemListRequest = {
ids: productChatArr.value,
};
const res : any = await productItemList(data);
// ------------ 关键逻辑:根据 RefreshId 判断 ------------
const index = chatList.value.findIndex(item => item.id == RefreshId.value);
chatList.value[index].answers[chatList.value[index].answers.length - 1].productDetails = res;
chatList.value[index].answersInfo.productDetails = res;
console.log('获取到的商品信息(重新生成)', res);
productChatArr.value = '';
RefreshId.value = '';
processData();
doScroll();
return
// ------------ 逻辑完全结束后再清空 RefreshId ------------
};
const productChatArr : any = ref('');
const makedowType = ref('')//是否是正常的信息 false不是 true是
const messageCore = (msg : any) => {
console.log('msg获得到的信息', msg)
if (RefreshId.value == '') {//不是重新生成的
if (isfwqerr.value) {//服务器错误
if (msg.data !== '' && msg.data !== 'event: keep' && msg.data !== 'event: end') {
console.log('msg.datamsg.datamsg.datamsg.data', msg.data)
const jsonStr = msg.data.replace(/^data:\s*/, '');
console.log('jsonStr', JSON.parse(jsonStr))
// const isfwqerrChatValueIndex = ref(0)//服务器错误重新生成的数据id
// const isfwqerrAnswersIndex = ref(0)//对应chatvlaue下的答案数组的小标
// 先解析一次
const data = JSON.parse(jsonStr);
const answer = chatList.value[isfwqerrChatValueIndex.value].answers[isfwqerrAnswersIndex.value];
const answerInfo = chatList.value[isfwqerrChatValueIndex.value].answersInfo;
switch (data.Type) {
case "message": {
const msg = data.Body;
if (!answer.message.includes(msg)) {
answer.errmessage = ''
answerInfo.errmessage = ''
answer.message += msg;
answerInfo.message += msg;
}
scrollTop.value = chatList.value.length * 150000;
doScroll();
break;
}
case "product": {
const arr = Array.from(new Set(data.Body.split(',')));
productChatArr.value = arr;
scrollTop.value = chatList.value.length * 150000;
doScroll();
if (RefreshId.value !== '') {
productItemListsrealse()
} else {
productItemLists()
}
break;
}
case "note": {
const note = data.Body;
if (!answer.noteMessage.includes(note)) {
answer.noteMessage.push(note);
answerInfo.noteArr.push(note);
}
doScroll();
break;
}
case "OPTIONS": {
const optionsArr = Array.from(new Set(data.Body.split(',')));
answer.noteMessage = optionsArr;
answerInfo.sceneArr = optionsArr;
doScroll();
break;
}
case "USERPROFILE": {
answer.noteMessage = data.Body;
answerInfo.isPreferences = data.Body;
doScroll();
break;
}
case "QUESTION": {
const question = data.Body;
if (!answer.asOtherQuestArr.includes(question)) {
answer.asOtherQuestArr.push(question);
answerInfo.asOtherQuestArr.push(question);
}
doScroll();
break;
}
case "STATEMENT": {
answerInfo.disclaimerText = data.Body;
doScroll();
break;
}
case "err": {
answer.errmessage = data.Body;
answerInfo.errmessage = data.Body;
doScroll();
stop();
break;
}
default:
console.warn("未处理的 Type:", data.Type);
break;
}
}
return
}
if (msg.data !== '' && msg.data !== 'event: keep' && msg.data !== 'event: end') {
console.log('msg.datamsg.datamsg.datamsg.data', msg.data)
const jsonStr = msg.data.replace(/^data:\s*/, '');
console.log('jsonStr', JSON.parse(jsonStr))
// if (chatList.value[chatList.value.length - 1].answers[0].message = '') {
// chatList.value[chatList.value.length - 1].answers[0].message = ''
// chatList.value[chatList.value.length - 1].answersInfo.message = ''
// }
// item.noteArr = []//通知的数据数组
// item.sceneArr = []//询问场景,通勤,运动这些
// item.isBotAsked = false//不想被问的按钮显示,false不显示,true显示
// item.isPreferences = ''//将信息填写到我得喜好按钮,false不显示,true显示
// item.asOtherQuestArr = []//你可能还想问的数据结构
// item.disclaimerText = ''//免责说明
if (JSON.parse(jsonStr).Type == "message") {
chatList.value[chatList.value.length - 1].answers[0].message += JSON.parse(jsonStr).Body
chatList.value[chatList.value.length - 1].answersInfo.message += JSON.parse(jsonStr).Body
scrollTop.value = chatList.value.length * 150000
doScroll()
} else if (JSON.parse(jsonStr).Type == "product") {
let arr = JSON.parse(jsonStr).Body.split(',')
console.log('arrarr', arr)
arr = JSON.stringify(arr).replace(/"/g, '')
productChatArr.value = JSON.parse(arr);
scrollTop.value = chatList.value.length * 150000
if (RefreshId.value !== '') {
productItemListsrealse()
} else {
productItemLists()
}
doScroll()
} else if (JSON.parse(jsonStr).Type == "note") {//通知队列
// makedowType.value = 'note'
chatList.value[chatList.value.length - 1].answers[0].noteArr.push(JSON.parse(jsonStr).Body)
chatList.value[chatList.value.length - 1].answersInfo.noteArr.push(JSON.parse(jsonStr).Body)
doScroll()
} else if (JSON.parse(jsonStr).Type == "OPTIONS") {//通勤什么运动这个
// makedowType.value = 'note'
chatList.value[chatList.value.length - 1].answers[0].sceneArr = (JSON.parse(jsonStr).Body.split(','))
chatList.value[chatList.value.length - 1].answersInfo.sceneArr = (JSON.parse(jsonStr).Body.split(','))
doScroll()
} else if (JSON.parse(jsonStr).Type == "USERPROFILE") {//填写之我得喜好
// makedowType.value = 'note'
chatList.value[chatList.value.length - 1].answers[0].isPreferences = JSON.parse(jsonStr).Body
chatList.value[chatList.value.length - 1].answersInfo.isPreferences = (JSON.parse(jsonStr).Body)
doScroll()
} else if (JSON.parse(jsonStr).Type == "QUESTION") {//
// makedowType.value = 'note'
chatList.value[chatList.value.length - 1].answers[0].asOtherQuestArr.push(JSON.parse(jsonStr).Body)
chatList.value[chatList.value.length - 1].answersInfo.asOtherQuestArr.push(JSON.parse(jsonStr).Body)
doScroll()
} else if (JSON.parse(jsonStr).Type == "STATEMENT") {//免责说明
// makedowType.value = 'note'
// chatList.value[chatList.value.length - 1].answersInfo.noteMessage = JSON.parse(jsonStr).Body
chatList.value[chatList.value.length - 1].answersInfo.disclaimerText = JSON.parse(jsonStr).Body
doScroll()
} else if (JSON.parse(jsonStr).Type == "err") {//断掉
// makedowType.value = "err"
// makedowType.value = "err"
chatList.value[chatList.value.length - 1].answers[0].errmessage = JSON.parse(jsonStr).Body
chatList.value[chatList.value.length - 1].answersInfo.errmessage = JSON.parse(jsonStr).Body
scrollTop.value = chatList.value.length * 150000
doScroll()
stop()
}
}
return
} else {
console.log('msg获得到的信息重新获取这里是重新生成的数据', msg)
let index = chatList.value.findIndex(item => item.id == RefreshId.value)
const answersIndexAll = chatList.value[index].answers.length - 1
console.log('answersIndexAll', answersIndexAll)
// chatList.value[index].answersInfo.message = ''
// chatList.value[index].answersInfo.noteMessage = ''
console.log('RefreshIdRefreshId', chatList.value[index].answersInfo)
if (msg.data !== '' && msg.data !== 'event: keep' && msg.data !== 'event: end') {
const jsonStr = msg.data.replace(/^data:\s*/, '');
console.log('jsonStr', JSON.parse(jsonStr))
if (JSON.parse(jsonStr).Type == "message") {
// if (makedowType.value !== "message") {
// chatList.value[index].answers[chatList.value[index].answers.length - 1].message = ''
// chatList.value[index].answersInfo.message = ''
// }
const body = JSON.parse(jsonStr).Body
const answer = chatList.value[index].answers[answersIndexAll]
if (!answer.message.includes(body)) {
answer.message += body
chatList.value[index].answersInfo.message = answer.message
}
doScroll()
} else if (JSON.parse(jsonStr).Type == "product") {
let arr = JSON.parse(jsonStr).Body.split(',')
console.log('arrarr', arr)
arr = JSON.stringify(arr).replace(/"/g, '')
productChatArr.value = JSON.parse(arr);
if (RefreshId.value !== '') {
productItemListsrealse()
} else {
productItemLists()
}
doScroll()
} else if (JSON.parse(jsonStr).Type == "note") {
const body = JSON.parse(jsonStr).Body
const answer = chatList.value[index].answers[answersIndexAll]
if (!answer.noteArr.includes(body)) {
answer.noteArr.push(body)
chatList.value[index].answersInfo.noteArr = [...answer.noteArr]
}
doScroll()
} else if (JSON.parse(jsonStr).Type == "OPTIONS") {//通勤什么运动这个
// makedowType.value = 'note'
// chatList.value[chatList.value.length - 1].answersInfo.noteMessage = JSON.parse(jsonStr).Body
const bodyArr = JSON.parse(jsonStr).Body.split(',')
const answer = chatList.value[index].answers[answersIndexAll]
// 初始化数组(防止未定义)
answer.sceneArr = answer.sceneArr || []
chatList.value[index].answersInfo.sceneArr = chatList.value[index].answersInfo.sceneArr || []
// 遍历累加去重
bodyArr.forEach(item => {
if (!answer.sceneArr.includes(item)) {
answer.sceneArr.push(item)
}
})
chatList.value[index].answersInfo.sceneArr = [...answer.sceneArr]
doScroll()
} else if (JSON.parse(jsonStr).Type == "QUESTION") {//
// makedowType.value = 'note'
const body = JSON.parse(jsonStr).Body
const answer = chatList.value[index].answers[answersIndexAll]
if (!answer.asOtherQuestArr.includes(body)) {
answer.asOtherQuestArr.push(body)
chatList.value[index].answersInfo.asOtherQuestArr = [...answer.asOtherQuestArr]
}
doScroll()
} else if (JSON.parse(jsonStr).Type == "STATEMENT") {//免责说明
// makedowType.value = 'note'
chatList.value[index].answersInfo.disclaimerText = JSON.parse(jsonStr).Body
doScroll()
} else if (JSON.parse(jsonStr).Type == "err") {//断掉
// makedowType.value = "err"
makedowType.value = "err"
chatList.value[index].answers[answersIndexAll].errmessage = JSON.parse(jsonStr).Body
chatList.value[index].answersInfo.errmessage = chatList.value[index].answers[answersIndexAll].errmessage
doScroll()
stop()
}
}
}
// scrollTop.value = (chatList.value.length + msg.data.length * 2) * 100000 // 假设每条消息大约 100rpx 高
// console.log('autoScroll.value', autoScroll.value)
if (msg.data == 'event: end') {
console.log('makedowType.valueend ', makedowType.value)
stop()
console.log('productChatArr.value ', productChatArr.value)
}
}
const finishCore = () => {
console.log("finish sse")
loading.value = false;
// processData()
stop()
}
// 统一滚动函数 —— 放在 script 顶部附近
const doScroll = (force = false) => {
// 如果用户手动滚动过并且没有强制滚动,则不自动滚
// if (!autoScroll.value && !force) return;
// 等待两次 nextTick,确保列表 DOM 完全渲染(SSE 流和异步数据较稳)
nextTick(() => {
// 设为一个大数,scroll-view 会剪裁到最大滚动位置
// 也可以用具体计算或查询容器高度(见注释)
scrollTop.value = chatList.value.length * Math.floor(Math.random() * 9000000000) + 1000000000;
});
};
const start = (messageId : number) => {
if (loading.value) return;
const localInfo = uni.getStorageSync('localInfo')
const headetToken = generateRandomString()
const newDate = Date.now()
openLoading.value = true;
loading.value = true;
chatSSEClientRef.value.startChat({
/**
* 将它换成你的地址
* 注意:
* 如果使用 sse-server.js 要在手机端使用的话,请确保你的手机和电脑处在一个局域网下并且是正常的ip地址
*/
url: `https://api.wonsouai.com/api/custom/chat/GetMessage?messageId=${messageId}`,
// 请求头
headers: {
'Content-Type': 'application/json',
'lng': localInfo?.longitude,
'lat': localInfo?.latitude,
'authorization': getToken() ? `Bearer ${getToken()}` : '',
'timestamp': newDate,
'secretKey': headetToken,
'ClientPlatform': '1',
'secret': md5Encrypt(processString(headetToken) + newDate),
},
// 默认为 post
method: 'get',
// body: {
// "stream":true,
// "model": "deepseek-chat",
// "messages": [
// {"role": "system", "content": "你是来自艺咖科技的数字员工,你的名字叫小咖。"}]
// }
})
makedowType.value = 'wait'
}
const stop = async () => {
makedowType.value = ""
let data = {
messageId: chatList.value[chatList.value.length - 1].answers[chatList.value[chatList.value.length - 1].answers.length - 1].id
}
console.log('datadatadata', data)
const res : any = await chatStopMessage(data)
console.log("stop停止的接口", res);
chatSSEClientRef.value.stopChat()
console.log('chatStopList', chatList.value)
chatList.value[chatList.value.length - 1].answersInfo.status = 1;
chatList.value[chatList.value.length - 1].answers[chatList.value[chatList.value.length - 1].answers.length - 1].status = 1
processData()
refresherTriggered.value = false
isfwqerr.value = false
isfwqerrChatValueIndex.value = ''
isfwqerrAnswersIndex.value = ''
nextTick(() => {
// 设为一个大数,scroll-view 会剪裁到最大滚动位置
// 也可以用具体计算或查询容器高度(见注释)
scrollTop.value = 1000000000000;
});
}
</script>
<style lang="less" scoped>
.chatIndex {
background: #F2F2F2;
width: 100vw;
height: 100vh;
padding: 0 0;
position: relative;
overflow: hidden;
.back-icon {
width: 48rpx;
height: 48rpx;
}
.chatIndexLogo {
width: 200rpx;
height: 200rpx;
display: flex;
margin-left: 20rpx;
}
.chatIndexLogoText {
font-size: 36rpx;
font-weight: bold;
display: inline-block;
height: 88rpx;
line-height: 44rpx;
margin-top: 52rpx;
margin-left: 5rpx;
background: linear-gradient(135deg,
#8128FF,
#2866F6,
#8128FF);
/* 两头紫,中间蓝,流动时过渡更顺滑 */
background-size: 200% auto;
/* 背景扩大,才有流动效果 */
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: flowLight 1s linear infinite;
/* 控制流光速度 */
}
@keyframes flowLight {
0% {
background-position: 0% center;
}
100% {
background-position: -200% center;
}
}
.back-logo {
width: 78rpx;
height: 100rpx;
display: flex;
margin-left: 16rpx;
}
.icon-more {
width: 48rpx;
height: 48rpx;
display: flex;
margin-right: 20rpx;
}
.chatIndex_card {
width: calc(686rpx - 76rpx);
// height: calc(588rpx - 96rpx);
background: #FFFFFF;
border-radius: 40rpx;
padding: 48rpx 48rpx;
.blueColor {
font-family: Alimama Agile VF, Alimama Agile VF;
font-weight: bold;
font-size: 44rpx;
color: #2256DE;
}
.chatIndex_card_tips {
font-family: PingFang SC, PingFang SC;
font-weight: bold;
font-size: 32rpx;
color: #000000;
margin-top: 8rpx;
}
.changechatindex_icon {
width: 36rpx;
height: 36rpx;
flex-shrink: 0;
}
/* 无限旋转 */
.spinning {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.changechatindex_text {
font-family: PingFang SC, PingFang SC;
font-weight: 400;
font-size: 28rpx;
height: 36rpx;
line-height: 36rpx;
color: #333333;
margin-left: 16rpx;
}
.cardItem {
width: calc(622rpx - 64rpx);
// height: calc(80rpx - 48rpx);
background: rgba(34, 86, 222, 0.1);
padding: 24rpx 32rpx;
border-radius: 16rpx;
margin: 32rpx 0 0 0;
display: inline-flex;
.cardItem_img {
width: 36rpx;
height: 36rpx;
display: flex;
flex-shrink: 0;
margin-right: 12rpx;
}
.cardItem_text {
font-family: PingFang SC, PingFang SC;
font-weight: 500;
font-size: 28rpx;
margin-top: 6rpx;
color: #333333;
}
}
}
.sublimt {
height: 184rpx;
width: 750rpx;
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #F2F2F2;
// background: red;
.downloadScooltop {
width: 92rpx;
height: 92rpx;
position: absolute;
top: -70px;
left: 330rpx;
}
.shopcat {
width: 196rpx;
height: 72rpx;
border-radius: 16rpx 16rpx 16rpx 16rpx;
border: 2rpx solid #D6DADE;
position: absolute;
left: 32rpx;
bottom: 170rpx;
background: #F2F2F2;
.shopcatNumInfo {
position: relative;
.shopcat_img {
width: 32rpx;
height: 32rpx;
}
.shopcatNum {
position: absolute;
padding: 0 8rpx;
display: inline-flex;
height: 28rpx;
background: #F2525E;
border-radius: 50rpx;
border: 2px solid #FFFFFF;
font-family: PingFang SC, PingFang SC;
font-weight: 500;
font-size: 20rpx;
top: -20rpx;
right: -20rpx;
color: #FFFFFF;
line-height: 28rpx;
}
}
.shopcat_text {
font-family: PingFang SC, PingFang SC;
font-weight: 500;
font-size: 28rpx;
margin-left: 16rpx;
color: #000000;
}
}
.SandTypeInfo {
position: absolute;
left: 50rpx;
bottom: 170rpx;
width: calc(686rpx - 48rpx);
font-family: PingFang SC, PingFang SC;
font-weight: 400;
font-size: 28rpx;
text-align: center;
}
.index_button {
width: calc(686rpx - 48rpx);
background: #FFFFFF;
box-shadow: 0rpx 8rpx 20rpx 0rpx rgba(150, 174, 226, 0.25);
border-radius: 20rpx 20rpx 20rpx 20rpx;
position: absolute;
bottom: 48rpx;
left: 32rpx;
right: 32rpx;
padding: 30rpx 32rpx;
.index_button_img {
width: 48rpx;
height: 48rpx;
display: flex;
margin: 0 16rpx;
}
.inputValue {
font-family: PingFang SC, PingFang SC;
font-weight: bold;
font-size: 32rpx;
color: #333333;
flex: 1;
margin: 0 10rpx;
}
.video_tips {
font-family: PingFang SC, PingFang SC;
font-weight: 400;
font-size: 28rpx;
height: 45rpx;
line-height: 45rpx;
color: #1D1D1D;
flex: 1;
text-align: center;
margin: 0 10rpx;
}
}
}
}
</style>梳理一下,给我讲解一下
最新发布