JLBA202505130001_关于吉林银行新建AI智能培训系统的需求_开发聊天记录回答提示

This commit is contained in:
田岩
2026-01-20 16:06:04 +08:00
parent 6ffe01bcf5
commit d7f885b66c
5 changed files with 237 additions and 112 deletions
+6 -9
View File
@@ -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'
+133 -53
View File
@@ -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>
+30
View File
@@ -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
+41 -37
View File
@@ -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
View File
@@ -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);
}