flinfo/dc-App/pages/Chat/newChat.vue

904 lines
24 KiB
Vue
Raw Normal View History

2025-03-01 10:26:49 +08:00
<template>
2025-03-07 17:57:49 +08:00
<!-- 聊天界面展示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">
<u-icon name="arrow-rightward" color="#fff" size="20"></u-icon>
</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">
<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'">
<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.filePath)">
<!-- 音频 -->
<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" 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.answer)">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="[11]" :columns="columns" @confirm="langConfirm"
@cancel="langCancel"></u-picker>
<u-picker :show="sayLangShow" ref="langPicker" :defaultIndex="[1]" keyName="label" :columns="[sayLangColumns]"
@confirm="sayLangConfirm"
@cancel="sayLangCancel"></u-picker>
</view>
2025-03-01 10:26:49 +08:00
</template>
<script>
2025-03-07 17:57:49 +08:00
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'
//音频播放
const innerAudioContext = uni.createInnerAudioContext();
// 录音
const recorderManager = uni.getRecorderManager();
export default {
data() {
return {
audioSrc: '', // 用于存储音频的 URL
clickIdx: null,
//是否长按事件
timer: null,//长按计时器
show: '0',
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.getRecordsToken()
},
components: {
submit,
},
methods: {
async voiceTxt(text) {
let res = await request({
url: 'youDaoApi/tts',
method: 'post',
data: {
q: text,
voiceName: this.lang,
}
})
this.playBase64Mp3(res.data)
this.show = false
},
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()
//追加心跳机制
},
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
//调用api翻译成中文
request({
url: 'chatHttpApi/getChatInfo',
method: 'post',
data: {
q: this.messagesList[this.messagesList.length - 1].answer,
lang: this.lang
}
}).then(res => {
this.$nextTick(() => {
this.messagesList[this.messagesList.length - 1].answerCh = this.messagesList[this.messagesList.length - 1].answerCh + res.msg
})
})
} 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);
innerAudioContext.src = e;
innerAudioContext.play(() => {
console.log('开始播放');
});
},
//地图定位
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,
};
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') {
2025-03-10 15:10:08 +08:00
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
2025-03-07 17:57:49 +08:00
}
setTimeout(() => {
sendMsg(that.msgSocket, JSON.stringify(requestData))
}, 500)
},
//输入框高度
heights(e) {
console.log("高度:", e)
this.inputh = e;
this.goBottom();
},
// 滚动到底部
goBottom() {
this.scrollToView = '';
this.$nextTick(() => {
// 设置当前滚动的位置
this.scrollToView = this.scrollId;
this.$forceUpdate()
})
}
}
}
2025-03-01 10:26:49 +08:00
</script>
<style lang="scss">
2025-03-07 17:57:49 +08:00
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;
2025-03-10 15:10:08 +08:00
2025-03-07 17:57:49 +08:00
left: 0px;
2025-03-10 15:10:08 +08:00
top: -55px;
2025-03-07 17:57:49 +08:00
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;
2025-03-10 15:10:08 +08:00
position: relative;
2025-03-07 17:57:49 +08:00
}
.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: 12px;
margin-left: 5px;
display: flex;
align-items: center;
line-height: 29px;
}
2025-03-03 14:31:40 +08:00
</style>