flinfo/dc-App/pages/Chat/newChat.vue
2025-03-12 18:08:19 +08:00

991 lines
27 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<!-- 聊天界面展示https://www.bilibili.com/video/BV1hT4y1P75N?p=22 搭建1和2 -->
<view class="content" @click="clickContent">
<view class="top_po">
<view class="" @click="goback()">
<u-icon name="arrow-left" color="#fff" size="20"></u-icon>
</view>
<view style="display: flex; justify-content: space-between; align-items: center; width: 100%;">
<!-- 标题居中 -->
<text style="flex: 1; text-align: center;">{{ info.title }}</text>
<!-- 右侧内容 -->
<view style="display: flex; align-items: center;">
<view v-if="info.conversation == 'Translator'" class="sm-text" @click="chooseSayLang">
{{ sayLangStr }}
<u-icon style="margin-top: 6rpx; margin-left: 5rpx;" name="arrow-down" color="#fff" size="12"></u-icon>
</view>
<view v-if="info.conversation == 'Translator'" style="margin: 0 15rpx">
</view>
<view v-if="info.conversation == 'Translator'" class="sm-text" @click="chooseLang">
{{ lang }}
<u-icon style="margin-top: 6rpx; margin-left: 5rpx;" name="arrow-down" color="#fff" size="12"></u-icon>
</view>
</view>
</view>
<view class="right_top"></view>
</view>
<!-- 聊天内容 -->
<scroll-view class="chat" scroll-y="true" scroll-with-animation="true" :scroll-into-view="scrollToView">
<view class="chat-main" :style="{paddingBottom:inputh+'px'}">
<view class="chat-ls" v-for="(item,index) in messagesList" :key="index" :id="'msg'+ index">
<view class="msg-m msg-right" v-if="item.isTrans">
<image class="user-img" :src="imagesUrl+userAvatar"></image>
<view class="message" v-if="item.inputs.type == 'text'">
<!-- 文字 -->
<view class="msg-text">
<text>{{ item.query }}</text>
</view>
</view>
<view class="message" v-if="item.inputs.type == 'image'" @click="previewImage(item.query)">
<image :src="'data:image/png;base64,'+item.query" class="msg-img" mode="widthFix"></image>
</view>
<view class="message" v-if="item.inputs.type == 'voice'" @tap="playVoice(item.url)">
<!-- 音频 -->
<view class="msg-text voice" :style="{width:item.time*4+'rpx'}">
<image src="/static/chat/sy.png" class="voice-img"></image>
{{ item.time }}″
</view>
</view>
</view>
<view class="msg-m msg-left" v-if="item.answer">
<image class="user-img" :src="info.icon"></image>
<view class="msg-text" @click="clickSprinkImage(index)"
v-if="item.inputs.type == 'image' && info.conversation == 'Translator'">
<view class="po_i" v-if="show=='2'&&clickIdx==index">
<view class="size_" @click="showTxt(item)">text</view>
</view>
<view class="po_i" v-if="!item.showImage&&clickIdx==index">
<view class="size_" @click="showImageFunction (item)">image</view>
</view>
<image :src="'data:image/png;base64,'+item.answer" @click="previewImage(item.answer)" v-if="item.showImage" class="msg-img"
mode="widthFix"></image>
<text v-else>{{ item.imageText }}</text>
</view>
<view class="msg-text" @click="clickSprink(index)" id="po_" v-else>
<view class="po_z" v-if="show=='1'&&clickIdx==index">
<view class="size_" @click="voiceTxt(item)">Voice</view>
</view>
{{ item.answer }}
<br/>
<!-- <text v-if="item.answerCh">{{ item.answerCh }}</text>-->
</view>
</view>
</view>
<span id="bottomId"></span>
</view>
</scroll-view>
<submit @inputs="inputs" @heights="heights" :title="this.info.title"></submit>
<u-picker :show="langShow" ref="langPicker" :defaultIndex="langIndex" :columns="columns" @confirm="langConfirm"
@cancel="langCancel"></u-picker>
<u-picker :show="sayLangShow" ref="langPicker" :defaultIndex="sayLangIndex" keyName="label" :columns="[sayLangColumns]"
@confirm="sayLangConfirm"
@cancel="sayLangCancel"></u-picker>
</view>
</template>
<script>
import dateTime from './newChat/dateTime.js';
import submit from './newChat/submit.vue';
import config from '@/config'
import {
startMsgSocket,
msgSocketConnect,
sendMsg,
closeMsgSocket
} from './msgSocket'
import requestChat from '../../utils/requestChat'
import permision from "@/js_sdk/wa-permission/permission.js"
import request from '../../utils/request'
import {base64ToPath} from "image-tools";
//音频播放
const innerAudioContext = uni.createInnerAudioContext();
// 录音
const recorderManager = uni.getRecorderManager();
export default {
data() {
return {
audioSrc: '', // 用于存储音频的 URL
clickIdx: null,
//是否长按事件
timer: null,//长按计时器
show: '0',
sayLangIndex : [1],
langIndex: [11],
columns: [
['Arabic-阿拉伯语',
'German-德语',
'English-英语',
'Spanish-西班牙语',
'French-法语',
'Hindi-印地语',
'Indonesian-印度尼西亚语',
'Italian-意大利语',
'Japanese-日语',
'Korean-韩语',
'Russian-俄语',
"Chinese-简体中文"
]
],
sayLangColumns: [
{
value: "zh-CHS",
label: "Mandarin (China)-普通话(中国)"
},
// {
// value: 'en',
// label: 'Arabic-阿拉伯语',
// },
// {
// value: 'in',
// label: 'Bahasa (Indonesia)-巴哈萨语(印度尼西亚)',
// },
// {
// value: 'yue',
// label: 'Cantonese-粤语',
// },
// {
// value: 'ca',
// label: 'Catalan-加泰隆语',
// },
// {
// value: 'cs',
// label: 'Czech-捷克语',
// },
// {
// value: 'da',
// label: 'Danish-丹麦语',
// },
// {
// value: 'nl',
// label: 'Dutch-荷兰语',
// },
// {
// value: 'nl-BEL',
// label: 'Dutch (Belgium)-荷兰语(比利时',
// },
// {
// value: 'en-AUS',
// label: 'English (Australia)-英语(澳大利亚)',
// },
// {
// value: 'en-GBR',
// label: 'English (GB)-英语(英国)',
// },
// {
// value: 'en-IND',
// label: 'English (India)-英语(印度)'
// },
// {
// value: 'en-IRL',
// label: 'English (Ireland)-英语(爱尔兰)'
// },
// {
// value: 'en-SCT',
// label: 'English (Scotland)-英语(苏格兰)'
// },
// {
// value: 'en-ZAF',
// label: 'English (South Africa)-英语(南非)'
// },
{
value: 'en',
label: 'English (US)-英语'
},
{
value: 'ja',
label: 'Japanese (US)-日语)'
},
],
langShow: false,
sayLangShow: false,
sayLang: 'en',
sayLangStr: 'English',
lang: 'Chinese',
imagesUrl: config.imagesUrl,
msgSocket: null,
// 反转数据接收
messagesList: [],
imgMsg: [],
scrollToView: '',
showImageTextIndex: null,
showImage: true,
oldTime: new Date(),
inputh: '60',
info: {},
userId: null,
userAvatar: null,
firstId: null,
limit: 20,
socketId: null,
// 请求参数
ajax: {
rows: 20, //每页数量
page: 1, //页码
flag: true, // 请求开关
sendFlag: false
},
scrollId: 'bottomId',
storeList: 'msgHisList'
}
},
onLoad(option) {
if (option) {
let infoData = JSON.parse(option.data)
let tempInfo = {
icon: infoData.icon,
token: infoData.token,
conversation: infoData.conversation,
userId: infoData.userId,
userAvatar: infoData.userAvatar,
title: infoData.title
}
uni.setStorageSync('userId', infoData.userId)
this.info = tempInfo
this.userId = infoData.userId
this.userAvatar = infoData.userAvatar
this.getMessageByStore()
this.goBottom()
}
this.getRecordsToken()
},
components: {
submit,
},
methods: {
async voiceTxt(item) {
let res = await request({
url: 'youDaoApi/tts',
method: 'post',
data: {
q: item.answer,
voiceName: this.lang,
language: item.lang
}
})
this.playBase64Mp3(res.data)
this.show = false
},
// 预览图片单张
previewImage(photoImg) {
console.log('预览图片')
photoImg = 'data:image/jpeg;base64,' + photoImg;
base64ToPath(photoImg).then((resInfo)=>{
uni.getImageInfo({
src: resInfo,
success: function(res){
uni.previewImage({
urls:[res.path]
});
},
fail: function(err){
console.log(err)
}
})
}).catch((err)=>{
console.log(err)
})
},
showTxt(item) {
item.showImage = false
// this.showImageTextIndex = index
// console.log('触发',this.showImageTextIndex)
// this.showImage = false
this.show = false
},
showImageFunction(item) {
item.showImage = true
// this.showImageTextIndex = index
// console.log('触发',this.showImageTextIndex)
// this.showImage = false
this.show = false
},
// 播放 base64 编码的 MP3 文件
playBase64Mp3(base64Data) {
innerAudioContext.src = config.baseUrl + base64Data; // 不推荐,仅用于演示
innerAudioContext.play();
// 注意:如果你使用的是音频组件,确保在模板中正确绑定和使用它
},
clickContent(index) {
if (this.info.conversation == 'Translator') {
// 非长按
this.show = '0'
this.clickIdx = null
}
},
//点击事件
clickSprink(index) {
if (this.info.conversation == 'Translator') {
// 非长按
setTimeout(() => {
this.show = '1'
this.clickIdx = index
}, 10); // 延时200毫秒即0.2秒
}
},
//点击事件
clickSprinkImage(index) {
if (this.info.conversation == 'Translator') {
// 非长按
setTimeout(() => {
this.show = '2'
this.clickIdx = index
}, 10); // 延时200毫秒即0.2秒
}
},
// 回调参数为包含columnIndex、value、values
langConfirm(e) {
this.lang = e.value[0].split('-')[0]
this.langShow = false
},
langCancel() {
this.langShow = false
},
// 回调参数为包含columnIndex、value、values
sayLangConfirm(e) {
this.sayLangStr = e.value[0].label.split('-')[0]
this.sayLang = e.value[0].value
this.sayLangShow = false
},
sayLangCancel() {
this.sayLangShow = false
},
chooseLang() {
//选择语言
this.langShow = true
},
chooseSayLang() {
//选择录音语言
this.sayLangShow = true
},
async getRecordsToken() {
var result = await permision.requestAndroidPermission('android.permission.RECORD_AUDIO')
console.log(result, 124);
var strStatus
if (result == 1) {
strStatus = "已获得授权"
} else if (result == 0) {
strStatus = "未获得授权"
} else {
strStatus = "被永久拒绝权限"
}
},
startSocket() {
this.socketId = this.userId + "_" + Date.now()
this.msgSocket = startMsgSocket(this.socketId)
this.msgInfo()
//追加心跳机制
},
translatorChinese() {
//调用api翻译成中文
request({
url: 'chatHttpApi/getChatInfo',
method: 'post',
data: {
q: this.messagesList[this.messagesList.length - 1].answer,
lang: this.lang
}
}).then(res => {
this.$nextTick(() => {
let msgItem = {
"inputs": {
"type": "text"
},
"query": '',
"response_mode": 'streaming',
"conversation_id": uni.getStorageSync(this.info.conversation) || null,
"user": this.userId,
'token': this.info.token,
'answer': res.msg,
'answerCh': '',
'showImageTextIndex': '',
'showImage': '',
'imageText': '',
'time': '',
'type': '',
'filePath': '',
"lang": "Chinese",
'isTrans':false,
};
this.messagesList.push(msgItem);
// this.messagesList[this.messagesList.length - 1].answerCh = this.messagesList[this.messagesList.length - 1].answerCh + res.msg
})
})
},
msgInfo() {
if (this.msgSocket) {
this.msgSocket.onMessage(res => {
if (this.info.conversation == 'Translator') {
if (this.messagesList[this.messagesList.length - 1].inputs.type != 'image') {
this.messagesList[this.messagesList.length - 1].answer = this.messagesList[this
.messagesList.length - 1].answer + res.data
if (this.lang == 'Chinese' || this.sayLang == 'zh-CHS') {
}else {
this.translatorChinese()
}
} else {
//将res.data 转为json
let json = JSON.parse(res.data)
this.messagesList[this.messagesList.length - 1].answer = this.messagesList[this
.messagesList.length - 1].answer + json.answer
this.messagesList[this.messagesList.length - 1].imageText = this.messagesList[this
.messagesList.length - 1].imageText + json.tranContent
}
this.goBottom()
uni.setStorageSync(this.storeList + '_' + this.info.conversation, this.messagesList)
} else {
if (res.data.indexOf("conversation_id") > -1) {
uni.setStorageSync(this.info.conversation, JSON.parse(res.data).conversation_id);
} else if (res.data.indexOf("workflow_finished") > -1) {
//代表结束
uni.setStorageSync(this.storeList + '_' + this.info.conversation, this.messagesList)
} else {
this.messagesList[this.messagesList.length - 1].answer = this.messagesList[this
.messagesList.length - 1].answer + res.data
this.goBottom()
}
}
})
}
},
//缓存中获取历史消息
getMessageByStore() {
let tempList = uni.getStorageSync(this.storeList + '_' + this.info.conversation) || []
if (tempList && tempList.length > 30) {
//截取最新的30条
this.messagesList = tempList.slice(-30);
} else {
this.messagesList = tempList
}
// 数据挂载后执行,不懂的请自行阅读 Vue.js 文档对 Vue.nextTick 函数说明。
this.$nextTick(() => {
this.$forceUpdate()
// 设置当前滚动的位置
this.scrollToView = this.scrollId;
})
},
// 获取历史消息
getMessage() {
if (!uni.getStorageSync(this.info.conversation)) {
return;
}
console.log(uni.getStorageSync(this.info.conversation), 136);
let url = 'v1/messages?user=' + this.userId + '&conversation_id=' + uni.getStorageSync(this.info
.conversation) + '&limit=' + this.limit
if (this.firstId) {
url = url + "&first_id=" + this.firstId
}
let that = this
let get = async () => {
that.ajax.flag = false;
let res = await requestChat({
url: url,
method: 'get',
token: that.info.token
})
let data = res.data
// 获取待滚动元素选择器,解决插入数据后,滚动条定位时使用。取当前消息数据的第一条信息元素
console.log(res, 144);
for (var i = 0; i < data.length; i++) {
if (i == 0) {
that.firstId = data[i].id
}
that.messagesList.push(data[i])
}
that.$set(that.messagesList, that.messagesList.length - 1, that.messagesList[that.messagesList
.length - 1])
// 数据挂载后执行,不懂的请自行阅读 Vue.js 文档对 Vue.nextTick 函数说明。
that.$nextTick(() => {
that.$forceUpdate()
// 设置当前滚动的位置
that.scrollToView = this.scrollId;
})
}
get();
},
goback() {
uni.navigateBack()
},
changeTime(date) {
return dateTime.dateTime1(date);
},
// 进行图片的预览
previewImg(e) {
let index = 0;
for (let i = 0; i < this.imgMsg.length; i++) {
if (this.imgMsg[i] == e) {
index = i;
}
}
console.log("index", index)
// 预览图片
uni.previewImage({
current: index,
urls: this.imgMsg,
longPressActions: {
itemList: ['发送给朋友', '保存图片', '收藏'],
success: function (data) {
console.log('选中了第' + (data.tapIndex + 1) + '个按钮,第' + (data.index + 1) + '张图片');
},
fail: function (err) {
console.log(err.errMsg);
}
}
});
},
//音频播放
playVoice(e) {
console.log('地址',e)
// let innerAudioContext1 = uni.createInnerAudioContext();
// innerAudioContext1.autoplay = true;
// innerAudioContext1.playbackRate = 0.5;
innerAudioContext.src = this.imagesUrl + e;
console.log(innerAudioContext.src)
innerAudioContext.play()
// innerAudioContext.onPlay(() => {
// console.log('开始播放');
// });
// innerAudioContext.onError((err) => {
// console.log('播放错误:', err);
// });
// console.log('音频开始')
// innerAudioContext.src = `data:audio/mp3;base64,${e}`;
// // innerAudioContext.src = e;
// // console.log(innerAudioContext.src);
//
// // 3. 添加事件监听
// innerAudioContext.onPlay(() => {
// console.log('开始播放');
// });
//
// innerAudioContext.onError((err) => {
// console.error('播放错误:', err);
// });
//
// // 4. 开始播放
// innerAudioContext.play();
},
//地图定位
covers(e) {
let map = [{
latitude: e.latitude,
longitude: e.longitude,
iconPath: '/static/chat/sy.png'
}]
return (map);
},
//跳转地图信息
openLocation(e) {
uni.openLocation({
latitude: e.latitude,
longitude: e.longitude,
name: e.name,
address: e.address,
success: function () {
console.log('success');
}
});
},
//接受输入内容
inputs(inputData) {
console.log(inputData, 220);
let that = this;
if (this.msgSocket) {
closeMsgSocket(this.msgSocket);
this.msgSocket = null
}
this.startSocket()
let typeStr = "text"
let msgItem = {
"inputs": {
"type": typeStr
},
"query": inputData.message,
"response_mode": 'streaming',
"conversation_id": uni.getStorageSync(this.info.conversation) || null,
"user": this.userId,
'token': this.info.token,
'answer': '',
'answerCh': '',
'showImageTextIndex': '',
'showImage': '',
'imageText': '',
'time': inputData.time,
'type': '',
'filePath': inputData.filePath,
"lang": this.lang,
'isTrans':true,
'url': inputData.url
};
if (this.info.conversation == 'Translator') {
msgItem.response_mode = 'blocking'
msgItem.inputs.lang = this.lang
}
if (inputData.type == 0) {
msgItem.inputs.type = "text"
} else if (inputData.type == 1) {
typeStr = "image"
msgItem.inputs.type = "image"
msgItem.showImage = true
msgItem.query = inputData.base64
} else if (inputData.type == 2) {
typeStr = "voice"
msgItem.inputs.type = "voice"
msgItem.query = inputData.base64
}
this.messagesList.push(msgItem);
// 数据挂载后执行,不懂的请自行阅读 Vue.js 文档对 Vue.nextTick 函数说明。
this.goBottom()
//时间间隔处理
let requestData = {
"inputs": {
"type": typeStr
},
"query": inputData.message,
"response_mode": 'streaming',
"conversation_id": uni.getStorageSync(this.info.conversation) || null,
"user": this.userId,
'token': this.info.token,
'answer': '',
'socketId': this.socketId,
};
if (this.info.conversation == 'Translator') {
requestData.response_mode = 'blocking'
requestData.inputs.lang = this.lang
requestData.inputs.sayLang = this.sayLang
} else if (this.info.conversation == 'Trip') {
if (requestData.inputs.type == 'image') {
requestData.query = "描述图片"
// requestData.inputs = {}
requestData.files = [
{
"type": "image",
// "type": "image/jpeg",
"transfer_method": "local_file",
"url": '',
"upload_file_id": inputData.message
}
]
}
setTimeout(() => {
sendMsg(that.msgSocket, JSON.stringify(requestData))
requestData.query = inputData.base64
}, 500)
return
}
setTimeout(() => {
sendMsg(that.msgSocket, JSON.stringify(requestData))
}, 500)
},
//输入框高度
heights(e) {
console.log("高度:", e)
this.inputh = e;
this.goBottom();
},
// 滚动到底部
goBottom() {
// this.scrollToBottom()
this.scrollToView = '';
this.$nextTick(() => {
// 设置当前滚动的位置
this.scrollToView = this.scrollId;
this.$forceUpdate()
})
},
// 滚动到底部
scrollToBottom() {
this.$nextTick(() => {
setTimeout(() => {
this.scrollTop = 999999; // 设一个足够大的值,确保滚动到底部
}, 50); // 增加微小延迟,等待 DOM 渲染完成
});
},
}
}
</script>
<style lang="scss">
page {
height: 100%;
}
#po_ {
position: relative;
}
.po_z {
position: absolute;
top: -60px;
left: 0px;
box-sizing: border-box;
padding: 10px;
height: 50px;
background: #3d3d3d;
display: flex;
align-items: center;
color: #fff;
border-radius: 8px;
}
.po_i {
position: absolute;
left: 0px;
top: -55px;
box-sizing: border-box;
padding: 10px;
height: 50px;
background: #3d3d3d;
display: flex;
align-items: center;
color: #fff;
border-radius: 8px;
}
.size_ {
margin-right: 10px;
}
.content {
height: 100%;
background-color: rgba(244, 244, 244, 1);
}
.top_po {
position: fixed;
z-index: 9999;
width: 750rpx;
height: 180rpx;
box-sizing: border-box;
padding-top: 80rpx;
display: flex;
align-items: center;
justify-content: space-between;
font-weight: bold;
font-size: 36rpx;
color: #FFFFFF;
padding-left: 30rpx;
padding-right: 30rpx;
left: 0px;
top: 0px;
background: #32714f;
overflow: hidden;
}
.right_top {
width: 20px;
height: 20px;
}
.chat {
height: 90%;
margin-top: 180rpx;
margin-bottom: 30rpx;
.chat-main {
padding-left: 32rpx;
padding-right: 32rpx;
padding-top: 20rpx;
// padding-bottom: 120rpx; //获取动态高度
display: flex;
flex-direction: column;
}
.chat-ls {
.chat-time {
font-size: 24rpx;
color: rgba(39, 40, 50, 0.3);
line-height: 34rpx;
padding: 10rpx 0rpx;
text-align: center;
}
.msg-m {
display: flex;
padding: 20rpx 0;
.user-img {
flex: none;
width: 80rpx;
height: 80rpx;
border-radius: 20rpx;
}
.message {
flex: none;
max-width: 480rpx;
}
.msg-text {
max-width: 540rpx;
font-size: 32rpx;
color: rgba(39, 40, 50, 1);
line-height: 44rpx;
padding: 18rpx 24rpx;
white-space: pre-wrap;
word-wrap: break-word;
position: relative;
}
.msg-img {
max-width: 400rpx;
border-radius: 20rpx;
}
.msg-map {
background: #fff;
width: 464rpx;
height: 284rpx;
overflow: hidden;
.map-name {
font-size: 32rpx;
color: rgba(39, 40, 50, 1);
line-height: 44rpx;
padding: 18rpx 24rpx 0 24rpx;
//下面四行是单行文字的样式
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
}
.map-address {
font-size: 24rpx;
color: rgba(39, 40, 50, 0.4);
padding: 0 24rpx;
//下面四行是单行文字的样式
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
}
.map {
padding-top: 8rpx;
width: 464rpx;
height: 190rpx;
}
}
.voice {
// width: 200rpx;
min-width: 100rpx;
max-width: 400rpx;
}
.voice-img {
width: 28rpx;
height: 36rpx;
}
}
.msg-left {
flex-direction: row;
.msg-text {
max-width: 540rpx;
margin-left: 16rpx;
background-color: #fff;
border-radius: 0rpx 20rpx 20rpx 20rpx;
white-space: pre-wrap;
word-wrap: break-word;
}
.ms-img {
margin-left: 16rpx;
}
.msh-map {
margin-left: 16rpx;
border-radius: 0rpx 20rpx 20rpx 20rpx;
}
.voice {
text-align: right;
}
.voice-img {
float: left;
transform: rotate(180deg);
width: 28rpx;
height: 36rpx;
padding-bottom: 4rpx;
}
}
.msg-right {
flex-direction: row-reverse;
.msg-text {
max-width: 540rpx;
margin-right: 16rpx;
background-color: rgba(255, 228, 49, 0.8);
border-radius: 20rpx 0rpx 20rpx 20rpx;
white-space: pre-wrap;
word-wrap: break-word;
}
.ms-img {
margin-right: 16rpx;
}
.msh-map {
margin-left: 16rpx;
border-radius: 20rpx 0rpx 20rpx 20rpx;
}
.voice {
text-align: left;
}
.voice-img {
float: right;
padding: 4rpx;
width: 28rpx;
height: 36rpx;
}
}
}
}
.sm-text {
font-size: 20rpx;
//margin-left: 5px;
display: flex;
align-items: center;
line-height: 29px;
}
</style>