feat: 陪练预览,陪练记录,陪练报告,陪练对话等联调

This commit is contained in:
杨航
2025-11-14 17:36:42 +08:00
parent f74435023d
commit 8e50a7f418
23 changed files with 1935 additions and 218 deletions
+1 -1
View File
@@ -6,5 +6,5 @@ VITE_APP_BASE_API_Url = 'http://25.18.122.65:7001'
# VITE_APP_BASE_H5_API_Url = 'http://25.18.122.78:9786'
# VITE_APP_BASE_H5_API_Url_TRAEXAM = 'http://25.64.32.154:9604'
# VITE_APP_BASE_H5_API_Url_TRAPRACTICE = 'http://25.64.32.154:9603'
VITE_APP_BASE_H5_API_Url_TRAPRACTICE = 'http://25.64.32.156:9603'
+8
View File
@@ -10,3 +10,11 @@ export const queryTraStdyInfoPreviewPaging = (data) => {
data
});
};
export const queryTraListPreviewPaging = (data) => {
return request({
url: '/trapractice/traPartnerInfo/queryTraPartnerPreviewPaging',
method: 'post',
toastErrors: true,
data
});
};
+58
View File
@@ -0,0 +1,58 @@
import request from '@/api/request'
const base_url = '/trapractice'
/**练习*/
// 练习列表页
export const queryPracticeRecordPaging = (data) => {
return request({
url: base_url + '/traPartnerExecuteHis/queryTraPartnerExecutePaging',
method: 'post',
toastErrors: true,
data:{
traType:'01',
...data
}
});
};
// 练习每一项列表
export const queryPracticeHis = (data) => {
return request({
url: base_url + '/traPartnerExecuteHis/queryTraPartnerExecuteHisPaging',
method: 'post',
toastErrors: true,
data
});
};
// 练习详情 通过 exrId 查询
export const queryPracticeRecordByExrId = (data) => {
return request({
url: base_url + '/traCrsPractice/queryPracticeRecordByExrId',
method: 'post',
toastErrors: true,
data
});
};
/**考试*/
export const queryCrsExamRecordPaging = (data) => {
return request({
url: base_url + '/traPartnerExecuteHis/queryTraPartnerExecutePaging',
method: 'post',
toastErrors: true,
data:{
traType:'02',
...data
}
});
};
// 考试每一项列表
export const queryCrsExamHisByExamId = (data) => {
return request({
url: base_url + '/traPartnerExecuteHis/queryTraPartnerExecuteHisPaging',
method: 'post',
toastErrors: true,
data
});
};
+9
View File
@@ -510,6 +510,15 @@
"titleNView": false
}
}
},
{
"path": "pages/traRecord/index",
"style": {
"navigationBarTitleText": "陪练记录页",
"app-plus": {
"titleNView": false
}
}
}
],
"tabBar": {
@@ -119,7 +119,6 @@
dataY: dataY + '%'
});
}
console.log(vertices)
return vertices;
},
-1
View File
@@ -119,7 +119,6 @@
dataY: dataY + '%'
});
}
console.log(vertices)
return vertices;
},
+1 -1
View File
@@ -97,7 +97,7 @@
</view>
</view>
<view class="scroll-view-item">
<view class="item" @click="goTest()">
<view class="item" @click="common.navigateTo('/pages/traRecord/index')">
<image class="icon" src="@/static/images/me/report.png"></image>
<view class="title">AI陪练记录</view>
</view>
@@ -1,42 +1,67 @@
<template>
<view class="scroll_div">
<scroll-view class="scroll-Y" :show-scrollbar="false">
<view class="item">
<image src="/src/static/images/common/default_img_2.png" mode="aspectFit"></image>
</view>
</scroll-view>
</view>
<z-paging ref="pagingRef" v-model="data_list" @query="queryList" :auto="false" empty-view-text="暂无数据"
paging-class="pagingClass">
<view class="sparring-item-box" v-for="item in data_list">
<SparringItem :itemInfo="item" :activeTag="[]" @clickItem="toSparringDetail(item)" />
</view>
</z-paging>
</template>
<script setup>
import common from '@/common/common';
import {
ref,
reactive,
onMounted,
nextTick,
watch
} from 'vue';
import {
queryTraListPreviewPaging
} from '@/api/preview.js';
import useZpaging from '@/composables/useZpaging';
import SparringItem from '@/pages/sparring/components/sparring-item.vue'
const {
pagingRef,
queryList,
data_list,
reload,
total
} = useZpaging({
fetchFunction: queryTraListPreviewPaging
});
const props = defineProps(['tabIndex', 'currentIndex']);
const toSparringDetail = (item) => {
if (item.traId) {
common.navigateTo('/pages/sparring/detail?traId=' + item.traId + '&isPreview=1')
}
};
onMounted(() => {
watch(
() => props.currentIndex,
(v) => v === props.tabIndex && total.value === -1 && reload(), {
immediate: true
}
);
});
</script>
<style scoped lang="scss">
.scroll_div {
/* 圆形渐变,颜色顺序已反转(原1和3交换) */
background: radial-gradient(circle,
#C3B9FF 0%,
/* 原第三种颜色,现在作为起始颜色 */
#DFF3FF 46%,
/* 中间过渡色保持不变 */
#BFEBFF 100%
/* 原第一种颜色,现在作为结束颜色 */
);
/* 渐变中心点仍在左下角,向右侧和上方扩散 */
background-position: 0% 100%;
background-size: 200% 200%;
.pagingClass {
width: calc(100% - 44rpx);
margin: 22rpx auto;
background-color: #fff;
border-radius: 16rpx;
}
.scroll-Y {
height: calc(100vh - var(--status-bar-height) - 54rpx - 88rpx - 20rpx);
}
.scroll-Y .item {
// background-color: red;
display: flex;
flex-direction: column;
align-items: center;
margin-top: 300rpx;
.sparring-item-box {
width: 50%;
float: left;
margin-top: 24rpx;
margin-bottom: -24rpx;
// background-color: #f1f1f1;
// margin-bottom: 16rpx;
&:nth-child(2n-1) {
// margin-right: 16rpx;
}
}
</style>
+172 -40
View File
@@ -5,16 +5,19 @@
</view>
</view>
<view class="answer-content" v-else>
<view :class="['talking-gif', !voiceLoading&&(props.activeVoiceId === dataInfo.id) ? 'is-play' : '']">
<CommonLoading color="#06f" v-show="voiceLoading" />
</view>
<view class="talking-cont">
<MarkdownPreview :mdString="mdText" />
</view>
<view class="btns-cont" v-if="!props.dataInfo.isLoading">
<view class="replay-btn" @click="reAnswer" v-if="!props.dataInfo.isHistory">
<view class="btns-cont" v-if="!dataInfo.isLoading">
<!-- <view class="replay-btn" @click="reAnswer" v-if="!props.dataInfo.isHistory">
<image class="icon" src="@/static/images/dialog/answer-restart1.png" style="width: 100%; height: 100%"></image>
</view>
<view class="replay-btn" @click="stopAnswer" v-if="props.isAnswering && props.isLast">
<image class="icon" src="@/static/images/dialog/answer-stop1.png" style="width: 100%; height: 100%"></image>
</view>
</view> -->
<!-- 重新播放 -->
<view class="replay-btn" @click.stop="restartPlay">
<image class="icon" src="@/static/images/course/replay.png" style="width: 100%; height: 100%"></image>
@@ -39,9 +42,20 @@
watch,
toRefs
} from 'vue'
import { onUnload } from '@dcloudio/uni-app';
import {
onUnload
} from '@dcloudio/uni-app';
import MarkdownPreview from '@/components/markdown-preview/markdown-preview.vue'
import common from '@/common/common'
import {
useStorageStore
} from '@/store/storage.js';
import {
downloadFile,
downloadVoiceFile
} from '@/api/common.js'
const storageStore = useStorageStore();
const emit = defineEmits(['handleEvents', 'changePlay'])
const props = defineProps({
@@ -49,6 +63,10 @@
default: () => ({}),
type: Object
},
roleInfo: {
default: () => ({}),
type: Object
},
isAnswering: {
default: false,
type: Boolean
@@ -56,24 +74,31 @@
isLast: {
default: false,
type: Boolean
}
},
activeVoiceId: {
default: '',
type: String
},
isStudy: {
default: true,
type: Boolean
},
})
const { isLast, dataInfo } = toRefs(props)
watch(
() => props.dataInfo,
() => {}, {
deep: true,
immediate: true
}
)
const {
isLast,
dataInfo,
roleInfo,
activeVoiceId,
isStudy
} = toRefs(props)
const isPlaying = ref(false)
const voiceIsPlaying = ref(false)
const startPlay = ref(false)
const mdText = computed(() => {
let _text = (props.dataInfo?.talkingText || '').replace(/\\n/g, `\n`);
return _text
})
const fileList = computed(() => props.dataInfo?.talkingFiles || [])
const crsList = computed(() => props.dataInfo?.talkingCrsList || [])
const activeNames = ref([1, 2])
const changeActive = val => {
@@ -102,6 +127,22 @@
common.navigateTo('/pages/courseDetail/index?crsId=' + item.crsId)
}
const talkVoiceObj = ref(null)
const voiceLoading = ref(false)
const itemVoice = ref('')
// 重播
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 = mdText.value;
if (!_content) {
@@ -109,41 +150,91 @@
return;
}
isPlaying.value = true
voiceLoading.value = true;
setTimeout(() => {
downloadFile(props.dataInfo?.talkingVoiceId)
.then((res) => {
if (res?.data) {
common.msg(res.data.message);
return;
}
voiceLoading.value = false;
itemVoice.value = res.tempFilePath;
talkVoiceObj.value.src = res.tempFilePath;
emit('changePlay', unref(dataInfo)?.id);
// readType 阅读内容类型pgrh段落/qns问题
if (talkVoiceObj.value.src && talkVoiceObj.value.src === itemVoice.value) {
if (voiceIsPlaying.value) return
talkVoiceObj.value.play();
voiceLoading.value = false;
return;
}
talkVoiceObj.value.src = '';
if (itemVoice.value) {
setTimeout(() => {
talkVoiceObj.value.src = itemVoice.value;
if (voiceIsPlaying.value) return
talkVoiceObj.value.play();
})
.catch((err) => {
voiceLoading.value = false;
}, 100);
} else {
voiceLoading.value = true;
let _params = {
chaId: unref(roleInfo)?.chaId || '',
execLogId: unref(dataInfo)?.execLogId || '',
};
let _url =
'/trapractice/traPartnerChat/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;
if (voiceIsPlaying.value) return
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;
if (voiceIsPlaying.value) return
talkVoiceObj.value.play();
storageStore.setStorage('audio', _url + _params.teachNo, itemVoice.value);
})
.catch((err) => {
voiceLoading.value = false;
emit('changePlay', '');
});
}
});
}
}, 100);
};
const initVoice = () => {
talkVoiceObj.value = uni.createInnerAudioContext()
talkVoiceObj.value.onEnded(()=>{
talkVoiceObj.value.onEnded(() => {
emit('changePlay', '')
talkVoiceObj.value.src = ''
voiceIsPlaying.value = false
})
talkVoiceObj.value.onError(()=>{
talkVoiceObj.value.onPause(() => {
voiceIsPlaying.value = false
});
talkVoiceObj.value.onPlay(() => {
console.log('onPlay');
startPlay.value = true
voiceIsPlaying.value = true
setTimeout(() => {
startPlay.value = false
}, 300)
emit('changePlay', unref(dataInfo)?.id);
});
talkVoiceObj.value.onError(() => {
talkVoiceObj.value.src = ''
voiceIsPlaying.value = false
})
if(unref(dataInfo).intrctWay === '02'){
itemVoice.value = unref(dataInfo).talkingVoice
talkVoiceObj.value.src = itemVoice.value
setTimeout(()=>{
setVoiceTime(0)
},100)
}
}
const pauseVoice = () => {
if (!talkVoiceObj.value) return
@@ -158,18 +249,45 @@
pauseVoice()
})
watch(() => props.dataInfo.talkingVoice, (val) => {
if (val) {
if (val && isStudy.value) {
initVoice()
}
}, {
deep: true,
immediate: true
})
watch(
() => props.dataInfo,
(val) => {
if (val.execLogId && isStudy.value) {
playVoice()
}
}, {
deep: true,
immediate: true
}
)
watch(
() => props.activeVoiceId,
(val) => {
if (val !== props.dataInfo.id) {
if (isPlaying.value) {
isPlaying.value = false
if (!talkVoiceObj.value) return
talkVoiceObj.value.pause()
}
}
}, {
deep: true,
immediate: true
}
)
</script>
<style scoped lang="scss">
.answer-content {
background-color: #f5f5f5;
background-color: #fff;
border: 2rpx solid #efefef;
padding: 20rpx;
border-radius: 15rpx;
padding-bottom: 10rpx;
@@ -179,6 +297,20 @@
width: 100%;
min-height: 80rpx;
.talking-gif {
width: 200rpx;
height: 38rpx;
margin-bottom: 10rpx;
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: auto 38rpx;
}
}
.talking-cont {
padding-bottom: 20rpx;
}
@@ -0,0 +1,248 @@
<template>
<view class="talking-info" v-if="dataInfo.intrctWay === '02'">
<view :class="['talking-gif', !voiceLoading&&(props.activeVoiceId === dataInfo.id) ? 'is-play' : '']">
<CommonLoading color="#f60" v-show="voiceLoading"/>
</view>
<view class="right-time">{{voiceTime}}</view>
<view class="talking-cont">
<text :class="['talking-text translate-text loading']" v-show="!translateLoading" v-html="showContent">
</text>
<view class="talking-text translate-text" v-if="translateLoading">
<CommonLoading color="#f60"/>
</view>
</view>
<view class="btns-cont">
<view class="play-btn">
<image class="icon"
v-show="!(!voiceLoading&&(props.activeVoiceId === dataInfo.id))"
@click.stop="playVoice"
src="@/static/images/course/play-blue.png"
style="width: 100%;height: 100%;"
></image>
<image class="icon"
v-show="!voiceLoading&&(props.activeVoiceId === dataInfo.id)"
@click.stop="pauseVoice"
src="@/static/images/course/stop-blue.png"
style="width: 100%;height: 100%;"
></image>
<!-- <view class="icon text-btn" v-if="type === 1" @click.stop="translateVoice(dataInfo)"></view> -->
</view>
</view>
</view>
<!-- <view class="talking-info talking-info-text" v-else-if="dataInfo.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>
<view :class="['talking-gif']">
<CommonLoading color="#f60"/>
</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: () => ({}),
type: Object
},
activeVoiceId:{
default:'',
type: String
},
})
const { activeVoiceId, dataInfo } = toRefs(props)
const emit = defineEmits(['changePlay'])
// 声音播放初始化
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 dataInfo.value.talkingText || ''
})
onMounted(()=>{
initVoice()
})
onUnload(()=>{
pauseVoice()
})
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) {
setTimeout(()=>{
talkVoiceObj.value.src = unref(dataInfo).talkingVoice
talkVoiceObj.value.play()
},300)
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.onPause(()=>{
})
talkVoiceObj.value.onPlay(()=>{
if(isTesting.value){
talkVoiceObj.value.pause()
isTesting.value = false
}
})
talkVoiceObj.value.onEnded(()=>{
emit('changePlay', '')
talkVoiceObj.value.src = ''
})
talkVoiceObj.value.onError(()=>{
talkVoiceObj.value.src = ''
})
}
const isTesting= ref(false)
watch(() => props.dataInfo, () => {},{deep: true,immediate: true})
</script>
<style scoped lang="scss">
.talking-info{
padding: 20rpx;
border-radius: 15rpx;
background-color: #f5f5f5;
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-gif.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;
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 #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>
+31 -18
View File
@@ -16,7 +16,7 @@
<view class="play-btn">
<image class="icon"
v-show="!(!voiceLoading&&(props.activeVoiceId === dataInfo.id))"
@click="playVoice"
@click.stop="playVoice"
src="@/static/images/course/play-blue.png"
style="width: 100%;height: 100%;"
></image>
@@ -59,7 +59,6 @@ import { onUnload } from '@dcloudio/uni-app';
})
const { activeVoiceId, dataInfo } = toRefs(props)
const emit = defineEmits(['changePlay'])
watch(() => props.dataInfo, () => {},{deep: true,immediate: true})
// 声音播放初始化
const talkVoiceObj = ref(null)
@@ -78,14 +77,6 @@ import { onUnload } from '@dcloudio/uni-app';
onUnload(()=>{
pauseVoice()
})
watch(() => props.dataInfo.talkingVoice,(val)=>{
if(val){
initVoice()
}
},{
deep: true,
immediate: true
})
const pauseVoice = () => {
if(!talkVoiceObj.value) return
talkVoiceObj.value.pause()
@@ -95,7 +86,10 @@ import { onUnload } from '@dcloudio/uni-app';
const playVoice = () =>{
emit('changePlay', unref(dataInfo)?.id)
if(talkVoiceObj.value && talkVoiceObj.value.src) {
talkVoiceObj.value.play()
setTimeout(()=>{
talkVoiceObj.value.src = unref(dataInfo).talkingVoice
talkVoiceObj.value.play()
},300)
return
}else{
talkVoiceObj.value.src = ''
@@ -110,6 +104,14 @@ import { onUnload } from '@dcloudio/uni-app';
}
const initVoice = () => {
talkVoiceObj.value = uni.createInnerAudioContext()
talkVoiceObj.value.onPause(()=>{
})
talkVoiceObj.value.onPlay(()=>{
if(isTesting.value){
talkVoiceObj.value.pause()
isTesting.value = false
}
})
talkVoiceObj.value.onEnded(()=>{
emit('changePlay', '')
talkVoiceObj.value.src = ''
@@ -117,14 +119,16 @@ import { onUnload } from '@dcloudio/uni-app';
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)
}
itemVoice.value = unref(dataInfo).talkingVoice
talkVoiceObj.value.src = itemVoice.value
talkVoiceObj.value.play()
isTesting.value = true
talkVoiceObj.value.pause()
setTimeout(()=>{
setVoiceTime(0)
},100)
}
const isTesting= ref(false)
const setVoiceTime = (sum) => {
sum += 1
if(Math.ceil(talkVoiceObj.value.duration)>0){
@@ -139,6 +143,15 @@ import { onUnload } from '@dcloudio/uni-app';
},200)
}
}
watch(() => props.dataInfo, () => {},{deep: true,immediate: true})
watch(() => props.dataInfo.talkingVoice,(val)=>{
if(val){
initVoice()
}
},{
deep: true,
immediate: true
})
</script>
<style scoped lang="scss">
@@ -10,10 +10,10 @@
<!-- <view class="ai-name-desc">{{ roleInfo?.teacherDesc }}</view> -->
</view>
</view>
<questionVue v-if="talkingType === 'question'" @changePlay="changePlay" :dataInfo="talkingInfo" :isLast="isLast"
<questionVue v-if="talkingType === 'question'" @changePlay="changePlay" :dataInfo="talkingInfo" :isLast="isLast" :roleInfo="roleInfo"
:activeVoiceId="activeVoiceId"></questionVue>
<answerVue v-else-if="talkingType === 'answer'" @changePlay="changePlay" :dataInfo="props.talkingInfo" :isLast="isLast"
:isAnswering="props.isAnswering" @handleEvents="handleEvents">
<answerVue v-else-if="talkingType === 'answer'" @changePlay="changePlay" :dataInfo="talkingInfo" :isLast="isLast" :roleInfo="roleInfo"
:isAnswering="props.isAnswering" @handleEvents="handleEvents" :activeVoiceId="activeVoiceId">
</answerVue>
<view v-else></view>
<view class="talking-tips" v-if="talkingType === 'answer'">
@@ -82,7 +82,8 @@
})
const {
talkingType,
talkingInfo
talkingInfo,
roleInfo
} = toRefs(props)
const loadingTips = ref(false)
@@ -97,6 +98,7 @@
}
const getTips = () => {
loadingTips.value = true
handleEvents('scrollToBottom')
partnerChatPrompt({
execId: props.execId,
traId: props.traId,
@@ -104,6 +106,7 @@
}).then(res=>{
tipsContent.value = res.body
loadingTips.value = false
handleEvents('scrollToBottom')
}).catch(()=> {
loadingTips.value =false
})
@@ -191,7 +194,7 @@
.talking-tips-loading{
display: flex;
justify-content: center;
min-height: 40rpx;
min-height: 80rpx;
padding-top: 20rpx;
}
}
@@ -0,0 +1,180 @@
<template>
<view class="talking-item">
<!-- 头像信息 -->
<view class="user-info" v-if="talkingType === 'answer'">
<view class="avatar-box">
<ImagePreview class="img-box" :src="roleInfo?.ossAddr"></ImagePreview>
</view>
<view class="ai-name">
{{ roleInfo?.chaName }}
<!-- <view class="ai-name-desc">{{ roleInfo?.teacherDesc }}</view> -->
</view>
</view>
<questionVue v-if="talkingType === 'question'" @changePlay="changePlay" :dataInfo="talkingInfo" :isLast="isLast" :roleInfo="roleInfo"
:activeVoiceId="activeVoiceId"></questionVue>
<answerVue v-else-if="talkingType === 'answer'" @changePlay="changePlay" :dataInfo="talkingInfo" :isLast="isLast" :roleInfo="roleInfo"
:isAnswering="props.isAnswering" @handleEvents="handleEvents" :activeVoiceId="activeVoiceId" :isStudy="false">
</answerVue>
<view v-else></view>
</view>
</template>
<script setup>
import {
toRefs,
watch,
ref
} from 'vue'
import questionVue from './question-preview.vue';
import answerVue from './answer.vue';
import ImagePreview from '@/components/image-preview/image-preview.vue';
const emit = defineEmits(['handleEvents'])
const props = defineProps({
talkingType: {
default: 'question',
type: String
},
traId: {
default: '',
type: String
},
execId: {
default: '',
type: String
},
talkingInfo: {
default: () => ({}),
type: Object
},
roleInfo:{
default: () => ({}),
type: Object
},
activeVoiceId: {
default: '',
type: String
},
isAnswering: {
default: false,
type: Boolean
},
isLast: {
default: false,
type: Boolean
},
})
watch(() => props.activeVoiceId, () => {}, {
deep: true,
immediate: true
})
watch(() => props.talkingInfo, () => {}, {
deep: true,
immediate: true
})
const {
talkingType,
talkingInfo,
roleInfo
} = toRefs(props)
const loadingTips = ref(false)
const tipsContent = ref('')
const handleEvents = (type, info = {}) => {
emit('handleEvents', {
type,
data: props.talkingInfo,
id: "",
info
})
}
const changePlay = (id) => {
emit('handleEvents', {
type: 'changePlay',
data: props.talkingInfo,
id,
info: {}
})
}
</script>
<style scoped lang="scss">
.talking-item {
overflow: hidden;
margin-top: 0rpx;
position: relative;
margin-bottom: 24rpx;
&+.talking-item {
margin-top: 30rpx;
}
.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: 76rpx;
height: 76rpx;
overflow: hidden;
}
}
.ai-name {
float: left;
color: #666;
font-size: 25rpx;
line-height: 35rpx;
width: 140rpx;
white-space: pre-wrap;
.ai-name-desc {
color: #999;
margin-left: 16rpx;
display: inline-block;
}
}
}
.talking-tips{
float: left;
position: relative;
padding-top: 60rpx;
width: 100%;
&-btn{
position: absolute;
left: 50%;
top: 22rpx;
height: 24rpx;
line-height: 24rpx;
font-size: 24rpx;
color: #06f;
transform: translateX(-50% );
padding-left: 30rpx;
background: url(@/static/images/sparring/ask-icon.png) no-repeat;
background-position: left center;
background-size: 24rpx 24rpx;
}
&-cont{
width: 100%;
font-size: 24rpx;
line-height: 38rpx;
color: #8191AA;
}
.talking-tips-loading{
display: flex;
justify-content: center;
min-height: 80rpx;
padding-top: 20rpx;
}
}
}
</style>
+7 -1
View File
@@ -60,7 +60,10 @@
@click="previewFile(item)"> {{ item.dataName }}.{{ item.dataSuffix }} </view>
</view>
</view>
<view class="body-bottom">
<view class="body-bottom" v-if="isPreview">
<view class="body-bottom-btn" @click="toChooseRole('preview')">预览陪练</view>
</view>
<view class="body-bottom" v-else>
<view class="body-bottom-btn" @click="toChooseRole('test')">去练习</view>
<view class="body-bottom-btn" @click="toChooseRole('exam')">去考试</view>
</view>
@@ -117,8 +120,11 @@
const toChooseRole = type => {
common.navigateTo('/pages/sparring/chooseRole?traId=' + traId.value + '&type=' + type)
}
const isPreview = ref(false)
onLoad(params => {
traId.value = params.traId
isPreview.value = params.isPreview === '1'
console.log(params)
})
onMounted(() => {
getDetailInfo()
+256 -111
View File
@@ -23,36 +23,49 @@
<view class="body-top-collepse-title">
<view :class="['collepse-btn', showAllTalk?'show-all':'']" @click="showAllTalk = !showAllTalk"></view>
<view class="collepse-tab">
<uv-tabs :list="tabList" @click="changeTab" :scrollable="false" active-style="font-weight:bold; color:#000;"
class="font_pf my_tabs" :inactive-style="{ color: '#333333', fontSize: '28rpx', fontWeight: '500' }"
:activeStyle="{ color: '#333333', fontSize: '28rpx', fontWeight: '600' }"></uv-tabs>
<uv-tabs :list="tabList" @click="changeTab" :scrollable="false" active-style="font-weight:bold; color:#000;"
class="font_pf my_tabs" :inactive-style="{ color: '#333333', fontSize: '28rpx', fontWeight: '500' }"
:activeStyle="{ color: '#333333', fontSize: '28rpx', fontWeight: '600' }"></uv-tabs>
</view>
<view class="talk-item"></view>
<!-- 头像信息 -->
<view class="user-info" v-if="showTalkList.length>0 && showTalkList[0].type==='question'">
<!-- <view class="avatar-box">
<ImagePreview class="img-box" :src="roleInfo?.ossAddr"></ImagePreview>
</view>
<view class="ai-name">
{{ roleInfo?.chaName }}
</view> -->
</view>
<TalkingPreviewItem class="talk-item" v-for="item in showTalkList" :talkingType="item.type"
:talkingInfo="item" :roleInfo="roleInfo" :traId="traId" :execId="execId" :isAnswering="false"
:activeVoiceId="activeVoiceTalkingItemId" @handleEvents="talkItemEvents"></TalkingPreviewItem>
</view>
<view :class="['body-top-collepse-cont', showAllTalk?'show-all':'']">
<view class="talk-item"></view>
<TalkingPreviewItem class="talk-item" v-for="item in hideTalkList" :talkingType="item.type"
:talkingInfo="item" :roleInfo="roleInfo" :traId="traId" :execId="execId" :isAnswering="false"
:activeVoiceId="activeVoiceTalkingItemId" @handleEvents="talkItemEvents"></TalkingPreviewItem>
</view>
</view>
<view class="body-bottom">
<view class="body-bottom-title">维度评价总览</view>
<view class="body-bottom-item" v-for="item in evalDimens">
<view class="item-title">{{item.gradeDimensName}} {{ item.traGrade }}
<!-- <image class="item-title-icon" src="@/static/images/sparring/info-icon.png"></image> -->
<!-- <image class="item-title-icon" src="@/static/images/sparring/info-icon.png"></image> -->
</view>
<view class="item-cont">{{item.settDesc}}</view>
</view>
<view :class="['body-bottom-static', 'pass','fialed']">
<view :class="['body-bottom-static', isPass?'pass':'fialed']">
<view class="static-tip">得分</view>
<view class="static-score">{{ totalScore }}</view>
<view class="static-desc">{{1?'革命尚未成功,同志仍需努力!':'恭喜阁下过关斩将,顺利通关!'}}</view>
<view class="static-charts">
<ChartThree v-if="length === 3" fromType="tra" :dataList="evalDimens" />
<ChartThree v-if="length === 3" fromType="tra" :dataList="evalDimens" />
<ChartFour v-if="length === 4" fromType="tra" :dataList="evalDimens" />
<ChartFive v-if="length === 5" fromType="tra" :dataList="evalDimens" />
<ChartSix v-if="length === 6" fromType="tra" :dataList="evalDimens" />
<ChartSeven v-if="length === 7" fromType="tra" :dataList="evalDimens"/>
<ChartSix v-if="length === 6" fromType="tra" :dataList="evalDimens" />
<ChartSeven v-if="length === 7" fromType="tra" :dataList="evalDimens" />
</view>
</view>
</view>
@@ -72,13 +85,18 @@
onLoad,
onUnload
} from '@dcloudio/uni-app';
import common from '@/common/common'
import Feedback from './components/feedback.vue';
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 ChartSix from '@/pages/course/components/chartSix.vue';
import ChartSeven from '@/pages/course/components/chartSeven.vue';
import { partnerChatReport } from "@/api/sparring.js"
import TalkingPreviewItem from './components/talking-preview-item.vue'
import ImagePreview from '@/components/image-preview/image-preview.vue';
import {
partnerChatReport
} from "@/api/sparring.js"
const traId = ref('')
const execId = ref('')
const feedbackRef = ref()
@@ -87,8 +105,8 @@
const length = ref(6)
const traInfo = ref({})
const traTm = ref('')
const tabList = ref([
{
const activeVoiceTalkingItemId = ref('')
const tabList = ref([{
name: '全部',
value: ''
},
@@ -101,60 +119,135 @@
value: '02'
}
])
// const evalDimens = [{
// gradeDimensName: '维度1',
// anlyGrade: 60
// }, {
// gradeDimensName: '维度2',
// anlyGrade: 80
// }, {
// gradeDimensName: '维度3',
// anlyGrade: 30
// }]
const showTalkList = computed(() => {
return allTalkList.value.slice(0, 2)
})
const hideTalkList = computed(() => {
return allTalkList.value.slice(2)
})
const allTalkList = computed(() => {
if (!filterType.value) {
return traPartnerExecuteAnswerRecords.value
} else if (filterType.value === '01') {
return traPartnerExecuteAnswerRecords.value.filter((t, index) => {
return t.talkingText.indexOf('#15b859') >= 0 || traPartnerExecuteAnswerRecords.value[index + 1]
?.talkingText.indexOf('#15b859') >= 0
})
} else if (filterType.value === '02') {
return traPartnerExecuteAnswerRecords.value.filter((t, index) => {
return t.talkingText.indexOf('#d70c18') >= 0 || traPartnerExecuteAnswerRecords.value[index + 1]
?.talkingText.indexOf('#d70c18') >= 0
})
}
})
// 关闭播放状态
const changePlayItemId = (id = '') => {
activeVoiceTalkingItemId.value = id
}
const talkItemEvents = ({
type,
data,
id,
info = {}
}) => {
switch (type) {
case 'changePlay':
activeVoiceTalkingItemId.value = id;
break;
case 'scrollToBottom':
break;
default:
break;
}
};
const feedbackNow = () => {
feedbackRef.value.open({
traId: traId.value
});
}
const filterType = ref('')
const changeTab = (info) => {
console.log(info)
filterType.value = info.value
}
const evalDimens = ref([])
const traPartnerExecuteAnswerRecords = ref([])
const totalScore = ref(0)
const showLoading = ref(true)
const isPass = ref(false)
const negativeList = ref([]) // 劣质话术
const positiveList = ref([]) // 劣质话术
const getData = () => {
// execId=db2f275b-c04a-4572-83dd-74a95ff49d67&traId=PARTNERINFO02501812207220251103160409000007402
partnerChatReport({
// traId: traId.value,
// execId: execId.value
traId: 'PARTNERINFO02501812207220251103160409000007402',
execId: 'db2f275b-c04a-4572-83dd-74a95ff49d67'
}).then(res=>{
if(res.body.code === 200){
traId: traId.value,
execId: execId.value
// traId: 'PARTNERINFO02501812207220251103160409000007402',
// execId: 'db2f275b-c04a-4572-83dd-74a95ff49d67'
}).then(res => {
if (res.body.code === 200) {
setTimeout(getData, 2000)
}else{
traInfo.value = res.body.traPartnerInfo
roleInfo.value = res.body.traPartnerCharacterInfoVo
traTm.value = res.body.traTm
evalDimens.value = res.body.traPartnerSettlement.map(t=>{
} else {
traInfo.value = res.body.traPartnerInfo || {}
roleInfo.value = res.body.traPartnerCharacterInfoVo || {}
traTm.value = res.body.traTm || '0'
isPass.value = res.body.traPartnerExecuteHisVo?.traResult === 'P'
totalScore.value = res.body.traPartnerExecuteHisVo?.traGrade || '0'
positiveList.value = res.body.positiveList || []
negativeList.value = res.body.negativeList || []
let _evalDimens = res.body.traPartnerSettlement || []
evalDimens.value = _evalDimens.map(t => {
return {
...t,
anlyGrade: t.traGrade,
gradeDimensName: t.gradeDimensName||'维度'
gradeDimensName: t.gradeDimensName || '维度'
}
})
traPartnerExecuteAnswerRecords.value = res.body.traPartnerExecuteAnswerRecords
setTimeout(()=>{
let _talkingList = res.body.traPartnerExecuteAnswerRecords || []
traPartnerExecuteAnswerRecords.value = _talkingList.map(t => {
let _content = setTextColor(t.content)
if (t.openStyle === '02') {
return {
...t,
type: 'question',
talkingText: _content,
isLoading: false,
execLogId: t.execLogId || '',
intrctWay: '02',
id: t.execLogId,
...t,
}
} else if (t.openStyle === '01') {
return {
...t,
type: 'answer',
talkingText: _content,
isLoading: false,
execLogId: t.execLogId || '',
id: t.execLogId,
}
} else {
return t
}
})
setTimeout(() => {
showLoading.value = false
},800)
}, 800)
}
})
}
const setTextColor = (str) => {
let _str = str
positiveList.value.forEach(t => {
_str = _str.split(t).join(`<text style="color: #15b859;text-decoration:underline;">${t}</text>`)
})
negativeList.value.forEach(t => {
_str = _str.split(t).join(`<text style="color: #d70c18;text-decoration:underline;">${t}</text>`)
})
console.log(_str)
return _str
}
onLoad((params) => {
traId.value = params.traId;
execId.value = params.execId
@@ -179,44 +272,46 @@
padding: calc(var(--status-bar-height) + 90rpx) 0 0rpx;
// background-image: url(@/static/images/sparring/bg.png);
// background-size: cover;
}
.sparring-result-loading {
position: fixed;
z-index: 20;
height: 100vh;
width: 100vw;
background-image: url(@/static/images/sparring/bg.png);
.loading-img {
.sparring-result-loading {
position: fixed;
z-index: 20;
height: 100vh;
width: 100vw;
background-image: url(@/static/images/sparring/bg.png);
.loading-img {
position: absolute;
width: 380rpx;
height: 540rpx;
top: 40%;
transform: translateY(-50%) translateX(-50%);
left: 50%;
padding-top: 540rpx;
line-height: 50rpx;
box-sizing: border-box;
text-align: center;
background-image: url(@/static/images/sparring/waiting.png);
background-repeat: no-repeat;
background-size: 380rpx 590rpx;
&::after {
content: '报告生成中,请耐心等待 ~ ';
display: block;
position: absolute;
width: 380rpx;
height: 540rpx;
top: 40%;
transform: translateY(-50%) translateX(-50%);
left: 50%;
padding-top: 540rpx;
left: 0%;
width: 100%;
bottom: -50rpx;
height: 50rpx;
line-height: 50rpx;
box-sizing: border-box;
text-align: center;
background-image: url(@/static/images/sparring/waiting.png);
background-repeat: no-repeat;
background-size: 380rpx 590rpx;
&::after{
content: '报告生成中,请耐心等待 ~ ';
display: block;
position: absolute;
left: 0%;
width: 100%;
bottom: -50rpx;
height: 50rpx;
line-height: 50rpx;
text-align: center;
font-size: 28rpx;
}
font-size: 28rpx;
}
}
}
.top-box {
height: 90rpx;
@@ -325,13 +420,56 @@
}
.body-top-collepse-title {
padding-top: 10rpx;
position: relative;
.collepse-tab{
position: absolute;
width: 360rpx;
right: 120rpx;
top: 4rpx;
min-height: 100rpx;
.user-info {
overflow: hidden;
height: 92rpx;
.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: 76rpx;
height: 76rpx;
overflow: hidden;
}
}
.ai-name {
float: left;
color: #666;
font-size: 25rpx;
line-height: 35rpx;
width: 140rpx;
white-space: pre-wrap;
.ai-name-desc {
color: #999;
margin-left: 16rpx;
display: inline-block;
}
}
}
.collepse-tab {
position: absolute;
width: 300rpx;
right: 120rpx;
top: 0rpx;
z-index: 10;
}
.collepse-btn {
position: absolute;
right: 0;
@@ -340,6 +478,7 @@
height: 46rpx;
border: 2rpx solid #d4dce8;
border-radius: 23rpx;
z-index: 10;
&.show-all {
&::after {
@@ -399,25 +538,25 @@
// top: 2rpx;
// left: 10rpx;
// }
line-height: 40rpx;
position: relative;
padding: 0rpx 46rpx 0rpx 26rpx;
line-height: 42rpx;
font-size: 29rpx;
margin-bottom: 16rpx;
&::before {
content: '';
display: block;
background-color: #000;
width: 12rpx;
height: 12rpx;
border-radius: 50%;
position: absolute;
left: 0rpx;
top: 14rpx;
}
line-height: 40rpx;
position: relative;
padding: 0rpx 46rpx 0rpx 26rpx;
line-height: 42rpx;
font-size: 29rpx;
margin-bottom: 16rpx;
&::before {
content: '';
display: block;
background-color: #000;
width: 12rpx;
height: 12rpx;
border-radius: 50%;
position: absolute;
left: 0rpx;
top: 14rpx;
}
}
.item-cont {
@@ -425,32 +564,37 @@
line-height: 50rpx;
}
}
&-static{
&-static {
border-top: 2px solid #eee;
padding-top: 38rpx;
background-repeat: no-repeat;
background-size: 270rpx 190rpx;
background-position: right 40rpx;
&.fialed{
&.fialed {
background-image: url(@/static/images/sparring/def-res.png);
}
&.pass{
&.pass {
background-image: url(@/static/images/sparring/pass-res.png);
}
.static-tip{
.static-tip {
line-height: 44rpx;
font-size: 32rpx;
margin-bottom: 24rpx;
font-weight: 600;
}
.static-score{
.static-score {
font-size: 66rpx;
color: #06f;
font-family: Arial, Helvetica, sans-serif;
marign-bottom: 20rpx;
}
.static-desc{
.static-desc {
color: #999;
font-size: 25rpx;
margin-bottom: 32rpx;
@@ -460,14 +604,15 @@
}
.talk-item {
height: 200rpx;
// height: 200rpx;
overflow: hidden;
background-color: #0f0;
}
.my_tabs {
// 解决这个圆角的图片,开穿透
:deep(.uv-tabs__wrapper__nav__line) {
background: linear-gradient(90deg, #06f 0%, rgb(0,159,255) 50%, rgb(255,255,255) 100%) !important;
background: linear-gradient(90deg, #06f 0%, rgb(0, 159, 255) 50%, rgb(255, 255, 255) 100%) !important;
}
:deep(.uv-tabs__wrapper__nav__item) {
+12 -6
View File
@@ -235,8 +235,8 @@
case 'changePlay':
activeVoiceTalkingItemId.value = id;
break;
case 'askTip':
// 获取问答提示
case 'scrollToBottom':
scrollToBottomNow();
break;
default:
break;
@@ -249,7 +249,8 @@
const answerText = ref('')
const touchBtnCallBack = ref(null)
const initsocketTask = () => {
socketStore.creatSocket('/trapractice/partnersocket/open?summary=');
let _src = talkingType.value === 'preview'?'/trapractice/partnersocket/open':'/trapractice/partnersocket/open'
socketStore.creatSocket(_src + '?summary=');
// unref(SocketObj).on('open', (res)=>{
// if(!answerIsOver.value){
// sendMessage({
@@ -346,26 +347,31 @@
console.log('开始回答!');
answerIsOver.value = false;
answerText.value = '';
talkingList.value.pop(); detailInfo.chaName
talkingList.value.pop();
talkingList.value.push({
type: 'answer',
talkingText: answerText.value,
isLoading: false,
execLogId: body.execLogId||'',
id: _id,
...body,
askData: {
...askData.value
},
talkingFiles: [],
talkingCrsList: []
});
} else if (body.event === 'token') {
answerIsOver.value = false;
answerText.value += body.data;
console.log(answerText.value)
talkingList.value[talkingList.value.length - 1].talkingText = answerText.value;
} else if (body.event === 'end') {
console.log('完成回答!');
answerIsOver.value = true;
} else if (body.event === 'recordAi') {
console.log('推送声音id');
talkingList.value[talkingList.value.length - 1].execLogId = body.execLogId
}
scrollToBottomNow();
return;
}
if (commond === 'ASK-RE-ANSWER') {
@@ -0,0 +1,248 @@
<template>
<view class="item">
<view class="details_div">
<view class="img_div">
<image-preview class="img" :src="item.ossAddr"></image-preview>
</view>
<view class="right_div">
<view class="title ellipsis-text">
{{ item.traName }}
</view>
<view class="prompt_div">
<view class="prompt_div_left">
<view class="top">{{ item.maxGrade || '0' }}</view>
<view class="bottom">历史最高分</view>
</view>
<view class="prompt_div_line"></view>
<view class="prompt_div_right" @click="show_list_click(item.examCnt)">
<view class="click_text">
{{ item.executeHisNum || 0 }}
<uv-icon
class="click_text_icon"
:class="{ click_text_icon_action: show_list_state }"
name="arrow-right"
color="#0066FF"
size="12"
:bold="true"
></uv-icon>
</view>
<view class="bottom">考试记录</view>
</view>
</view>
</view>
</view>
<view class="list_div" :style="{ height: list_div_height }">
<view class="list_item" v-for="item_list in data_list" @click="click_list_item(item_list)">
<view class="list_content">
<view class="list_small_item">
<image src="@/static/images/courseRecord/time.png" class="icon_img"></image>
<view class="prompt">考试时间{{ item_list.startPracticeTm }}</view>
</view>
<view class="list_small_item">
<image src="@/static/images/courseRecord/duration.png" class="icon_img"></image>
<view class="prompt">考试得分{{ item_list.traGrade }}</view>
</view>
</view>
<view class="fl1"></view>
<uv-icon name="arrow-right" color="#979797" size="17" :bold="true"></uv-icon>
</view>
<uv-load-more
v-if="status !== 'nomore'"
@click="getData()"
:status="status"
loadmore-text="点击加载更多"
:height="30"
></uv-load-more>
</view>
</view>
</template>
<script setup>
import { computed, ref, reactive } from 'vue';
import common from '@/common/common';
import { queryCrsExamHisByExamId } from '@/api/courseRecord.js';
import { useItemList } from '../useItemList.js';
const props = defineProps({
item: {
type: Object,
default: () => {}
}
});
const item = computed(() => props.item);
const fetchFunction = (params) => queryCrsExamHisByExamId({ traId: item.value.traId, ...params });
const { status, data_list, show_list_state, list_div_height, show_list_click, getData, clear } =
useItemList(fetchFunction);
// 跳转到详情
const click_list_item = (list_item) => {
common.navigateTo('/pages/sparring/result?traId=' + list_item.traId + '&execId=' + list_item.execId)
};
defineExpose({ clear });
</script>
<style scoped lang="scss">
.icon_img {
width: 24rpx;
height: 24rpx;
}
.list_div {
overflow: hidden; /* 确保内容不会溢出容器 */
transition: height 0.2s ease-out; /* 高度动画 */
.list_item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18rpx 32rpx 18rpx 28rpx;
background-color: #f9fbff;
border-radius: 8rpx;
margin: 20rpx 0 0 0;
.list_content {
.list_small_item {
display: flex;
align-items: center;
font-weight: 400;
font-size: 24rpx;
color: #666666;
line-height: 40rpx;
text-align: left;
font-style: normal;
.prompt {
margin-left: 16rpx;
}
}
}
}
}
.item {
margin: 17rpx 20rpx;
background-color: #fff;
border-radius: 16rpx;
padding: 32rpx 22rpx;
.details_div {
display: flex;
.img_div {
position: relative;
width: 224rpx;
height: 168rpx;
overflow: hidden;
border-radius: 22rpx;
.img {
width: 224rpx;
height: 168rpx;
}
.subscript {
position: absolute;
right: 0;
top: 0;
width: 92rpx;
height: 46rpx;
border-radius: 0px 0px 0px 22rpx;
font-family: PingFangSC;
font-weight: 400;
font-size: 22rpx;
line-height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
}
.right_div {
flex: 1;
overflow: hidden;
margin-left: 24rpx;
display: flex;
flex-direction: column;
.title {
font-family: PingFangSC;
color: #333333;
text-align: left;
font-style: normal;
font-weight: 600;
font-size: 28rpx; /* 字体大小30rpx */
overflow: hidden; /* 超出部分隐藏 */
margin-bottom: 8rpx;
min-height: 40rpx;
}
.prompt_div {
font-family: PingFangSC;
font-weight: 400;
font-size: 22rpx;
color: #666;
line-height: 34rpx;
display: flex;
align-items: center;
overflow: hidden;
height: 116rpx;
background-color: #f9fbff;
border-radius: 8rpx;
justify-content: space-around;
.prompt_div_line {
width: 4rpx;
height: 30rpx;
background: linear-gradient(
to bottom,
RGBA(242, 244, 248, 1) 0%,
RGBA(230, 232, 238, 1) 30%,
RGBA(230, 232, 238, 1) 70%,
RGBA(242, 244, 248, 1) 100%
);
}
.prompt_div_left,
.prompt_div_right {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
.top,
.click_text {
font-family: Arial;
font-weight: 700;
font-size: 30rpx;
color: #333333;
line-height: 34rpx;
text-align: center;
font-style: normal;
}
.bottom {
}
.click_text {
position: relative;
.click_text_icon {
position: absolute;
margin-left: 18rpx;
top: calc(50% - 12rpx);
left: calc(100% - 14rpx);
/* 添加过渡动画,包含延迟效果 */
transition: transform 0.2s ease;
}
.click_text_icon_action {
transform: rotate(90deg);
}
}
}
}
.people_progress_div {
display: flex;
font-family: PingFangSC;
font-weight: 400;
font-size: 23rpx;
color: #666;
margin-bottom: 23rpx;
line-height: 23rpx;
flex-wrap: nowrap;
height: 23rpx;
overflow: hidden;
}
}
}
}
</style>
@@ -0,0 +1,53 @@
<template>
<z-paging
ref="pagingRef"
v-model="data_list"
@query="queryList"
class="scroll_div"
:auto="false"
empty-view-text="暂无记录"
@refresherTouchend="refresherTouchend"
>
<view class="scroll-Y">
<itemVue ref="itemVueRef" :item="item" v-for="item in data_list"></itemVue>
</view>
</z-paging>
</template>
<script setup lang="ts">
import itemVue from './examRecordItem.vue';
import { ref, reactive, onMounted, watch } from 'vue';
import { queryCrsExamRecordPaging } from '@/api/traRecord.js';
import useZpaging from '@/composables/useZpaging';
const itemVueRef = ref([]);
const { pagingRef, queryList, data_list, reload, total } = useZpaging({
fetchFunction: queryCrsExamRecordPaging
});
const clearAll = () => {
itemVueRef.value.forEach((child) => {
if (child && typeof child.clear === 'function') {
child.clear();
}
});
};
const refresherTouchend = () => {
clearAll();
};
const props = defineProps<{
tabIndex: number;
currentIndex: number;
}>();
onMounted(() => {
watch(
() => props.currentIndex,
(v) => v === props.tabIndex && total.value === -1 && reload(),
{ immediate: true }
);
});
</script>
<style scoped lang="scss">
.scroll-Y {
background-color: #f1f5fa;
height: 100%;
}
</style>
@@ -0,0 +1,250 @@
<template>
<view class="item">
<view class="details_div">
<view class="img_div">
<image-preview class="img" :src="item.ossAddr"></image-preview>
</view>
<view class="right_div">
<view class="title ellipsis-text">
{{ item.traName }}
</view>
<view class="prompt_div">
<view class="prompt_div_left">
<view class="top">{{ item.maxGrade || '0' }}</view>
<view class="bottom">历史最高分</view>
</view>
<view class="prompt_div_line"></view>
<view class="prompt_div_right" @click="show_list_click(item.exrCnt)">
<view class="click_text">
{{ item.executeHisNum || '0' }}
<uv-icon
class="click_text_icon"
:class="{ click_text_icon_action: show_list_state }"
name="arrow-right"
color="#0066FF"
size="12"
:bold="true"
></uv-icon>
</view>
<view class="bottom">练习记录</view>
</view>
</view>
</view>
</view>
<view class="list_div" :style="{ height: list_div_height }">
<view class="list_item" v-for="item_list in data_list" @click="click_list_item(item_list)">
<view class="list_content">
<view class="list_small_item">
<image src="@/static/images/courseRecord/time.png" class="icon_img"></image>
<view class="prompt">练习时间{{ item_list.startPracticeTm }}</view>
</view>
<view class="list_small_item">
<image src="@/static/images/courseRecord/duration.png" class="icon_img"></image>
<view class="prompt">练习得分{{item_list.traGrade }}</view>
</view>
</view>
<view class="fl1"></view>
<uv-icon name="arrow-right" color="#979797" size="17" :bold="true"></uv-icon>
</view>
<uv-load-more
v-if="status !== 'nomore'"
@click="getData()"
:status="status"
loadmore-text="点击加载更多"
:height="30"
></uv-load-more>
</view>
</view>
</template>
<script setup>
import { computed, ref, reactive } from 'vue';
import common from '@/common/common';
import { queryPracticeHis } from '@/api/traRecord.js';
import { useItemList, formatDuration } from '../useItemList.js';
const props = defineProps({
item: {
type: Object,
default: () => {}
}
});
const item = computed(() => props.item);
const fetchFunction = (params) => queryPracticeHis({ traId: item.value.traId, ...params });
const { status, data_list, show_list_state, list_div_height, show_list_click, getData, clear } =
useItemList(fetchFunction);
// 跳转到详情
const click_list_item = (list_item) => {
common.navigateTo('/pages/sparring/result?traId=' + list_item.traId + '&execId=' + list_item.execId)
};
defineExpose({ clear });
</script>
<style scoped lang="scss">
.icon_img {
width: 24rpx;
height: 24rpx;
}
.list_div {
overflow: hidden; /* 确保内容不会溢出容器 */
transition: height 0.2s ease-out; /* 高度动画 */
.list_item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18rpx 32rpx 18rpx 28rpx;
background-color: #f9fbff;
border-radius: 8rpx;
margin: 20rpx 0 0 0;
.list_content {
.list_small_item {
display: flex;
align-items: center;
font-weight: 400;
font-size: 24rpx;
color: #666666;
line-height: 40rpx;
text-align: left;
font-style: normal;
.prompt {
margin-left: 16rpx;
}
}
}
}
}
.item {
margin: 17rpx 20rpx;
background-color: #fff;
border-radius: 16rpx;
padding: 32rpx 22rpx;
.details_div {
display: flex;
.img_div {
position: relative;
width: 224rpx;
height: 168rpx;
overflow: hidden;
border-radius: 22rpx;
.img {
width: 224rpx;
height: 168rpx;
}
.subscript {
position: absolute;
right: 0;
top: 0;
width: 92rpx;
height: 46rpx;
border-radius: 0px 0px 0px 22rpx;
font-family: PingFangSC;
font-weight: 400;
font-size: 22rpx;
line-height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
}
.right_div {
flex: 1;
overflow: hidden;
margin-left: 24rpx;
display: flex;
flex-direction: column;
.title {
font-family: PingFangSC;
color: #333333;
text-align: left;
font-style: normal;
font-weight: 600;
font-size: 28rpx; /* 字体大小30rpx */
overflow: hidden; /* 超出部分隐藏 */
margin-bottom: 8rpx;
min-height: 40rpx;
}
.prompt_div {
font-family: PingFangSC;
font-weight: 400;
font-size: 22rpx;
color: #666;
line-height: 34rpx;
display: flex;
align-items: center;
overflow: hidden;
height: 116rpx;
background-color: #f9fbff;
border-radius: 8rpx;
justify-content: space-around;
.prompt_div_line {
width: 4rpx;
height: 30rpx;
background: linear-gradient(
to bottom,
RGBA(242, 244, 248, 1) 0%,
RGBA(230, 232, 238, 1) 30%,
RGBA(230, 232, 238, 1) 70%,
RGBA(242, 244, 248, 1) 100%
);
}
.prompt_div_left,
.prompt_div_right {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
.top,
.click_text {
font-family: Arial;
font-weight: 700;
font-size: 30rpx;
color: #333333;
line-height: 34rpx;
text-align: center;
font-style: normal;
}
.bottom {
}
.click_text {
position: relative;
.click_text_icon {
position: absolute;
margin-left: 18rpx;
top: calc(50% - 12rpx);
left: calc(100% - 14rpx);
/* 添加过渡动画,包含延迟效果 */
transition: transform 0.2s ease;
}
.click_text_icon_action {
transform: rotate(90deg);
}
}
}
}
.people_progress_div {
display: flex;
font-family: PingFangSC;
font-weight: 400;
font-size: 23rpx;
color: #666;
margin-bottom: 23rpx;
line-height: 23rpx;
flex-wrap: nowrap;
height: 23rpx;
overflow: hidden;
}
}
}
}
</style>
@@ -0,0 +1,53 @@
<template>
<z-paging
ref="pagingRef"
v-model="data_list"
@query="queryList"
class="scroll_div"
:auto="false"
empty-view-text="暂无记录"
@refresherTouchend="refresherTouchend"
>
<view class="scroll-Y">
<itemVue ref="itemVueRef" :item="item" v-for="item in data_list"></itemVue>
</view>
</z-paging>
</template>
<script setup lang="ts">
import itemVue from './practiceRecordItem.vue';
import { ref, reactive, onMounted, watch } from 'vue';
import useZpaging from '@/composables/useZpaging';
import { queryPracticeRecordPaging } from '@/api/traRecord.js';
const itemVueRef = ref([]);
const { pagingRef, queryList, data_list, reload, total } = useZpaging({
fetchFunction: queryPracticeRecordPaging
});
const clearAll = () => {
itemVueRef.value.forEach((child) => {
if (child && typeof child.clear === 'function') {
child.clear();
}
});
};
const refresherTouchend = () => {
clearAll();
};
const props = defineProps<{
tabIndex: number;
currentIndex: number;
}>();
onMounted(() => {
watch(
() => props.currentIndex,
(v) => v === props.tabIndex && total.value === -1 && reload(),
{ immediate: true }
);
});
</script>
<style scoped lang="scss">
.scroll-Y {
background-color: #f1f5fa;
height: 100%;
}
</style>
+161
View File
@@ -0,0 +1,161 @@
<template>
<z-paging-swiper>
<template #top>
<view class="detail-main">
<nav-bar :is_seat="false"></nav-bar>
<view class="fixed_search_div">
<view class="back_div" @click="common.navigateBack()">
<view class="back-icon"></view>
</view>
<view class="title font_pf">陪练记录</view>
</view>
<view class="detail-body">
<view class="body-tab">
<uv-tabs
class="font_pf my_tabs"
:current="tabAct"
:list="tabList"
@change="tabChange"
:scrollable="false"
line-color="#0066FF"
:inactive-style="{ color: '#333333', fontSize: '30rpx', fontWeight: '600' }"
:activeStyle="{ color: '#0066FF', fontSize: '30rpx', fontWeight: '600' }"
:lineColor="`url(${LineBg}) 10% 10%`"
></uv-tabs>
</view>
</view>
</view>
</template>
<swiper class="swiper" :current="tabAct" :indicator-dots="false" @change="swiperChange">
<swiper-item class="swiper-item">
<practiceRecordList
:ref="(el) => (listItemRefs[0] = el)"
:tabIndex="0"
:currentIndex="tabAct"
></practiceRecordList>
</swiper-item>
<swiper-item class="swiper-item">
<examRecordList :ref="(el) => (listItemRefs[1] = el)" :tabIndex="1" :currentIndex="tabAct"></examRecordList>
</swiper-item>
</swiper>
</z-paging-swiper>
</template>
<script setup>
import { LineBg } from '@/enum.js';
import practiceRecordList from './components/practiceRecordList.vue';
import examRecordList from './components/examRecordList.vue';
import { ref, onMounted, watch } from 'vue';
import common from '@/common/common.js';
import useIndexList from '@/composables/useIndexList.js';
const { tabAct, listItemRefs, tabChange, swiperChange } = useIndexList();
const tabList = ref([
{
name: '练习记录'
},
{
name: '考试记录'
}
]);
</script>
<style lang="scss" scoped>
$bg-margin-left: 50rpx;
.my_tabs {
// 解决这个圆角的图片,开穿透
:deep(.uv-tabs__wrapper__nav__line) {
height: 26rpx !important;
left: 60rpx;
background-size: 28rpx !important;
background-repeat: no-repeat !important;
}
}
.swiper {
height: 100%;
}
.swiper-item {
display: block;
height: 100%;
background-color: #f1f5fa;
}
.detail-body {
background: #ffffff;
border-radius: 15rpx;
z-index: 101;
overflow: hidden;
.body-tab {
height: 88rpx;
}
}
.detail-main {
background-color: #f6f8ff;
overflow: hidden;
}
.title {
color: #000000;
font-size: 34rpx;
height: 34rpx;
font-weight: 600;
line-height: 34rpx;
}
.fixed_search_input_div {
width: 100%;
height: 80rpx;
padding: 0 22rpx;
// background-color: red;
background-color: #fff;
:deep(.uv-input.uv-input--radius) {
border-radius: 16rpx !important;
}
}
.fixed_search_div {
padding: calc(20rpx + var(--status-bar-height)) 15rpx 34rpx 30rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
background-color: #fff;
// background-color: red;
// height: 180rpx;
position: relative;
.back_div {
position: absolute;
left: 20rpx;
top: var(--status-bar-height);
padding: 10rpx;
.back-icon {
position: relative;
width: 50rpx;
height: 50rpx;
cursor: pointer;
display: flex;
align-items: center;
}
.back-icon::before,
.back-icon::after {
content: '';
position: absolute;
transition: all 0.3s ease;
}
.back-icon::after {
top: 25%;
left: 25%;
width: 40%;
height: 40%;
border-top: 4rpx solid #000000;
border-left: 4rpx solid #000000;
transform: rotate(-45deg);
}
}
}
</style>
+112
View File
@@ -0,0 +1,112 @@
import {
computed,
reactive,
nextTick,
ref
} from 'vue';
// 计算两个标准时间的时间差值
export const formatDuration = (startTm, endTm) => {
if (endTm === null || startTm === null) return "00:00:00";
// 解析时间字符串为时间戳(毫秒)
const startTime = new Date(startTm).getTime();
const endTime = new Date(endTm).getTime();
// 计算时间差(秒),确保为正数
const seconds = Math.abs(Math.floor((endTime - startTime) / 1000));
// 计算时、分、秒
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const remainingSeconds = seconds % 60;
// 补零格式化函数(统一处理所有单位)
const formatNumber = (num) => num.toString().padStart(2, '0');
// 小时、分钟、秒均保持两位数格式
return `${formatNumber(hours)}:${formatNumber(minutes)}:${formatNumber(remainingSeconds)}`;
};
// 秒数转换成小时,带小数点的小时
export const secondsToHours = (seconds) => {
if (typeof seconds === 'number') {
// 转换为小时并保留一位小数
return (seconds / 3600).toFixed(1);
}
return '0.0';
};
export function useItemList(fetchFunction, item) {
const status = ref('loadmore'); // loadmore - 加载前,loading - 加载中,nomore - 没有数据
const limit = 5;
const page = ref(0);
const total = ref(-1);
const data_list = reactive([]);
const show_list_state = ref(false);
const list_div_height = computed(() => {
const statusHeight = status.value === 'nomore' ? 0 : 100;
if (!show_list_state.value) {
return '0';
} else if (status.value === 'loading') {
return data_list.length * 136 + statusHeight + 'rpx';
} else {
return data_list.length * 136 + statusHeight + 'rpx';
}
});
// 点击获取详情
const show_list_click = (num = -1) => {
if (num === 0) return;
show_list_state.value = !show_list_state.value;
if (show_list_state.value) {
nextTick(() => {
getData('first');
});
}
};
const getData = (from = '') => {
if (from === 'first') {
if (total.value !== -1) {
return;
}
status.value = 'loading';
total.value = -1;
page.value = 1;
data_list.length = 0;
} else {
if (status.value !== 'loadmore' && status.value !== '') return;
status.value = 'loading';
page.value++;
}
fetchFunction({
page: page.value,
limit: limit
})
.then((res) => {
total.value = res.total;
data_list.push(...res.body);
})
.finally(() => {
if (data_list.length >= total.value) {
status.value = 'nomore';
} else {
status.value = 'loadmore';
}
});
};
const clear = () => {
total.value = -1
data_list.length = 0
show_list_state.value = false
}
return {
status,
data_list,
show_list_state,
list_div_height,
show_list_click,
getData,
clear
};
}
+10 -1
View File
@@ -116,6 +116,15 @@
"titleNView": false
}
}
}
},
{
"path": "pages/traRecord/index",
"style": {
"navigationBarTitleText": "陪练记录页",
"app-plus": {
"titleNView": false
}
}
},
]
}