Files
tra-app/src/pages/course/study.vue
T
2025-06-30 14:54:39 +08:00

659 lines
16 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.
<template>
<view class="course-study max_page">
<view class="top-bg"></view>
<view class="bot-bg"></view>
<view class="course-study-body" v-show="step === 1">
<view class="body-title">
<uv-icon class="arrow-icon" name="arrow-left" @click="common.navigateBack()"/>
语言选择开启学习之旅
</view>
<view class="voice-list">
<uv-radio-group v-model="choiceTeacher">
<view class="voice-item" v-for="item in teacherList" :key="item.teacherNo" @click="choiceTeacher = item.teacherNo">
<view class="avatar-box">
<ImagePreview class="img-box" :imgId="item.imgAddr" :isCache="true" :width="120" :height="160"></ImagePreview>
</view>
<view class="right-cont">
<view class="right-name">{{item.teacherName}}</view>
<view class="right-desc">描述{{item.voiceDesc || '--'}}</view>
<view class="audio-box">
<view :class="['talking-gif', activeVoiceTeachNo === item.teacherNo ? 'is-play':'']"></view>
<view class="play-btn" @click="playTeacherVoice(item)">
<image class="icon" v-if="activeVoiceTeachNo !== item.teacherNo" src="@/static/images/course/play-blue.png" style="width: 100%;height: 100%;"></image>
<image class="icon" v-else src="@/static/images/course/stop-blue.png" style="width: 100%;height: 100%;"></image>
</view>
</view>
</view>
<uv-radio :name="item.teacherNo" class="radio-box" @click.stop/>
</view>
</uv-radio-group>
<view class="start-btns" @click="nextStep">
开启学习之旅
</view>
</view>
</view>
<view class="course-study-body" v-show="step === 2" @click="playRecord">
<view class="body-title">
<uv-icon class="arrow-icon" name="arrow-left" @click="common.navigateBack()"/>
课程学习
</view>
<view class="talking-list">
<view class="study-num">
<view class="color-blue">1</view> / 20
</view>
<TalkingItem
class="talking-item"
v-for="item in talkingList"
:type="item.type"
:dataInfo="item"
:activeVoiceId="activeVoiceTalkingItemId"
@changePlay="changePlayItemId"
@nextQuestion="nextQuestionSuccess"
:id="'item' + item.id"
></TalkingItem>
<!-- <video v-if="videoUrlShow" :src="videoUrlShow" controls></video> -->
</view>
<view class="talk-btns">
<view
class="touch-btn"
@touchstart="showOverlayTalking"
@touchend="hideOverlayTalking"
@touchmove="handleMove"
>按住说话
<image class="talk-btn-icon" src="/src/static/images/course/jianpan.png" mode=""></image>
</view>
</view>
</view>
<uv-overlay :show="showTalkingModel" opacity=".9" @click="showTalkingModel = false">
<view class="overlay-box">
<view class="circle-bg-box">
<view class="icon-box">
<image src="/src/static/images/course/talk-icon.png" mode=""></image>
</view>
</view>
<view class="tips-box">松开发送</view>
<view class="btns-box">
<view :class="['close-btn',isOut?'is-out':'']" >取消
<image class="btn-box" v-if="!isOut" src="/src/static/images/course/close.png" mode=""></image>
<image class="btn-box" v-else src="/src/static/images/course/close-active.png" mode=""></image>
</view>
</view>
<!-- <view class="talk-dialog">
<view class="voice-gif"></view>
</view> -->
</view>
</uv-overlay>
</view>
</template>
<script setup>
import { reactive, ref, computed, unref, onMounted, onUnmounted } from 'vue';
import { onLoad,onShow,onHide } from '@dcloudio/uni-app';
import { previewFile, downloadFile, downloadVoiceFile, uploadVoiceFile } from "@/api/common.js"
import { getToken } from '@/common/common.js'
import { get_base_url } from '@/api/request'
import { getCourseStudyInfoApi, queryTraStdyBatchWaitParagraphInfoApi,audioTranscriptionsApi } from "@/api/study.js"
import common from '@/common/common';
import TalkingItem from '@/pages/course/components/talkingItem.vue'
import ImagePreview from '@/components/image-preview/image-preview.vue'
import { useSocketStore } from "@/store/socket.js"
import { storeToRefs } from 'pinia'
const socketStore = useSocketStore()
const { SocketObj } = storeToRefs(socketStore)
const crsNum = ref('');
const crsId = ref('');
const step = ref(0)
const teacherList = ref([])
const choiceTeacher = ref('')
const videoUrlShow = ref('')
const talkingList = ref([])
const activeVoiceTalkingItemId = ref('')
const getData = async () => {
try{
const { body } = await getCourseStudyInfoApi({crsId: crsId.value})
initsocketTask(1)
if(body.stdyProgressFlag === 'N'){
step.value = 1
teacherList.value = body.teacheList.map(t=>({
...t,
voicePath: '',
voiceObj: null
}))
}else{
common.show('提示信息', '此门课程已有学习历史,请选择是否继续学习?',true, '重新开始','继续学习').then((res) => {
console.log(res)
if(res) {
// 继续学习 获取学习历史接口
// 滚动到最下面
scrollToBottomNow()
choiceTeacher.value = body.choiceTeacher
sendMessage({
"commond" : 'OPEN',
"body" : {
crsId: crsId.value,
teacherNo: choiceTeacher.value
}
})
step.value = 2
getGraphInfoUnStudy()
}else{
// 重新开始
step.value = 1
teacherList.value = body.teacheList.map(t=>({
...t,
voicePath: '',
voiceObj: null
}))
}
}).finally(() => {})
}
}catch{
}
// downloadFile('S3F0250640161332025061618052000000267276').then(res=>{
// videoUrlShow.value = res.tempFilePath
// })
};
// 关闭播放状态
const changePlayItemId = (id) => {
if(activeVoiceTalkingItemId.value === id){
activeVoiceTalkingItemId.value = ''
}else{
activeVoiceTalkingItemId.value = id
}
}
const choiceTeacherInfo = computed(()=>{
return teacherList.value.find(t=> t.teacherNo === choiceTeacher.value)
})
const activeVoiceTeachNo = ref('')
const teacherVoiceObj = uni.createInnerAudioContext()
teacherVoiceObj.onEnded(()=>{
activeVoiceTeachNo.value = ''
teacherVoiceObj.src = ''
})
const playTeacherVoice = (item) =>{
teacherVoiceObj.stop()
teacherVoiceObj.src = ''
let _isStop = item.teacherNo === activeVoiceTeachNo.value
activeVoiceTeachNo.value = _isStop ? '' : item.teacherNo
if(_isStop) return
if(item.voicePath){
teacherVoiceObj.src = item.voicePath
teacherVoiceObj.play()
}else{
downloadVoiceFile('/trastudy/traStdyInfo/trialListening?teachNo='+item.teacherNo).then(res=>{
item.voicePath = res.tempFilePath
teacherVoiceObj.src = item.voicePath
teacherVoiceObj.play()
})
}
}
var socketTask = null
const nextStep = () => {
if(choiceTeacher.value){
teacherVoiceObj.destroy()
sendMessage({
"commond" : 'OPEN',
"body" : {
crsId: crsId.value,
teacherNo: choiceTeacher.value
}
})
step.value = 2
getGraphInfoUnStudy()
}else{
uni.showToast({
title: '请选择一位教师!',
icon: "none",
duration:1000
});
}
}
const getGraphInfoUnStudy = () => {
queryTraStdyBatchWaitParagraphInfoApi({crsId: crsId.value}).then(res=>{
console.log(res)
let _id = new Date().getTime() + 'id'
talkingList.value.push({
...res.body,
type: 0,
userInfo: {...choiceTeacherInfo.value},
id: _id
})
scrollToBottomNow()
})
}
// 继续 执行方法
const nextQuestionSuccess = (info) => {
let _id = new Date().getTime() + 'id'
talkingList.value.push({
crsId: info.crsId,
pgrphId: info.pgrphId,
talkingContent: '继续',
type: 2,
id: _id
})
scrollToBottomNow()
getGraphInfoUnStudy()
}
const scrollToBottomNow = () => {
if(talkingList.value.length > 0){
let _last = talkingList.value[talkingList.value.length - 1]
uni.pageScrollTo({
selector: '#item' + _last.id,
duration: 300
})
}
}
const reconnectTimer = null // 重连timer
const isManualClose = ref(false) // 是否手动关闭
const initsocketTask = ($step) => {
socketStore.creatSocket()
// socketTask = uni.connectSocket({
// url: get_base_url() + '/trastudy/trasoket/open?summary=' + getToken(),
// complete: function(){}
// })
// uni.onSocketMessage((res)=>{
// let _data = JSON.parse(res.data)
// console.log(_data)
// })
// uni.onSocketOpen((e)=>{
// console.log('链接成功!')
// })
// uni.onSocketError((e)=>{
// console.log(e,111)
// })
// uni.onSocketClose((e)=>{
// // 重连
// reconnect()
// })
}
const reconnect = ()=> {
if(reconnectTimer || isManualClose.value) return
reconnectTimer = setTimeout(()=>{
initsocketTask(2)
},3000)
}
const sendMessage = (data) => {
unref(SocketObj).send(data)
}
const closeWebSocket = () => {
isManualClose.value = true
if(socketTask){
// socketTask.close()
socketTask = null
uni.closeSocket({})
}
}
onLoad((params) => {
crsId.value = params.crsId||'';
crsNum.value = params.crsNum||'';
});
const showTalkingModel = ref(false)
// 录音
const audioPath = ref('')
const audioObj = uni.createInnerAudioContext()
//#ifdef APP-PLUS
// app端
const recordObj = uni.getRecorderManager()
const initRecord = () => {
unref(recordObj)?.onStop((res)=>{
audioPath.value = res.tempFilePath
})
}
const startRecord = () => {
unref(recordObj).start({format:'mp3'})
}
const stopRecord = () => {
unref(recordObj).stop()
}
// #endif
//#ifndef APP-PLUS
const recordObj = {}
const initRecord = () => {}
// #endif
onMounted(()=>{
// getData();
initRecord?.()
})
onShow(()=>{
getData();
})
onHide(()=>{
closeWebSocket()
teacherVoiceObj.stop()
teacherVoiceObj.src = ''
})
// onUnmounted(()=>{
// closeWebSocket()
// })
const playRecord = () => {
audioObj.src = audioPath.value
unref(audioObj).play()
}
// 录音结束
// 记录 按下移动位置
const touchX = ref(0)
const touchY = ref(0)
const baseY = ref(0)
const systemInfo = uni.getSystemInfoSync()
const widowHeight = systemInfo.windowHeight
// 计算是否超出按下盒子位置
const isOut = computed(() => {
let moveVal = (widowHeight - baseY.value)/140 * 234
return (widowHeight - moveVal) > touchY.value
})
const showOverlayTalking = (obj) => {
showTalkingModel.value = true
touchX.value = obj.changedTouches[0].pageX
touchY.value = obj.changedTouches[0].pageY
baseY.value = obj.currentTarget.offsetTop
//#ifdef APP-PLUS
startRecord()
// #endif
}
const hideOverlayTalking = (obj) => {
showTalkingModel.value = false
//#ifdef APP-PLUS
stopRecord()
// #endif
if(!isOut.value){
console.log('发送')
setTimeout(()=>{
uploadVoiceFile(audioPath.value,{}).then(res=>{
console.log(JSON.parse(res.data))
}).catch(err=>{
console.log(err)
})
},1000)
}
// else{
// console.log('取消发送')
// }
}
const handleMove = (obj) => {
touchX.value = obj.changedTouches[0].pageX
touchY.value = obj.changedTouches[0].pageY
}
</script>
<style lang="scss">
.course-study{
position: relative;
display: flex;
flex-direction: column;
.top-bg{
background: linear-gradient(16deg,rgba(234,246,255,0) 0%,#c3e6ff 100%);
height: 480rpx;
width: 750rpx;
}
.bot-bg{
// background-color: #F6F8FF;
background: linear-gradient(136deg,rgba(234,246,255,0) 0%,#c3e6ff 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;
.body-title{
height: 150rpx;
text-align: center;
font-size: 33rpx;
padding-top: 70rpx;
line-height: 46rpx;
box-sizing: border-box;
font-weight: 500;
color:#000;
position: relative;
// margin-bottom: 23rpx;
.arrow-icon{
position: absolute;
left:36rpx;
top:80rpx;
color: #000;
}
}
.talking-list{
flex:1;
overflow-y: auto;
padding: 30rpx;
background: url(@/static/images/course/study-bg.png) no-repeat;
background-size: cover;
border-radius: 30rpx 30rpx 0 0;
.study-num{
height: 36rpx;
line-height: 36rpx;
text-align: center;
margin-bottom: 15rpx;
.color-blue{
color: #06f;
display: inline;
}
}
.talking-item{
margin-bottom: 46rpx;
}
}
.talk-btns{
height: 150rpx;
background-color: #fff;
padding: 25rpx 30rpx 0;
.touch-btn{
height: 92rpx;
box-shadow: 0 8rpx 20rpx 0 rgba(0,0,0,.06);
text-align: center;
line-height: 92rpx;
color: #000;
font-weight: 400;
position: relative;
.talk-btn-icon{
width: 40rpx;
height: 40rpx;
position: absolute;
right: 40rpx;
top: 50%;
margin-top: -20rpx
}
}
}
.voice-list{
flex:1;
overflow-y: auto;
padding: 10rpx 40rpx 30rpx;
.voice-item{
min-height: 246rpx;
width: 100%;
overflow: hidden;
background-color: #fff;
border-radius: 16rpx;
display: flex;
padding: 25rpx;
position: relative;
&:not(:last-child){
margin-bottom: 30rpx;
}
.avatar-box{
width: 160rpx;
height: 207rpx;
margin-right: 20rpx;
border-radius: 16rpx;
overflow: hidden;
background-color: #ccc;
.img-box{
width: 100%;
height: 100%;
}
}
.right-cont{
flex:1;
.right-name{
width: 380rpx;
line-height: 40rpx;
font-size: 28rpx;
font-weight: 500;
margin-bottom: 25rpx;
word-break: break-all;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.right-desc{
width: 380rpx;
line-height: 38rpx;
font-size: 25rpx;
color: #999;
margin-bottom: 12rpx;
word-break: break-all;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.audio-box{
width: 90%;
height: 70rpx;
line-height: 70rpx;
background-color: #eaf2ff;
padding: 15rpx 25rpx;
overflow: hidden;
.talking-gif{
width: 140rpx;
height: 40rpx;
// background-color: #ccc;
background: url(@/static/images/course/voice-gray.png) no-repeat;
background-size: 100%;
box-sizing: border-box;
float: left;
&.is-play{
background: url(@/static/images/course/voice-gif.gif) no-repeat;
background-size: 100%;
}
}
.talking-time{
float: right;
text-align: right;
line-height: 40rpx;
font-size: 23rpx;
color: #b1bbd3;
}
.play-btn{
float:right;
width: 35rpx;
height: 40rpx;
padding-top:2rpx;
line-height: 35rpx;
margin-left: 20rpx;
}
}
}
.radio-box{
position: absolute;
right: 20rpx;
top: 20rpx;
}
}
.start-btns{
margin-top:20rpx;
height: 80rpx;
width: 100%;
background-color: #06f;
color:#fff;
text-align: center;
line-height: 80rpx;
}
}
}
}
.overlay-box{
display: flex;
flex-direction: column-reverse;
// justify-content: flex-end;
width: 100%;
height: 100%;
.circle-bg-box{
height: 234rpx;
background: url(@/static/images/course/talking.png) no-repeat;
background-size: cover;
position: relative;
.icon-box{
position: absolute;
left:50%;
top: 50%;
margin-left: -17rpx;
margin-top: -24rpx;
width: 34rpx;
height: 48rpx;
image{
width: 100%;
height: 100%;
}
}
}
.tips-box{
height: 40rpx;
line-height: 40rpx;
color:#c0c0c0;
text-align: center;
margin: 12rpx 0 24rpx;
}
.btns-box{
height: 270rpx;
// padding: 0 120rpx;
width: 154rpx;
margin: 0 auto 100rpx;
overflow: hidden;
font-size: 28rpx;
text-align: center;
line-height: 116rpx;
.close-btn{
float: left;
height: 100%;
width: 154rpx;
overflow: hidden;
color:#999;
&.is-out{
color:#fff;
}
}
.translate-btn{
float: right;
height: 100%;
width: 154rpx;
overflow: hidden;
color:#999;
}
.btn-box{
width: 100%;
height: 154rpx;
}
}
.talk-dialog{
width: 506rpx;
height: 234rpx;
margin: 0 auto;
background-color: #06f;
}
}
</style>