Files
tra-app/src/pages/course/components/talkingItem.vue
T
2025-07-11 11:08:51 +08:00

758 lines
21 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.
<!-- type: 0纯文本信息不翻译 1语音回答 2 文本回答 3, 段落信息 4 问题信息 5 统计信息 6: 发送loading状态 7: 教师反馈文本不翻译 -->
<template>
<view :class="['ai-answer', props.class]" :id="props.id" v-if="[3,4,5].indexOf(type) >= 0">
<view class="user-info">
<view class="avatar-box">
<ImagePreview class="img-box" :imgId="dataInfo?.userInfo?.imgAddr"></ImagePreview>
</view>
<view class="ai-name">
{{ dataInfo?.userInfo?.teacherName }}
<view class="ai-name-desc">{{dataInfo?.userInfo?.teacherDesc}}</view>
</view>
</view>
<view :class="['talking-info', [4].indexOf(type) >= 0 ? 'talking-info-less':''] ">
<view :class="['talking-gif', !voiceLoading && (activeVoiceId === dataInfo.id)?'is-play':'']" v-if="type !== 5">
<CommonLoading color="#06f" v-show="voiceLoading"/>
</view>
<view class="talking-cont">
<CommonLoading color="#06f" v-show="dataInfo.isLoading"/>
<text class="talking-text" v-if="[3, 5, 6].indexOf(type) >= 0 && !dataInfo.isLoading">
{{ showContent }}
</text>
<text class="talking-text" v-if="[4].indexOf(type) >= 0">
<!-- 请您思考并作答:<br> -->
{{ showContent }}
</text>
<view v-if="type === 3">
<ImagePreview class="cont-img-box" v-if="(dataInfo.imageAddrs||[]).length>0" :imgId="dataInfo.imageAddrs[0]"></ImagePreview>
<VideoPreview class="cont-img-box" v-if="(dataInfo.vidoAddrs||[]).length>0" :imgId="dataInfo.vidoAddrs[0]"></VideoPreview>
</view>
<view v-if="type === 5 && !dataInfo.isLoading" style="padding-top: 16rpx;">
<ChartFour v-if="dataInfo.evalDimens.length === 4" :dataList="dataInfo.evalDimens" :score="dataInfo.intrctGrade"/>
<ChartThree v-if="dataInfo.evalDimens.length === 3" :dataList="dataInfo.evalDimens" :score="dataInfo.intrctGrade"/>
</view>
</view>
<!-- <view class="talking-cont">
<view class="talking-text">
阿列克山大是快乐的你艾德卡省的mask安东拉森扣篮大赛了快递那里是啊克里斯蒂娜老师的阿萨是看
</view>
</view> -->
<view class="btns-cont" v-show="!dataInfo.isLoading">
<view class="replay-btn" @click.stop="restartPlay" v-if="!([5].indexOf(type) >= 0)">
<image class="icon" src="@/static/images/course/replay.png" style="width: 100%;height: 100%;"></image>
</view>
<view class="play-btn" v-if="!([5].indexOf(type) >= 0)">
<image
class="icon"
src="@/static/images/course/play.png"
style="width: 100%;height: 100%;"
@click.stop="playVoice()"
v-show="!isPlaying"
></image>
<image
class="icon"
v-show="isPlaying"
src="@/static/images/course/pause.png"
style="width: 100%;height: 100%;"
@click.stop="pauseVoice"
></image>
</view>
<view
class="next-learn-btn"
v-if="showNextBtn && [5].indexOf(type) >= 0 && isLast && dataInfo.passStat === 'PS'"
@click.stop="goOnGetQues"
>
继续学
<image class="icon icon-iamge" src="@/static/images/course/goon.png" ></image>
</view>
<view
class="next-learn-btn restart"
v-if="showNextBtn && [5].indexOf(type) >= 0 && isLast"
@click.stop="goOnRestart"
>
重新答
<image class="icon icon-iamge" src="@/static/images/course/restart.png" ></image>
</view>
<view class="replay-study-btn next-learn-btn"
v-if="showNextBtn && [5].indexOf(type) >= 0 && isLast"
@click.stop="goOnRestartPrgh"
>
重新学
<image class="icon icon-iamge" src="@/static/images/course/refresh-white.png" ></image>
</view>
<view class="next-btn" @click.stop="goOnGetQues" v-if="showGoonBtn">继续
<image class="icon icon-iamge" src="@/static/images/course/goon.png" ></image></view>
</view>
<view class="talking-cont last-cont" v-if=" [5].indexOf(type) >= 0 && !dataInfo.isLoading">
<view class="talking-text">
{{ changeBr(dataInfo.suggestContent || '--') }}
</view>
</view>
</view>
</view>
<view :class="['ai-answer', props.class]" :id="props.id" v-if="[7].indexOf(type) >= 0">
<view class="user-info">
<view class="avatar-box">
<ImagePreview class="img-box" :imgId="dataInfo?.userInfo?.imgAddr"></ImagePreview>
</view>
<view class="ai-name">
{{ dataInfo?.userInfo?.teacherName }}
<view class="ai-name-desc">{{dataInfo?.userInfo?.teacherDesc}}</view>
</view>
</view>
<view :class="['talking-info', [4,7].indexOf(type) >= 0 ? 'talking-info-less':''] ">
<view class="talking-cont">
<view class="talking-text">
{{ showContent }}
</view>
</view>
</view>
</view>
<view :class="['user-answer', props.class]" :id="props.id" v-else-if="[1, 2].indexOf(type) >= 0">
<view class="user-info">
<view class="avatar-box">
<image v-if="userInfo.imagePath" class="img-box" :src="userInfo.imagePath" style="width: 100%; height: 100%;"></image>
<ImagePreview v-else-if="userInfo.imageAddr" class="img-box" :src="userInfo.imageAddr" style="width: 100%; height: 100%;"></ImagePreview>
<image v-else class="img-box" src="/static/images/me/default_user_avatar.png" style="width: 100%; height: 100%;"></image>
</view>
<view class="ai-name">
{{ userInfo.userName }}
</view>
</view>
<view class="talking-info">
<view :class="['talking-gif', !voiceLoading && (activeVoiceId === dataInfo.id) ? 'is-play' : '']">
<CommonLoading color="#fff" v-show="voiceLoading || dataInfo.isLoading"/>
</view>
<view class="talking-cont" v-show="!dataInfo.isLoading">
<text :class="['talking-text translate-text loading']" v-show="!translateLoading">
{{ showContent }}
</text>
<view class="talking-text translate-text" v-if="translateLoading">
<CommonLoading color="#fff"/>
</view>
</view>
<view class="btns-cont" v-show="!dataInfo.isLoading">
<!-- <view class="replay-btn">
<image class="icon" src="@/static/images/course/replay.png" style="width: 100%;height: 100%;"></image>
</view> -->
<view class="play-btn">
<image class="icon"
@click.stop="playQnsVoice('qns', dataInfo)"
v-show="!isPlaying"
src="@/static/images/course/play-blue.png"
style="width: 100%;height: 100%;"
></image>
<image class="icon"
v-show="isPlaying"
@click.stop="pauseVoice"
src="@/static/images/course/stop-blue.png"
style="width: 100%;height: 100%;"
></image>
</view>
<view class="icon text-btn" v-if="type === 1" @click.stop="translateVoice(dataInfo)"></view>
<view class="icon text-right-btn" @click.stop="finishQuestion(dataInfo)" v-if="isLast">完成</view>
<view class="icon text-right-btn" @click.stop="withdraw(dataInfo)" v-show="showWithdraw">撤回
</view>
</view>
</view>
</view>
<view :class="['user-answer', props.class]" :id="props.id" v-else-if="[0, 6].indexOf(type) >= 0">
<view class="user-info">
<view class="avatar-box">
<image v-if="userInfo.imagePath" class="img-box" :src="userInfo.imagePath" style="width: 100%; height: 100%;"></image>
<ImagePreview v-else-if="userInfo.imageAddr" class="img-box" :src="userInfo.imageAddr" style="width: 100%; height: 100%;"></ImagePreview>
<image v-else class="img-box" src="/static/images/me/default_user_avatar.png" style="width: 100%; height: 100%;"></image>
</view>
<view class="ai-name">
{{ userInfo.userName }}
</view>
</view>
<view class="talking-info-less">
<text class="talking-text" v-if="type === 0">
{{ showContent }}
</text>
<view class="talking-text loading" v-if="type === 6">
<CommonLoading color="#fff"/>
</view>
</view>
</view>
<view v-else></view>
</template>
<script setup>
import { watch, unref, ref, toRefs, computed, nextTick,onMounted } from 'vue'
import ImagePreview from '@/components/image-preview/image-preview.vue'
import VideoPreview from '@/components/image-preview/video-preview.vue'
import { downloadVoiceFile, uploadVoiceFile, } from "@/api/common.js"
import { studyParagraphOverApi, getAskAboutRawApi } from "@/api/study.js"
import ChartFour from "./chartFour.vue"
import ChartThree from "./chartThree.vue"
import common from '@/common/common';
import { getUserInfo } from '@/common/common';
import { useStorageStore } from "@/store/storage.js"
import { onUnload } from '@dcloudio/uni-app'
const storageStore = useStorageStore()
const audioCacheObj = computed(() => storageStore.audioObj)
const props = defineProps({
type:{
default: 0,// 1 userAnswer 0: AIAnswer
type:[Number,String]
},
dataInfo: {
default: () => ({}),
type: Object
},
activeVoiceId:{
default:'',
type: String
},
isLast:{
default:false,
type: Boolean
},
isPlaying:{
default:false,
type: Boolean
},
voiceRate: {
default: '1.0',
type:[Number,String],
},
isTransform:{
default: false,
type: Boolean
},
lastQuestionInfo: {
default: () => ({}),
type: Object
},
class: {
default:'',
type: String
},
id: {
default:'',
type: String
}
})
const { type, dataInfo, activeVoiceId, isPlaying, isLast, lastQuestionInfo} = toRefs(props)
const emit = defineEmits(['changePlay', 'nextQuestion', 'withdraw', 'finishQuestion'])
// 声音播放初始化
const talkVoiceObj = ref(uni.createInnerAudioContext())
talkVoiceObj.value.playbackRate = Number(props.voiceRate)
const userInfo = computed(() => getUserInfo())
const showWithdraw = computed(()=>{
let _isMe = unref(lastQuestionInfo).qnsLogId === unref(dataInfo).qnsLogId
let _isQuestion = unref(lastQuestionInfo).isQuestion
return _isMe && _isQuestion
})
const voiceLoading = ref(false)
watch(() => props.isPlaying,(val) => {
if(!val){
talkVoiceObj.value?.pause()
}
},{
deep: true,
immediate: true
})
watch(() => props.voiceRate,(val) => {
talkVoiceObj.value.playbackRate = Number(val)
},{
deep: true,
immediate: true
})
watch(()=>props.lastTalkingInfo,()=>{},{
deep: true,
immediate: true
})
watch(() => props.dataInfo,()=>{},{
deep: true,
immediate: true
})
const transformContent = ref('')
const showContentBase = computed(() => {
if(props.isTransform){
return transformContent.value
}else{
if(props.type === 1) return unref(dataInfo)?.chatContent || unref(dataInfo)?.talkingContent
else if(props.type === 2) return unref(dataInfo)?.chatContent || unref(dataInfo)?.talkingContent
else if(props.type === 3) return unref(dataInfo)?.chatContent || unref(dataInfo)?.pgrphContent
else if(props.type === 4) return unref(dataInfo)?.chatContent || unref(dataInfo)?.qnsContent
else if(props.type === 5) return unref(dataInfo)?.chatContent || unref(dataInfo)?.evalContent
else if(props.type === 0) return unref(dataInfo)?.chatContent || unref(dataInfo)?.talkingContent
else return unref(dataInfo)?.chatContent || unref(dataInfo)?.talkingContent
}
})
const showContent = computed(() => {
let _str = showContentBase.value || ''
return _str.replace(/\/n/g,"\n")
})
const changeBr = (str = '') => {
return str.replace(/\/n/g,"\n")
}
onUnload(()=>{
pauseVoice()
})
talkVoiceObj.value.onEnded(()=>{
emit('changePlay', '')
talkVoiceObj.value.src = ''
})
talkVoiceObj.value.onError(()=>{
emit('changePlay', '')
talkVoiceObj.value.src = ''
})
const pauseVoice = () => {
if(!talkVoiceObj.value) return
talkVoiceObj.value.pause()
emit('changePlay', '')
}
// 重播
const restartPlay = () => {
if(itemVoice.value){
emit('changePlay', unref(dataInfo)?.id)
setTimeout(()=>{
talkVoiceObj.value.src = itemVoice.value
talkVoiceObj.value.play()
},100)
}else{
playVoice()
}
}
const itemVoice = ref('')
const playVoice = (readType = 'pgth') =>{
let _content = showContent.value
if(!_content){
common.msg('没有可以播放的文字信息!')
return
}
// readType 阅读内容类型pgrh段落/qns问题
emit('changePlay', unref(dataInfo)?.id)
if(talkVoiceObj.value.src && talkVoiceObj.value.src === itemVoice.value) {
talkVoiceObj.value.play()
return
}
if(itemVoice.value){
setTimeout(()=>{
talkVoiceObj.value.src = itemVoice.value
talkVoiceObj.value.play()
},100)
}else{
voiceLoading.value = true
let _params = {
crsId: unref(dataInfo)?.crsId || '',
pgrphId: unref(dataInfo)?.pgrphId || '',
qnsId: unref(dataInfo)?.qnsId || '',
readType: readType,
teachNo: unref(dataInfo)?.userInfo?.teacherNo, // 必填
content: _content
}
let _url = '/trastudy/traStdyInfo/readContent?' + Object.keys(_params).map(t=>{
return encodeURIComponent(t) + '=' + encodeURIComponent(_params[t])
}).join('&')
storageStore.getStorageById('audio', _url, (url)=>{
if(url){
voiceLoading.value = false
itemVoice.value = url
talkVoiceObj.value.src = url
talkVoiceObj.value.play()
}else{
downloadVoiceFile(_url).then(res=>{
if(res?.data){
common.msg(res.data.message)
return
}
voiceLoading.value = false
itemVoice.value = res.tempFilePath
talkVoiceObj.value.src = res.tempFilePath
talkVoiceObj.value.play()
storageStore.setStorage('audio', _url, itemVoice.value)
}).catch((err)=>{
voiceLoading.value = false
})
}
})
}
}
const playQnsVoice = (readType = 'qns', item) =>{
if(type.value === 2){
// 文本转语音
playVoice(readType)
return
}
// readType 阅读内容类型pgrh段落/qns问题
emit('changePlay', unref(dataInfo)?.id)
setTimeout(()=>{
itemVoice.value = item.talkingVoice
talkVoiceObj.value.src = item.talkingVoice
talkVoiceObj.value.play()
},100)
}
const translateLoading = ref(false)
// 翻译
const translateVoice = (item) =>{
if(!item.talkingVoice) return
translateLoading.value = true
uploadVoiceFile(item.talkingVoice,{}).then(res=>{
let _data = JSON.parse(res.data)
item.talkingContent = _data.body.text
translateLoading.value = false
}).catch(()=>{
translateLoading.value = false
})
}
const showNextBtn = ref(true)
const showGoonBtn = computed(()=>{
return showNextBtn.value && [3, 6].indexOf(props.type) >= 0 && isLast.value && !isPlaying.value
})
const goOnGetQues = () => {
if(props.type === 5){
emit('nextQuestion', unref(dataInfo))
}else if(props.type === 3){
// studyParagraphOverApi({
// pgrphId:unref(dataInfo)?.pgrphId,
// logId:unref(dataInfo)?.logId,
// }).then(res=>{
// showNextBtn.value = false
// })
emit('nextQuestion', unref(dataInfo), 'overGraph')
}
}
const goOnRestart = () => {
emit('nextQuestion', unref(dataInfo), 'reStart')
}
const goOnRestartPrgh = () => {
emit('nextQuestion', unref(dataInfo), 'reStartPrgh')
}
// 撤回
const withdraw = (data) => {
emit('withdraw',data)
}
// 完成
const finishQuestion = (data) => {
emit('finishQuestion',data)
}
</script>
<style lang="scss">
.ai-answer{
width: 100%;
overflow: hidden;
margin-bottom: 20rpx;
.user-info{
overflow: hidden;
.avatar-box{
float: left;
width: 76rpx;
height: 76rpx;
border: 3rpx solid #fff;
background-color: #ccc;
overflow: hidden;
border-radius: 10rpx;
margin-right: 16rpx;
margin-bottom: 16rpx;
.img-box{
width: 100%;
height: 100%;
}
}
.ai-name{
float: left;
color: #666;
font-size: 25rpx;
line-height: 35rpx;
.ai-name-desc{
color: #999;
margin-left: 16rpx;
display: inline-block;
}
}
}
.talking-info{
padding: 20rpx;
border-radius: 15rpx;
background-color: #fff;
overflow: hidden;
padding-bottom: 30rpx;
&.talking-info-less{
float: left;
min-width: 400rpx;
padding-bottom: 20rpx;
}
.talking-gif{
width: 200rpx;
height: 38rpx;
background: url(@/static/images/course/voice-gray.png) no-repeat;
background-size: contain;
padding-left: 168rpx;
&.is-play{
background: url(@/static/images/course/voice-gif.gif) no-repeat;
background-size: contain;
}
}
.talking-cont{
margin: 16rpx 0;
overflow: hidden;
min-height: 48rpx;
&.last-cont{
margin-bottom: 0;
.talking-text{
margin-bottom: 0;
}
}
.talking-text{
font-size: 28rpx;
line-height: 48rpx;
min-height: 48rpx;
margin-bottom: 20rpx;
word-break: break-all;
white-space: pre-line;
&.loading{
// min-height: 36px;
}
}
.cont-img-box{
width: 433rpx;
height: 269rpx;
border-radius: 15rpx;
overflow: hidden;
background-color: #eee;
margin-bottom: 20rpx;
}
}
.btns-cont{
overflow: hidden;
border-top: 3rpx solid #eee;
.replay-btn{
float: left;
width: 46rpx;
height: 46rpx;
overflow: hidden;
margin-right: 16rpx;
margin-top: 25rpx;
}
.play-btn{
float: left;
width: 46rpx;
height: 46rpx;
overflow: hidden;
margin-right: 16rpx;
margin-top: 25rpx;
}
.next-learn-btn{
float: left;
padding: 0 60rpx 0 20rpx;
height: 62rpx;
line-height: 62rpx;
font-size: 30rpx;
background-color: #06f;
color: #fff;
border-radius: 8rpx;
margin-top: 16rpx;
margin-right: 16rpx;
position: relative;
&.replay-study-btn{
float: right;
}
&.restart{
background-color: #eaf2ff;
color: #06f;
}
.icon-iamge{
width: 25rpx;
height: 25rpx;
position: absolute;
right: 20rpx;
top: 50%;
margin-top: -12rpx;
}
}
.replay-learn-btn{
float: left;
padding: 0 60rpx 0 20rpx;
height: 62rpx;
line-height: 62rpx;
font-size: 30rpx;
color: #06f;
background-color: #eaf2ff;
border-radius: 8rpx;
margin-top: 16rpx;
margin-right: 16rpx;
position: relative;
.icon-iamge{
width: 25rpx;
height: 25rpx;
position: absolute;
right: 20rpx;
top: 50%;
margin-top: -12rpx;
}
}
.next-btn{
float: right;
padding: 0 60rpx 0 20rpx;
height: 62rpx;
line-height: 62rpx;
font-size: 30rpx;
background-color: #06f;
color: #fff;
border-radius: 8rpx;
margin-top: 16rpx;
position: relative;
.icon-iamge{
width: 25rpx;
height: 25rpx;
position: absolute;
right: 20rpx;
top: 50%;
margin-top: -12rpx;
}
}
}
}
}
.user-answer{
overflow: hidden;
width: 100%;
margin-bottom: 20rpx;
.user-info{
overflow: hidden;
.avatar-box{
float: right;
width: 76rpx;
height: 76rpx;
border: 3rpx solid #fff;
background-color: #ccc;
overflow: hidden;
border-radius: 10rpx;
margin-left: 16rpx;
margin-bottom: 16rpx;
.img-box{
width: 100%;
height: 100%;
}
}
.ai-name{
float: right;
color: #666;
font-size: 25rpx;
line-height: 35rpx;
}
}
.talking-info-less{
padding: 20rpx;
float: right;
border-radius: 15rpx;
background-color: #06f;
color: #fff;
}
.talking-info{
padding: 20rpx;
border-radius: 15rpx;
background-color: #06f;
padding-bottom: 30rpx;
color: #fff;
.talking-gif{
width: 200rpx;
height: 38rpx;
background: url(@/static/images/course/voice-gray.png) no-repeat;
background-size: contain;
padding-left: 168rpx;
&.is-play{
background: url(@/static/images/course/voice-white.gif) no-repeat;
background-size: contain;
}
}
.talking-cont{
margin-top: 16rpx;
overflow: hidden;
.talking-text{
font-size: 28rpx;
line-height: 48rpx;
margin-bottom: 20rpx;
word-break: break-all;
white-space: pre-line;
overflow: hidden;
&.translate-text{
min-height: 48rpx;
}
}
}
.btns-cont{
overflow: hidden;
border-top: 3rpx solid #eee;
.replay-btn{
float: left;
width: 46rpx;
height: 46rpx;
overflow: hidden;
margin-right: 16rpx;
margin-top: 25rpx;
}
.play-btn{
float: left;
width: 46rpx;
height: 46rpx;
overflow: hidden;
margin-right: 16rpx;
margin-top: 25rpx;
}
.text-btn{
float: left;
width: 46rpx;
height: 46rpx;
overflow: hidden;
line-height: 46rpx;
text-align: center;
color: #fff;
margin-right: 16rpx;
margin-top: 25rpx;
background-color: #67A4ff;
font-size: 24rpx;
font-weight: bold;
border-radius: 20rpx;
}
.text-right-btn{
float: right;
padding: 0 10rpx;
height: 46rpx;
overflow: hidden;
line-height: 46rpx;
text-align: center;
margin-left: 16rpx;
margin-top: 25rpx;
}
.next-btn{
float: right;
padding: 0 60rpx 0 20rpx;
height: 62rpx;
line-height: 62rpx;
font-size: 30rpx;
background-color: #06f;
color: #fff;
border-radius: 8rpx;
margin-top: 16rpx;
position: relative;
.icon-iamge{
width: 25rpx;
height: 25rpx;
position: absolute;
right: 20rpx;
top: 50%;
margin-top: -12rpx;
}
}
}
}
}
</style>