JLBA202505130001_关于吉林银行新建AI智能培训系统的需求_开发聊天记录回答提示
This commit is contained in:
+6
-9
@@ -7,7 +7,7 @@ ENV = 'development'
|
||||
#VITE_APP_BASE_API_Url = 'https://aits.jlbank.com.cn:7002'
|
||||
|
||||
|
||||
VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7001'
|
||||
#VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7002'
|
||||
|
||||
# VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7002'
|
||||
# VITE_APP_BASE_API_Url = 'http://192.168.247.200'
|
||||
@@ -15,19 +15,14 @@ VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7001'
|
||||
# VITE_APP_BASE_API_Url = 'http://192.168.108.129'
|
||||
# dev
|
||||
|
||||
|
||||
#VITE_APP_BASE_API_Url = 'http://25.18.122.65:7001'
|
||||
|
||||
|
||||
# DEV
|
||||
VITE_APP_BASE_API_Url = 'http://25.18.122.65:7001'
|
||||
# sit
|
||||
|
||||
#VITE_APP_BASE_API_Url = 'http://25.18.122.91:9786'
|
||||
|
||||
|
||||
# UAT
|
||||
# VITE_APP_BASE_API_Url = 'http://25.18.122.78:9786'
|
||||
|
||||
# DEV
|
||||
# app入口
|
||||
|
||||
|
||||
@@ -38,7 +33,7 @@ VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7001'
|
||||
# VITE_APP_BASE_API_Url = 'http://25.18.122.66:9786'
|
||||
|
||||
# h5专用地址x2
|
||||
# VITE_APP_BASE_H5_API_Url_TRAAPP = 'http://25.64.32.150:9601'
|
||||
#VITE_APP_BASE_H5_API_Url = 'http://25.64.32.157:9601'
|
||||
# dev
|
||||
#VITE_APP_BASE_H5_API_Url = 'http://25.18.122.65:7001'
|
||||
|
||||
@@ -53,5 +48,7 @@ VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7001'
|
||||
# VITE_APP_BASE_H5_API_Url_TRAASK = 'http://25.64.16.144:9605'
|
||||
|
||||
# VITE_APP_BASE_H5_API_Url_TRAEXAM = 'http://25.64.16.154:9604'
|
||||
VITE_APP_BASE_H5_API_Url_TRAPRACTICE = 'http://25.64.32.157:9603'
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,84 +1,164 @@
|
||||
<template>
|
||||
<view class="talk-div">
|
||||
<view class="talk-list-max">
|
||||
<view class="talk-list-top-3"></view>
|
||||
<view class="talk-list-top-2"></view>
|
||||
<view class="talk-list-top-1"></view>
|
||||
<view class="talk-list">
|
||||
<view class="fl1"></view>
|
||||
<view class="talk-list-line">
|
||||
{{ talkList }}
|
||||
<view class="talk-div" :class="{ hidden }">
|
||||
<!-- 原文 -->
|
||||
<view class="talk-text-list-div">
|
||||
<view class="talk-text-list">
|
||||
<view class="talk-text-list-line">
|
||||
{{ talkTextList }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
<view class="answerTipButton">回答提示</view>
|
||||
<!-- 按钮 -->
|
||||
<view class="answerTipButton" v-show="tipButtonStatus" @click="clickTipButton">回答提示</view>
|
||||
<!-- ai提示 -->
|
||||
<view class="talk-tip-list-div">
|
||||
<view class="talk-tip-list">
|
||||
<view class="talk-tip-list-line">
|
||||
{{ talkTipList }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="fl1"></view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const textText =
|
||||
'你核心的诉求现在非常明确了:文字必须放在单个不拆分的盒子里(这样才能滑动查看上方内容),同时要实现两个效果 —— 一是文字整体越往上颜色越淡,二是底部新增文字时,内容能缓慢向上挤(有平滑动画),且不能用 JS 拆分文字行。我现在提供的方案完全满足你的要求:无 JS 拆分、单个文字盒子、支持滑动查看上方内容、文字越往上越淡、新增文字缓慢上挤动画,所有效果都通过 CSS 核心实现,JS 仅做 “追加文字” 的极简操作(无任何拆分逻辑)。';
|
||||
import { ref } from 'vue';
|
||||
const talkList = ref('');
|
||||
// 初始化文本索引,记录当前添加到哪个位置
|
||||
let currentIndex = 0;
|
||||
// 每次添加的字符数(1=逐个加,2=逐两个加,可自行调整)
|
||||
const step = 2;
|
||||
|
||||
// 定时器逐字/逐两字添加文本
|
||||
const addTextTimer = setInterval(() => {
|
||||
// 截取从当前索引开始的step个字符
|
||||
const addStr = textText.slice(currentIndex, currentIndex + step);
|
||||
// 拼接到talkList
|
||||
talkList.value += addStr;
|
||||
// 更新索引
|
||||
currentIndex += step;
|
||||
import { ref, nextTick, onMounted } from 'vue';
|
||||
const talkTextList = ref('');
|
||||
const talkTipList = ref('');
|
||||
const hidden = ref(false);
|
||||
const tipButtonStatus = ref(false);
|
||||
const aaaaaaa = ref(false);
|
||||
let clearTimer // 关闭清理的定时器
|
||||
const emits = defineEmits(['clickTipButton'])
|
||||
|
||||
// 当索引超过文本长度时,清除定时器(添加完成)
|
||||
if (currentIndex >= textText.length) {
|
||||
clearInterval(addTextTimer);
|
||||
console.log('文本添加完成');
|
||||
|
||||
// 点击回答提示按钮
|
||||
const clickTipButton = () => {
|
||||
emits('clickTipButton')
|
||||
};
|
||||
// 添加文字
|
||||
const addText = (text) => {
|
||||
talkTextList.value = talkTextList.value + text;
|
||||
};
|
||||
// 添加提示
|
||||
const addTip = (text) => {
|
||||
talkTipList.value = talkTipList.value + text;
|
||||
};
|
||||
// 展示提示框
|
||||
const showTipButton = () => {
|
||||
tipButtonStatus.value = true;
|
||||
};
|
||||
|
||||
// 清除全部数据
|
||||
const clear = () => {
|
||||
hidden.value = true;
|
||||
// clearTimer = setTimeout(() => {
|
||||
nextTick(() => {
|
||||
tipButtonStatus.value = false;
|
||||
talkTextList.value = '';
|
||||
talkTipList.value = '';
|
||||
hidden.value = false;
|
||||
clearTimer = null
|
||||
});
|
||||
// }, 300);
|
||||
};
|
||||
|
||||
// 控制状态变化
|
||||
const ctrlTalk = (state) => {
|
||||
console.log('控制状态变化', state);
|
||||
if(state === 'textStart') {
|
||||
clear() // 清除全部数据
|
||||
} else if(state === 'textEnd') {
|
||||
showTipButton() // 展示提示框
|
||||
}
|
||||
}, 70); // 70ms间隔,和你原来的节奏一致
|
||||
|
||||
};
|
||||
onMounted(() => {
|
||||
});
|
||||
defineExpose({ addText, addTip, clear, ctrlTalk });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
$talk-text-line-height: 48rpx;
|
||||
$talk-text-line-height: 50rpx;
|
||||
$talk-text-font-size: 32rpx;
|
||||
.talk-div{
|
||||
|
||||
$talk-tip-line-height: 40rpx;
|
||||
$talk-tip-font-size: 28rpx;
|
||||
.hidden {
|
||||
opacity: 0;
|
||||
transition: opacity 0.1s ease; // 0.5s=动画时长,ease=缓动曲线(先慢后快再慢)
|
||||
.talk-text-list-div {
|
||||
// opacity: 0;
|
||||
// transition: opacity 5s ease; // 0.5s=动画时长,ease=缓动曲线(先慢后快再慢)
|
||||
}
|
||||
.talk-tip-list-div {
|
||||
// opacity: 0;
|
||||
// transition: opacity 5s ease; // 0.5s=动画时长,ease=缓动曲线(先慢后快再慢)
|
||||
}
|
||||
.answerTipButton {
|
||||
// opacity: 0;
|
||||
// transition: opacity 5s ease; // 0.5s=动画时长,ease=缓动曲线(先慢后快再慢)
|
||||
}
|
||||
}
|
||||
.talk-list-max {
|
||||
// left: 0;
|
||||
// top: calc(150rpx + var(--status-bar-height) + 200rpx + 150rpx);
|
||||
.talk-text-list-div {
|
||||
position: relative;
|
||||
width: calc(100% - 60rpx);
|
||||
height: $talk-text-line-height * 6;
|
||||
width: 100%;
|
||||
height: $talk-text-line-height * 5;
|
||||
overflow: hidden;
|
||||
.talk-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
.talk-list-line {
|
||||
font-size: $talk-text-font-size;
|
||||
transition: min-height 0.3s ease-out; /* 高度变化动画:时长0.3s,缓动效果 */
|
||||
overflow: hidden; /* 隐藏超出max-height的内容 */
|
||||
min-height: 48rpx;
|
||||
transform: rotate(180deg);
|
||||
.talk-text-list {
|
||||
overflow-y: auto;
|
||||
height: $talk-text-line-height * 5;
|
||||
width: 100%;
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
}
|
||||
.talk-text-list-line {
|
||||
transform: rotate(180deg);
|
||||
line-height: $talk-text-line-height;
|
||||
font-size: $talk-text-font-size;
|
||||
vertical-align: middle;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
.answerTipButton {
|
||||
width: 160rpx;
|
||||
margin: 20rpx 0 20rpx 0;
|
||||
width: 190rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 20rpx;
|
||||
background-color: #2688F9;
|
||||
border-radius: 60rpx;
|
||||
line-height: 60rpx;
|
||||
background-color: #2688f9;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.talk-tip-list-div {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: $talk-tip-line-height * 5;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.talk-tip-list {
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
max-height: $talk-tip-line-height * 5;
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
}
|
||||
transform: rotate(180deg);
|
||||
.talk-tip-list-line {
|
||||
transform: rotate(180deg);
|
||||
line-height: $talk-tip-line-height;
|
||||
font-size: $talk-tip-font-size;
|
||||
vertical-align: middle;
|
||||
color: rgba(200, 200, 200, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -36,6 +36,36 @@ export enum MessageType {
|
||||
// 预留12种类型用于扩展(0b0100 ~ 0b1111)
|
||||
}
|
||||
|
||||
/**
|
||||
* 控制消息枚举
|
||||
* 二进制标识,统一编码风格
|
||||
*/
|
||||
export enum ControlCode {
|
||||
FINISH_PUSH = 0b0001, // 本轮tts推流结束
|
||||
FINISH_PLAY = 0b0010, // 语音播放结束(发后端)
|
||||
AI_ANSWER_BEGIN = 0b0011, // 本轮AI回答的文本内容开始
|
||||
AI_ANSWER_OVER = 0b0100, // 本轮AI回答的文本内容已经全部返回
|
||||
AI_CLUE = 0b0101, // 需要AI提示命令(发后端)
|
||||
AI_CLUE_OVER = 0b0110, // AI提示命令已经全部返回
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 错误类型枚举
|
||||
* 二进制标识,统一编码风格
|
||||
*/
|
||||
export enum ErrorType {
|
||||
PING = 2, // 认证错误
|
||||
AUDIO_DATA = 0b0010, // 纯音频数据
|
||||
TEXT_MESSAGE = 0b0011, // 大模型回复的文本消息
|
||||
CONTROL_CMD = 0b0100, // 控制指令
|
||||
IDENTITY = 0b0101, // 身份校验包json格式
|
||||
ERROR = 0b0110, // 错误信息json格式
|
||||
TIP_MESSAGE = 0b0111 // 大模型生成的回答提示
|
||||
// 预留12种类型用于扩展(0b0100 ~ 0b1111)
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化方式枚举(3位,存储在字节1高3位,1~8范围)
|
||||
* 二进制标识,统一编码风格(1~8对应0b001~0b111)
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import {
|
||||
ProtocolCodec,
|
||||
MessageType,
|
||||
ControlCode,
|
||||
SerializationType,
|
||||
CompressionType,
|
||||
ControlCommand,
|
||||
@@ -31,9 +32,12 @@ export default function useWebSocket(aaas = null, options = {}) {
|
||||
const isRecording = ref(false) //是否接通
|
||||
const ws = ref(null)
|
||||
const initConnection = ({
|
||||
authParams = {},
|
||||
onStartRecord,
|
||||
playPcmCallback,
|
||||
authParams = {}, // 认证信息
|
||||
onStartRecord, // 连接成功启动录音方法
|
||||
playPcmCallback, // 播放返回音频的方法
|
||||
addTalkText, // 显示返回的文字的方法
|
||||
addTalkTip, // 显示返回的文字提示的方法
|
||||
ctrlTalk, // 控制展示数据组件
|
||||
setPlayNumber = () => {}, //设置当前播放的音频的id,用于播放完成回调
|
||||
onError = () => {},
|
||||
onClose = () => {},
|
||||
@@ -83,51 +87,51 @@ export default function useWebSocket(aaas = null, options = {}) {
|
||||
playPcmCallback(numArray)
|
||||
break;
|
||||
case MessageType.ERROR:
|
||||
switch (body.errCode) {
|
||||
case 1:
|
||||
// ASR或TTS初始化出错
|
||||
console.log('后端返回的错误', body.errMsg);
|
||||
case 4:
|
||||
console.log('通话接通失败', body.errMsg);
|
||||
// 通话接通失败,请稍后再试!
|
||||
ws.value.close({
|
||||
code: 4001,
|
||||
reason: '当前服务器繁忙,请稍后在试!'
|
||||
})
|
||||
onClose({
|
||||
code: 4001,
|
||||
reason: '当前服务器繁忙,请稍后在试!'
|
||||
})
|
||||
break;
|
||||
case 5: // 使用人数过多等其他错误
|
||||
ws.value.close({
|
||||
code: 4001,
|
||||
reason: body.errMsg
|
||||
})
|
||||
break;
|
||||
default:
|
||||
console.log('其他错误码的默认处理', body);
|
||||
// 其他错误码的默认处理(如果需要)
|
||||
// ws.value.close({
|
||||
// code: 1001,
|
||||
// reason: body.errMsg
|
||||
// })
|
||||
break;
|
||||
if([1,2,4].includes(body.errCode)) {
|
||||
// 1 ASR或TTS初始化出错
|
||||
// 2 认证错误
|
||||
// 4 通话接通失败,请稍后再试!
|
||||
console.error('后端返回的错误码:', body.errCode, '错误消息:',body.errMsg);
|
||||
error = {
|
||||
code: 4001,
|
||||
reason: '当前服务器繁忙,请稍后在试!'
|
||||
}
|
||||
ws.value.close(error)
|
||||
onClose(error)
|
||||
} else if(body.errCode === 5) {
|
||||
// 5 使用人数过多等其他错误
|
||||
ws.value.close({
|
||||
code: 4001,
|
||||
reason: body.errMsg
|
||||
})
|
||||
} else {
|
||||
console.error('其他错误码的默认处理', body);
|
||||
}
|
||||
break;
|
||||
case MessageType.CONTROL_CMD:
|
||||
// 控制消息
|
||||
console.log('控制消息', msgType, body);
|
||||
if (body.type === 1) { // 开始播放,记录当前播放的语音id
|
||||
console.log('控制消息', body, body.type === ControlCode.AI_ANSWER_BEGIN);
|
||||
console.log('控制消息2', ControlCode.AI_ANSWER_BEGIN);
|
||||
|
||||
if (body.type === ControlCode.FINISH_PUSH) { // 开始播放,记录当前播放的语音id
|
||||
console.log('开始播放,记录当前播放的语音id', body.lock);
|
||||
setPlayNumber(body.lock)
|
||||
} else if (body.type === ControlCode.AI_ANSWER_BEGIN) {
|
||||
console.log('新一段对话内容来了');
|
||||
ctrlTalk('textStart') // 新一段对话内容来了
|
||||
} else if (body.type === ControlCode.AI_ANSWER_OVER) {
|
||||
console.log('新一段对话结束了');
|
||||
ctrlTalk('textEnd') // 新一段对话内容来了
|
||||
}
|
||||
break;
|
||||
case MessageType.TEXT_MESSAGE:
|
||||
console.log('TEXT_MESSAGE', body);
|
||||
addTalkText(body)
|
||||
break;
|
||||
case MessageType.TIP_MESSAGE:
|
||||
console.log('消息提示', body);
|
||||
addTalkTip(body)
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
console.log('其他消息', msgType, body);
|
||||
break;
|
||||
|
||||
+27
-13
@@ -36,7 +36,7 @@
|
||||
style="width: 240rpx; height: 360rpx; position: absolute; left: 0; top: 0"
|
||||
></canvas>
|
||||
</view>
|
||||
<talkList class="talk-list"></talkList>
|
||||
<talkList ref="talkListRef" class="talk-list" @clickTipButton="clickTipButton"></talkList>
|
||||
</view>
|
||||
<talk-microphone ref="talkMicrophoneRef" @noPermission="microphoneNoPermission"></talk-microphone>
|
||||
<talk-speaker ref="talkSpeakerRef" @playOver="playOver"></talk-speaker>
|
||||
@@ -44,9 +44,9 @@
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onUnmounted, computed, getCurrentInstance, nextTick, toRaw } from 'vue';
|
||||
import { ref, reactive, onMounted, onUnmounted, computed, getCurrentInstance, nextTick } from 'vue';
|
||||
import { onShow, onLoad, onUnload, onBackPress, onReady } from '@dcloudio/uni-app';
|
||||
import { ProtocolCodec, MessageType } from './js/ProtocolCodec';
|
||||
import { ProtocolCodec, MessageType,ControlCode } from './js/ProtocolCodec';
|
||||
import common from '@/common/common';
|
||||
import talkMicrophone from './components/talk-microphone.vue';
|
||||
import talkSpeaker from './components/talk-speaker.vue';
|
||||
@@ -55,10 +55,10 @@
|
||||
import useCamera from './js/useCamera';
|
||||
import talkList from './components/talk-list';
|
||||
|
||||
|
||||
const talkMicrophoneRef = ref(null);
|
||||
const talkSpeakerRef = ref(null);
|
||||
const talkCameraRef = ref(null);
|
||||
const talkListRef = ref(null);
|
||||
|
||||
const { initConnection, close, send, isRecording } = useWebSocket();
|
||||
const { cameraRef, isIOS, cameraInit, cameraStart, cameraClose } = useCamera();
|
||||
@@ -83,6 +83,12 @@
|
||||
sceneDescShow.value = !sceneDescShow.value;
|
||||
};
|
||||
|
||||
// 点击回答提示
|
||||
const clickTipButton = () => {
|
||||
console.log('点击回答提示', {type:ControlCode.AI_CLUE});
|
||||
send(ProtocolCodec.pack(MessageType.CONTROL_CMD, {type:ControlCode.AI_CLUE}))
|
||||
}
|
||||
|
||||
// 关闭通话
|
||||
const closeTalking = async () => {
|
||||
// todo 防抖
|
||||
@@ -104,6 +110,7 @@
|
||||
|
||||
// 关闭销毁流程,逐层清理
|
||||
const destroy = (type = 0) => {
|
||||
console.log('关闭销毁流程,逐层清理');
|
||||
clearAutomaticEndTimeTimer();
|
||||
if (type <= 0) {
|
||||
isVideo.value && cameraClose();
|
||||
@@ -220,8 +227,8 @@
|
||||
// 启动没人说话定时器
|
||||
const startAutomaticEndTimeTimer = () => {
|
||||
// 打开一个计时器,时间到了就中断
|
||||
|
||||
automaticEndTimeTimer = setTimeout(() => {
|
||||
console.log('没人说话挂断');
|
||||
destroy();
|
||||
}, automaticEndTime);
|
||||
};
|
||||
@@ -279,6 +286,7 @@
|
||||
});
|
||||
onMounted(() => {
|
||||
initConnection({
|
||||
// 认证信息
|
||||
authParams: {
|
||||
traId: dataInfo.traId,
|
||||
chaId: dataInfo.chaId,
|
||||
@@ -286,8 +294,13 @@
|
||||
traInterTyp: dataInfo.traInterTyp,
|
||||
preview: dataInfo.preview
|
||||
},
|
||||
onStartRecord: onStartRecord,
|
||||
// 连接成功启动录音方法
|
||||
onStartRecord,
|
||||
// 播放返回音频的方法
|
||||
playPcmCallback: (res) => talkSpeakerRef.value.playPCM(res),
|
||||
addTalkText:(text) => talkListRef.value.addText(text), // 显示返回的文字的方法
|
||||
addTalkTip:(text) =>talkListRef.value.addTip(text), // 显示返回的文字提示的方法
|
||||
ctrlTalk:(state) => talkListRef.value.ctrlTalk(state), // 控制展示数据组件
|
||||
setPlayNumber: (id) => {
|
||||
clearAutomaticEndTimeTimer();
|
||||
console.log('新一句话开始');
|
||||
@@ -324,9 +337,9 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.talk-list{
|
||||
.talk-list {
|
||||
left: 30rpx;
|
||||
top: calc(150rpx + var(--status-bar-height) + 200rpx + 150rpx);
|
||||
top: calc(150rpx + var(--status-bar-height) + 200rpx + 180rpx);
|
||||
position: absolute;
|
||||
width: calc(100% - 60rpx);
|
||||
}
|
||||
@@ -335,13 +348,14 @@
|
||||
.role-info {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.show-box {
|
||||
background: linear-gradient(
|
||||
120deg,
|
||||
rgba(51, 51, 51, 0.2) 0%,
|
||||
rgba(102, 102, 102, 0.2) 50%,
|
||||
rgba(51, 51, 51, 0.2) 100%
|
||||
0deg,
|
||||
rgba(50, 50, 50, 1) 0%,
|
||||
rgba(50, 50, 50, 1) 15%,
|
||||
rgba(50, 50, 50, 0.8) 75%,
|
||||
rgba(50, 50, 50, 0.8) 85%,
|
||||
rgba(50, 50, 50, 0.9) 100%
|
||||
);
|
||||
backdrop-filter: blur(0px);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user