Merge branch 'dev-20250930' of http://25.13.9.101:9000/K17_AITS/tra-app into dev-20250930

This commit is contained in:
田岩
2025-08-26 17:35:12 +08:00
6 changed files with 1732 additions and 5 deletions
@@ -0,0 +1,368 @@
<template>
<view class="question-info">
<view class="qns-type">问答题</view>
<view class="question_text">
{{ dataInfo.qnsContent }}
</view>
<view class="qns-desc">你的答案</view>
<view class="ans-list">
<esQuestionAnswer v-for="item in dataInfo.analyContentVos" :dataInfo="item" :intrctWay="item.ossKey?'02':'01'"/>
</view>
<view class="analysis_div">
<view class="analysis_title">
<view class="success_answer">正确答案{{ dataInfo.itemAnswer }}</view>
</view>
<view class="analysis_note">题目解析{{ dataInfo?.analyContent }}</view>
</view>
</view>
</template>
<script setup>
import {
computed,
unref,
ref,
toRefs,
watch,
onMounted
} from 'vue';
import {
onUnload
} from '@dcloudio/uni-app';
import esQuestionAnswer from './esQuestionAnswer.vue'
const props = defineProps({
dataInfo: {
default: () => ({
talkingText: '',
talkingVoice: ''
}),
type: Object
}
});
const {
dataInfo
} = toRefs(props);
const intrctWay = computed(() => {
console.log('props.intrctWay', props.intrctWay);
return props.intrctWay;
});
const emit = defineEmits(['changePlay', 'buutton_click']);
watch(
() => props.dataInfo,
() => {}, {
deep: true,
immediate: true
}
);
const initData = (item) => {
return {
}
}
// 声音播放初始化
const talkVoiceObj = ref(null);
const isPlaying = ref(false);
const voiceLoading = ref(false);
const translateLoading = ref(false);
const voiceTime = ref('');
const itemVoice = ref('');
// 中间显示的文字
const showContent = computed(() => {
return props.text || props.dataInfo.anserResult || '';
});
onMounted(() => {
initVoice();
});
onUnload(() => {
pauseVoice();
});
watch(
() => props.dataInfo.talkingVoice,
(val) => {
if (val) {
initVoice();
}
}, {
deep: true,
immediate: true
}
);
// 按钮点击
const buutton_click = (type) => {
emit('buutton_click', type, props.dataInfo);
};
const pauseVoice = () => {
if (!talkVoiceObj.value) return;
talkVoiceObj.value.pause();
isPlaying.value = false;
emit('changePlay', '');
};
const playVoice = () => {
emit('changePlay', unref(dataInfo)?.id);
if (talkVoiceObj.value && talkVoiceObj.value.src) {
talkVoiceObj.value.play();
return;
} else {
talkVoiceObj.value.src = '';
if (unref(dataInfo).talkingVoice) {
setTimeout(() => {
talkVoiceObj.value.src = unref(dataInfo).talkingVoice;
talkVoiceObj.value.play();
isPlaying.value = true;
}, 100);
}
}
};
const initVoice = () => {
talkVoiceObj.value = uni.createInnerAudioContext();
talkVoiceObj.value.onEnded(() => {
emit('changePlay', '');
talkVoiceObj.value.src = '';
});
talkVoiceObj.value.onError(() => {
talkVoiceObj.value.src = '';
});
if (unref(dataInfo).intrctWay === '02') {
itemVoice.value = unref(dataInfo).talkingVoice;
talkVoiceObj.value.src = itemVoice.value;
setTimeout(() => {
setVoiceTime(0);
}, 100);
}
};
const setVoiceTime = (sum) => {
sum += 1;
if (Math.ceil(talkVoiceObj.value.duration) > 0) {
voiceTime.value = (Math.ceil(talkVoiceObj.value.duration) || 1) + 's';
} else {
if (sum === 20) {
voiceTime.value = 0 + 's';
return;
}
setTimeout(() => {
setVoiceTime(sum);
}, 200);
}
};
</script>
<style scoped lang="scss">
.qns-type {
width: 108rpx;
height: 46rpx;
background: #267eff;
border-radius: 8rpx;
font-family: PingFangSC;
font-weight: 500;
font-size: 25rpx;
color: #ffffff;
text-align: center;
line-height: 46rpx;
}
.question_text {
font-family: PingFangSC;
font-weight: 500;
font-size: 30rpx;
color: #333333;
line-height: 48rpx;
text-align: left;
font-style: normal;
margin-top: 20rpx;
}
.qns-desc{
font-size: 30rpx;
margin-top: 20rpx;
margin-bottom: 20rpx;
}
.talking-info {
padding: 20rpx;
border-radius: 15rpx;
background-color: #06f;
padding-bottom: 30rpx;
position: relative;
color: #fff;
width: 100%;
float: right;
.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;
}
}
.right-time {
position: absolute;
right: 20rpx;
top: 20rpx;
font-size: 28rpx;
height: 38rpx;
line-height: 38rpx;
}
&.talking-info-loading {
padding-bottom: 20rpx;
}
&.talking-info-text {
padding-bottom: 20rpx;
width: auto;
min-width: calc(100vw * 0.45);
float: right;
.talking-cont {
margin-top: 0rpx;
}
}
.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 #1f79ff;
display: flex;
justify-content: space-between;
padding-top: 20rpx;
margin-top: 22rpx;
.replay-btn {
float: left;
width: 46rpx;
height: 46rpx;
overflow: hidden;
margin-right: 16rpx;
margin-top: 25rpx;
}
.play-btn {
width: 46rpx;
height: 46rpx;
margin-right: 16rpx;
}
.text-btn {
width: 46rpx;
height: 46rpx;
overflow: hidden;
line-height: 46rpx;
text-align: center;
color: #fff;
margin-right: 16rpx;
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;
}
}
.btns-cont-button-text {
font-weight: 400;
font-size: 30rpx;
color: #ffffff;
line-height: 30rpx;
text-align: left;
font-style: normal;
margin-left: 10rpx;
}
}
}
.question-info{
margin-bottom: 20rpx;
overflow: hidden;
}
.ans-list{
overflow: hidden;
}
.analysis_div {
width: 100%;
.analysis_title {
display: flex;
justify-content: space-between;
align-items: center;
margin: 12rpx 0 28rpx 0;
.success_answer {
font-size: 30rpx;
}
.your_answer {
.success {
color: #15b859;
}
.error {
color: #f5212d;
}
}
}
.analysis_note {
font-family: PingFangSC;
font-weight: 500;
font-size: 30rpx;
color: #666666;
line-height: 48rpx;
text-align: left;
font-style: normal;
}
}
</style>
@@ -0,0 +1,333 @@
<template>
<view class="talking-info-es" v-if="intrctWay === '02'">
<view :class="['talking-gif', 'is-play']">
<CommonLoading color="#fff" v-show="voiceLoading" />
</view>
<view class="right-time">{{ voiceTime }}</view>
<view class="talking-cont">
<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">
<view class="play-btn">
<image class="icon" @click="playVoice" src="@/static/images/course/play-blue.png"
style="width: 100%; height: 100%"></image>
</view>
<view class="text-btn" @click.stop="translateVoice()"></view>
</view>
</view>
<!-- 01纯文字 -->
<view class="talking-info-es talking-info-text" v-else-if="intrctWay === '01'">
<view class="talking-cont">
<text :class="['talking-text translate-text loading']">
{{ showContent }}
</text>
</view>
</view>
<view class="talking-info talking-info-loading" v-else-if="intrctWay === '00'">
<view :class="['talking-gif']">
<CommonLoading color="#fff" />
</view>
</view>
</template>
<script setup>
import {
computed,
unref,
ref,
toRefs,
watch,
onMounted
} from 'vue';
import {
onUnload
} from '@dcloudio/uni-app';
const props = defineProps({
dataInfo: {
default: () => ({
ossAddr: ''
}),
type: Object
},
text: {
// 当前激活的语音
default: '',
type: String
},
voice: {
// 当前激活的语音
default: '',
type: String
},
activeVoiceId: {
// 当前激活的语音
default: '',
type: String
},
intrctWay: {
default: '',
type: String
}
});
const {
dataInfo
} = toRefs(props);
const intrctWay = computed(() => {
console.log('props.intrctWay', props.intrctWay);
return props.intrctWay;
});
const emit = defineEmits(['changePlay', 'buutton_click']);
watch(
() => props.dataInfo,
() => {}, {
deep: true,
immediate: true
}
);
// 声音播放初始化
const talkVoiceObj = ref(null);
const isPlaying = ref(false);
const voiceLoading = ref(false);
const translateLoading = ref(false);
const voiceTime = ref('');
const itemVoice = ref('');
// 中间显示的文字
const showContent = computed(() => {
return props.text || props.dataInfo.anserResult || '';
});
onMounted(() => {
initVoice();
});
onUnload(() => {
pauseVoice();
});
watch(
() => props.dataInfo.ossAddr,
(val) => {
if (val) {
initVoice();
}
}, {
deep: true,
immediate: true
}
);
// 按钮点击
const buutton_click = (type) => {
emit('buutton_click', type, props.dataInfo);
};
const pauseVoice = () => {
if (!talkVoiceObj.value) return;
talkVoiceObj.value.pause();
isPlaying.value = false;
emit('changePlay', '');
};
const playVoice = () => {
emit('changePlay', unref(dataInfo)?.id);
if (talkVoiceObj.value && talkVoiceObj.value.src) {
talkVoiceObj.value.play();
return;
} else {
talkVoiceObj.value.src = '';
if (unref(dataInfo).ossAddr) {
setTimeout(() => {
talkVoiceObj.value.src = unref(dataInfo).ossAddr;
talkVoiceObj.value.play();
isPlaying.value = true;
}, 100);
}
}
};
const initVoice = () => {
talkVoiceObj.value = uni.createInnerAudioContext();
talkVoiceObj.value.onEnded(() => {
emit('changePlay', '');
talkVoiceObj.value.src = '';
});
talkVoiceObj.value.onError(() => {
talkVoiceObj.value.src = '';
});
if (unref(dataInfo).intrctWay === '02') {
itemVoice.value = unref(dataInfo).ossAddr;
talkVoiceObj.value.src = itemVoice.value;
setTimeout(() => {
setVoiceTime(0);
}, 100);
}
};
const setVoiceTime = (sum) => {
sum += 1;
if (Math.ceil(talkVoiceObj.value.duration) > 0) {
voiceTime.value = (Math.ceil(talkVoiceObj.value.duration) || 1) + 's';
} else {
if (sum === 20) {
voiceTime.value = 0 + 's';
return;
}
setTimeout(() => {
setVoiceTime(sum);
}, 200);
}
};
</script>
<style scoped lang="scss">
.talking-info-es {
padding: 20rpx;
border-radius: 15rpx;
background-color: #06f;
padding-bottom: 30rpx;
position: relative;
color: #fff;
width: 100%;
float: left;
margin-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-white.gif) no-repeat;
background-size: contain;
}
}
.right-time {
position: absolute;
right: 20rpx;
top: 20rpx;
font-size: 28rpx;
height: 38rpx;
line-height: 38rpx;
}
&.talking-info-loading {
padding-bottom: 20rpx;
}
&.talking-info-text {
padding-bottom: 20rpx;
width: auto;
min-width: calc(100vw * 0.45);
float: left;
.talking-cont {
margin-top: 0rpx;
}
}
.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 #1f79ff;
display: flex;
justify-content: space-between;
padding-top: 20rpx;
margin-top: 22rpx;
.replay-btn {
float: left;
width: 46rpx;
height: 46rpx;
overflow: hidden;
margin-right: 16rpx;
margin-top: 25rpx;
}
.play-btn {
width: 46rpx;
height: 46rpx;
margin-right: 16rpx;
}
.text-btn {
width: 46rpx;
height: 46rpx;
overflow: hidden;
line-height: 46rpx;
text-align: center;
color: #fff;
margin-right: 16rpx;
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;
}
}
.btns-cont-button-text {
font-weight: 400;
font-size: 30rpx;
color: #ffffff;
line-height: 30rpx;
text-align: left;
font-style: normal;
margin-left: 10rpx;
}
}
}
</style>
@@ -0,0 +1,862 @@
<!-- type:
0纯文本信息不翻译
1语音回答
2 文本回答
3 段落信息
4 问题信息
5 统计信息
6 发送loading状态
7 教师反馈文本不翻译
8 问题反馈信息
9 已学完当前知识点反馈信息
-->
<template>
<view :class="['ai-answer', props.class]" :id="props.id" v-if="[3,4,5,8,9].indexOf(type) >= 0">
<!-- 对话信息 -->
<view :class="['talking-info', [4].indexOf(type) >= 0 ? 'talking-info-less':''] ">
<!-- 语音加载状态 -->
<view :class="['talking-gif', !voiceLoading && (activeVoiceId === dataInfo.id)?'is-play':'']" v-if="!([3, 4, 5, 8, 9].indexOf(type) >= 0)">
<CommonLoading color="#06f" v-show="voiceLoading"/>
</view>
<view class="talking-cont">
<!-- 题干/段落内容 -->
<view class="talking-num"> {{dataInfo.titleNum||''}} </view>
<text class="talking-text" v-if="[3, 5, 6, 9].indexOf(type) >= 0 && !dataInfo.isLoading">
{{ showContent }}
</text>
<text class="talking-text" v-if="[4].indexOf(type) >= 0 && dataInfo.qnsTyp === 'ES'">
<!-- 请您思考并作答:<br> -->
{{ showContent }}
</text>
<!-- 试题问题 -->
<Subject v-if="[4, 8].indexOf(type) >= 0 && dataInfo.qnsTyp !== 'ES'"
:isPreview="!isLast"
:item="subjectInfo"
:mode="mode"
:clearResult="type === 4"
@subject_click="subject_click" />
<esQuestion
v-if="[4, 8].indexOf(type) >= 0 && dataInfo.qnsTyp === 'ES'"
:dataInfo="esSubjectInfo"
/>
<!-- 试题问题反馈 -->
<!-- <RadioSubjectRequest v-if="[8].indexOf(type) >= 0 && dataInfo.qnsTyp !== 'ES'" :item="dataInfo" :mode="mode" @subject_click="subject_click" /> -->
<!-- 段落图片视频预览 -->
<view v-if="type === 3" style="overflow: hidden;">
<ImagePreview class="cont-img-box" v-if="(dataInfo.imageAddrs||[]).length>0" :imgId="dataInfo.imageAddrs[0]" :previewDetail="true"></ImagePreview>
<VideoPreview class="cont-img-box" v-if="(dataInfo.vidoAddrs||[]).length>0" :fileId="dataInfo.vidoAddrs[0]"></VideoPreview>
</view>
<!-- 维度信息预览 -->
<view v-if="[4, 8].indexOf(type) >= 0 && dataInfo.qnsTyp === 'ES'" style="padding-top: 16rpx;">
<ChartFour v-if="esSubjectInfo?.evalSettingVos?.length === 4" :dataList="esSubjectInfo.evalSettingVos" :score="intrctGrade"/>
<ChartThree v-if="esSubjectInfo?.evalSettingVos?.length === 3" :dataList="esSubjectInfo.evalSettingVos" :score="intrctGrade"/>
<ChartFive v-if="esSubjectInfo?.evalSettingVos?.length === 5" :dataList="esSubjectInfo.evalSettingVos" :score="intrctGrade"/>
</view>
<!-- 参考答案 -->
<view class="talking-text" v-if="type === 5 && !dataInfo.isLoading" style="overflow: hidden;">
参考答案{{ dataInfo.itemAnswer }}
</view>
<!-- 举例预览 -->
<view class="talking-text" v-if="type === 3 && dataInfo.knoExampie " style="overflow: hidden;">
举例{{ dataInfo.knoExampie }}
</view>
</view>
<view class="btns-cont" v-show="!dataInfo.isLoading" v-if="!([3, 8, 4].indexOf(type) >= 0)">
<!-- 重新播放 -->
<view class="replay-btn" @click.stop="restartPlay" v-if="!([5, 8, 9].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, 8, 9].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>
</view>
</view>
<!-- ai回答 -->
<view :class="['ai-answer', props.class]" :id="props.id" v-if="[7].indexOf(type) >= 0">
<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="talking-info">
<view class="right-time">{{voiceTime}}</view>
<view class="talking-cont">
<text :class="['talking-text translate-text loading']" >
{{ showContent }}
</text>
</view>
<view class="btns-cont">
<!-- <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-show="isLast">完成</view>
<view
class="icon text-right-btn"
@click.stop="withdraw"
v-show="showWithdraw"
>撤回
<!-- 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="talking-info-less">
<text class="talking-text" v-if="type === 0">
{{ showContent }}
</text>
</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, getAudioByText } from "@/api/common.js"
import { studyParagraphOverApi, getAskAboutRawApi } from "@/api/study.js"
import ChartFour from "@/pages/course/components/chartFour.vue"
import ChartThree from "@/pages/course/components/chartThree.vue"
import ChartFive from '@/pages/course/components/chartFive.vue'
import common from '@/common/common';
import { useStorageStore } from "@/store/storage.js"
import { onUnload } from '@dcloudio/uni-app'
import esQuestion from './esQuestion.vue'
import Subject from '@/components/examination/subject.vue'
// import RadioSubjectRequest from '@/pages/wrong_question_record/components/radio-subject.vue'
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
},
teacherInfo: {
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, teacherInfo} = toRefs(props)
const emit = defineEmits(['changePlay', 'nextQuestion', 'withdraw', 'finishQuestion', 'answerQuestion'])
// 声音播放初始化
const talkVoiceObj = ref(uni.createInnerAudioContext())
talkVoiceObj.value.playbackRate = Number(props.voiceRate)
const itemVoice = ref('')
const subjectInfo = computed(()=>{
return {
...dataInfo.value,
...(dataInfo.value?.traQnsInfoVo||{}),
my_answer: dataInfo.value?.traQnsInfoVo?.anserResult||''
}
})
const esSubjectInfo = computed(()=>{
return {
...dataInfo.value,
...(dataInfo.value.itemVos && dataInfo.value.itemVos.length>0
?
dataInfo.value.itemVos[0]
:{})
}
})
const intrctGrade = computed(()=>{
let _sum = 0
let _arr = esSubjectInfo.value.evalSettingVos||[]
_arr.forEach(t=>{
_sum += t.anlyGrade
})
return _sum
})
const mode = ref('study') // 考试 examination 练习practice 学习study
const showWithdraw = computed(()=>{
let _isMe = unref(lastQuestionInfo).qstLogId === unref(dataInfo).qstLogId
let _isQuestion = unref(lastQuestionInfo).isQuestion
return _isMe && _isQuestion
})
const voiceLoading = ref(false)
watch(() => props.teacherInfo,(val) => {
itemVoice.value = ''
talkVoiceObj.value.src = ''
},{
deep: true,
immediate: true
})
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 showGoOnStudyBtn = computed(()=> {
if(!(showNextBtn.value && isLast.value)){
return false
}
if(type.value === 5){
return true
}
if(type.value === 8){
return !!dataInfo.value.traQnsInfoVo?.judgeResult
}
if(type.value === 9){
return dataInfo.value.stdyStat === 'Y'
}
return false
})
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)?.analyContent
else if(props.type === 9) return unref(dataInfo)?.chatContent || unref(dataInfo)?.analyContent
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")
}
const voiceTime = ref('')
const itemInfo = ref({})
onMounted(()=>{
initVoice()
})
onUnload(()=>{
pauseVoice()
})
talkVoiceObj.value.onEnded(()=>{
emit('changePlay', '')
talkVoiceObj.value.src = ''
})
talkVoiceObj.value.onError(()=>{
talkVoiceObj.value.src = ''
})
const pauseVoice = () => {
if(!talkVoiceObj.value) return
talkVoiceObj.value.pause()
emit('changePlay', '')
}
// 重播
const restartPlay = () => {
if(itemVoice.value){
emit('changePlay', '')
emit('changePlay', unref(dataInfo)?.id)
setTimeout(()=>{
talkVoiceObj.value.src = itemVoice.value
talkVoiceObj.value.seek(0)
talkVoiceObj.value.play()
},100)
}else{
playVoice()
}
}
const playVoice = (readType = 'pgth') =>{
let _content = showContent.value
if(!_content){
common.msg('没有可以播放的文字信息!')
return
}
emit('changePlay', unref(dataInfo)?.id)
// readType 阅读内容类型pgrh段落/qns问题
if(talkVoiceObj.value.src && talkVoiceObj.value.src === itemVoice.value) {
emit('changePlay', unref(dataInfo)?.id)
talkVoiceObj.value.play()
return
}
talkVoiceObj.value.src = ''
if(itemVoice.value){
setTimeout(()=>{
talkVoiceObj.value.src = itemVoice.value
talkVoiceObj.value.play()
},100)
}else{
voiceLoading.value = true
let _type = props.type == 2 ? 'ans' : props.type == 3 ? 'pgrh' : props.type == 4 ? 'qns' : ''
let _readId = props.type == 2 ? unref(dataInfo)?.intrctId : props.type == 3 ? unref(dataInfo)?.pgrphId : props.type == 4 ? unref(dataInfo)?.qnsId : ''
let _params = {
crsId: unref(dataInfo)?.crsId || '',
pgrphId: unref(dataInfo)?.pgrphId || '',
qnsId: unref(dataInfo)?.qnsId || '',
readType: _type,// 2 文本回答 3, 段落信息 4 问题信息
teachNo: unref(teacherInfo)?.teacherNo, // 必填
readId: _readId
// content: _content
}
let _url = '/trastudy/traStdyInfo/readContent?' + Object.keys(_params).map(t=>{
return encodeURIComponent(t) + '=' + encodeURIComponent(_params[t])
}).join('&')
storageStore.getStorageById('audio', _url + _params.teachNo, (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 + _params.teachNo, itemVoice.value)
}).catch((err)=>{
voiceLoading.value = false
})
}
})
}
}
const saveFile = (buffer,filePath, cb = ()=>{}) => {
let fileManager = uni.getFileSystemManager()
fileManager.writeFile({
filePath: filePath,
data: buffer,
encoding: 'binary',
success: cb,
file: ()=>{
voiceLoading.value = false
}
})
}
const initVoice = (readType = 'qns', item, isplay=false) => {
if(unref(dataInfo).talkingVoice){
itemVoice.value = unref(dataInfo).talkingVoice
talkVoiceObj.value.src = itemVoice.value
setTimeout(()=>{
setVoiceTime(0)
},100)
}
}
const setVoiceTime = (sum) => {
sum += 1
if(talkVoiceObj.value.duration){
voiceTime.value = ( Math.ceil(talkVoiceObj.value.duration) || 0 ) + 's'
}else{
if(sum === 10) {
voiceTime.value = 0 + 's'
return
}
setTimeout(()=>{
setVoiceTime(sum)
},200)
}
}
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 isClick = ref(false)
const goOnGetQues = () => {
if(isClick.value) return
clickNow()
if([5,8].indexOf(props.type) >= 0){
// 完成问题
emit('nextQuestion', unref(dataInfo))
}else if([3].indexOf(props.type) >= 0){
// 完成段落
emit('nextQuestion', unref(dataInfo), 'overGraph')
}else if([9].indexOf(props.type) >= 0){
// 下一段落
emit('nextQuestion', unref(dataInfo), 'nextGraph')
}
}
const clickNow = () => {
isClick.value = true
setTimeout(()=>{
isClick.value = false
},800)
}
const goOnRestart = () => {
if(isClick.value) return
clickNow()
emit('nextQuestion', unref(dataInfo), 'reStart')
}
const goOnRestartPrgh = () => {
if(isClick.value) return
clickNow()
emit('nextQuestion', unref(dataInfo), 'reStartPrgh')
}
// 撤回
const withdraw = () => {
if(isClick.value) return
clickNow()
emit('withdraw', unref(dataInfo))
}
// 完成
const finishQuestion = (data) => {
if(isClick.value) return
clickNow()
emit('finishQuestion',data)
}
// 答题 选择
const subject_click = (type, data) => {
if(['answer', 'choose'].includes(type)){
emit('answerQuestion', {
...data,
...unref(dataInfo)
})
}
}
</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: #f1f1f1;
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;
width: 100%;
&.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-num{
font-size: 30rpx;
margin-bottom: 20rpx;
}
.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: #fafafa;
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: #f1f1f1;
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;
position: relative;
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;
}
}
.right-time{
position: absolute;
right: 20rpx;
top: 20rpx;
font-size: 28rpx;
height: 38rpx;
line-height: 38rpx;
}
.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>
@@ -1,8 +1,173 @@
<template>
<view class="course-record-page">
<view class="top-bg"></view>
<view class="bot-bg"></view>
<view class="course-study-body">
<view class="body-title">
<uv-icon class="arrow-icon" name="arrow-left" @click="backRouter()" />
课程学习记录
</view>
<scroll-view class="talking-list" :scroll-y="true" :scroll-top="scrollTop" :show-scrollbar="false"
:scroll-with-animation="true">
<talkingItem v-for="item in talkingList" :type="item.type"
:dataInfo="item" :activeVoiceId="activeVoiceTalkingItemId" :isPlaying="activeVoiceTalkingItemId === item.id"
:voiceRate="chooseRate" />
</scroll-view>
</view>
</view>
</template>
<script>
<script setup>
import {
onLoad
} from '@dcloudio/uni-app';
import {
ref,
unref,
computed,
watch
} from 'vue'
import {
queryTraStudyExecuteRecord
} from '@/api/courseRecord.js'
import common from '@/common/common';
import talkingItem from './components/talkingItem.vue'
const stdyId = ref('')
const scrollTop = ref(0)
const showOrder = ref(1)
const pgrphNum = ref(1)
const activeVoiceTalkingItemId = ref('')
const chooseRate = ref('1.0')
onLoad((params) => {
stdyId.value = params.stdyId
getRecordInfo()
})
const talkingRecord = ref([])
const talkingList = computed(()=>{
let _arr = []
talkingRecord.value.forEach((t,index)=> {
_arr.push({
...t,
id: 'prgh-'+index,
chatContent:( t.pgrphContent || ''),
titleNum: '知识点'+(index+1)+'',
type: 3
})
if(t.qnsInfoVos && t.qnsInfoVos.length>0){
t.qnsInfoVos.forEach((k,indexk)=>{
if(k.qnsTyp === 'ES'){
_arr.push({
...k,
chatContent: (k.qnsContent || ''),
titleNum: '问题'+(indexk+1)+'',
type: 8,
id: 'qns-'+index+'-'+indexk,
})
}else{
_arr.push({
...k,
chatContent:(k.qnsContent || ''),
titleNum: '问题'+(indexk+1)+'',
type: 8,
id: 'qns-'+index+'-'+indexk,
isQuestion: true,
my_answer: [],
backend_answer: [],
})
}
})
}
})
return _arr
})
const getRecordInfo = () => {
queryTraStudyExecuteRecord({
stdyId: stdyId.value
}).then(res => {
talkingRecord.value = res.body
})
}
const backRouter = () => {
common.navigateBack()
}
</script>
<style>
<style scoped lang="scss">
.course-record-page {
position: relative;
display: flex;
flex-direction: column;
height: 100vh;
padding-top: var(--status-bar-height);
.top-bg {
background: linear-gradient(16deg, rgba(234, 246, 255, 0) 0%, #e6eafd 100%);
height: 480rpx;
width: 750rpx;
}
.bot-bg {
background: linear-gradient(136deg, rgba(234, 246, 255, 0) 0%, #e6eafd 100%);
width: 750rpx;
flex: 1;
}
.course-study-body {
position: absolute;
overflow: hidden;
left: 0;
top: 0;
height: 100%;
width: 100%;
padding: 0 0rpx;
box-sizing: border-box;
display: flex;
flex-direction: column;
padding-top: v-bind(pageTopPadding);
.body-title {
height: 90rpx;
text-align: center;
font-size: 33rpx;
padding-top: 10rpx;
line-height: 46rpx;
box-sizing: border-box;
font-weight: 500;
color: #000;
position: relative;
.arrow-icon {
position: absolute;
left: 21rpx;
top: 5rpx;
color: #000;
padding: 15rpx;
}
}
.talking-list {
flex: 1;
overflow-y: auto;
padding: 10rpx 30rpx;
background: url(@/static/images/course/study-bg.png) no-repeat;
background-size: cover;
background-position: center bottom;
box-sizing: border-box;
width: 100%;
.talking-list-cont {
padding: 0;
}
.talking-item {
margin-bottom: 46rpx;
}
}
}
}
</style>