开发代码

This commit is contained in:
田岩
2025-11-17 08:50:03 +08:00
43 changed files with 4989 additions and 2148 deletions
+7 -5
View File
@@ -4,13 +4,14 @@ ENV = 'development'
# VITE_APP_BASE_API_Url = 'https://aits.jlbank.com.cn:7001'
# VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7001'
VITE_APP_BASE_API_Url = 'http://103.3.121.66:7001'
VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7001'
# VITE_APP_BASE_API_Url = 'http://192.168.247.200'
# VITE_APP_BASE_API_Url = 'http://aitscdn.jlbank.com.cn:7001'
# VITE_APP_BASE_API_Url = 'http://192.168.108.129'
# dev
#VITE_APP_BASE_API_Url = 'http://25.18.122.65:7001'
# VITE_APP_BASE_API_Url = 'http://25.18.122.65:7001'
# sit
# VITE_APP_BASE_API_Url = 'http://25.18.122.91:9786'
@@ -20,13 +21,14 @@ VITE_APP_BASE_API_Url = 'http://103.3.121.66:7001'
# DEV
# app入口
#VITE_APP_BASE_API_Url = 'http://25.18.122.65:7001'
# VITE_APP_BASE_API_Url = 'http://25.16.122.91:9786'
# VITE_APP_BASE_API_Url = 'http://25.18.122.66:9786'
# h5专用地址x2
# VITE_APP_BASE_H5_API_Url = 'http://25.18.122.65:7001'
# VITE_APP_BASE_H5_API_Url = 'http://25.18.122.91:9786'
#VITE_APP_BASE_H5_API_Url = 'http://25.18.122.91:9786'
# VITE_APP_BASE_H5_API_Url = 'http://25.18.122.78:9786'
# VITE_APP_BASE_H5_API_Url_TRAAPP = 'http://192.168.247.200'
# VITE_APP_BASE_H5_API_Url_TRASTUDY = 'http://25.64.32.154:9602'
+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'
+15
View File
@@ -169,6 +169,21 @@ export function downloadVoiceFile(url) {
});
});
}
// 获取音频 通用参数
export function downloadVoiceParams(url) {
const token = getToken();
const header = {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
};
if (token)
header['summary'] = token
let baseUrl = get_base_url(url);
return {
url: `${baseUrl}${url}`,
header,
timeout: 180000,
}
}
export function getAudioByText(data) {
+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
});
};
+40
View File
@@ -41,4 +41,44 @@ export const queryTraPartnerCharacterInfoList = (data) => {
method: 'post',
data
});
};
// 陪练角色详情 + 对话详情 /traPartnerCharacterInfo/queryTraPartnerCharacterInfoById
export const queryTraPartnerCharacterInfoById = (data) => {
return request({
url: base_url + '/traPartnerCharacterInfo/queryTraPartnerCharacterInfoById',
method: 'post',
data
});
};
// 结束陪练 /trapractice/traPartnerChatReport/partnerChatReportTrigger?execId=
export const partnerChatReportTrigger = (data) => {
return request({
url: base_url + '/traPartnerChatReport/partnerChatReportTrigger',
method: 'post',
data
});
};
// 问答提示/trapractice/traPartnerChat/partnerChatPrompt
export const partnerChatPrompt = (data) => {
return request({
url: base_url + '/traPartnerChat/partnerChatPrompt',
method: 'post',
data
});
};
// 获取报告traPartnerChatReport/partnerChatReport
export const partnerChatReport = (data) => {
return request({
url: base_url + '/traPartnerChatReport/partnerChatReport',
method: 'post',
data
});
};
// 反馈
export const insertTraPartnerFeedback = (data) => {
return request({
url: base_url + '/traPartnerFeedback/insertTraPartnerFeedback',
method: 'post',
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
});
};
+61
View File
@@ -1,5 +1,7 @@
import { get_base_url } from '@/api/request';
// #ifdef APP-PLUS
import showDialog from '@/common/dialogMessage'
import common from '@/common/common'
// #endif
function msg(title, duration = 1000) {
uni.showToast({
@@ -277,6 +279,65 @@ export function setupKeyboardHeightListener(updateCallback) {
}
export const previewFileFnc = ({fileId, getFileSrc, fileName}) => {
let userInfo = getUserInfo()
let userParams = '/' + (userInfo?.loginName??'')
if (getPhoneEnvBool('AND')) {
common.navigateTo('/pages/index/preview?fileId=' + fileId +'&fileApiSrc=' + getFileSrc);
} else {
if (isDownloading.value) return
const base_url = get_base_url(getFileSrc);
let fileUrl = base_url + getFileSrc + fileId + userParams;
const localFilePath = '_doc/pdf_view/' + fileName + '.pdf';
isDownloading.value = true
downloadFileFnc(fileUrl, localFilePath).then((res) => {
uni.openDocument({
filePath: res,
showMenu: true,
success: function() {
console.log('打开文档成功');
}
})
}).finally(() => {
setTimeout(() => {
isDownloading.value = false
}, 1000)
});
}
}
const downloadFileFnc = async (fileUrl, localFilePath) => {
try {
//#ifdef APP-PLUS
return new Promise((resolve, reject) => {
console.log('开始下载:', fileUrl);
const dtask = plus.downloader.createDownload(
fileUrl, {
filename: localFilePath
},
(d, status) => {
if (status === 200) {
console.log('保存路径:', d.filename);
resolve(d.filename);
} else {
console.error('文件下载失败:', status);
reject(new Error(`下载失败,状态码: ${status}`));
}
}
);
dtask.start();
});
// #endif
//#ifdef H5
return new Promise((resolve, reject) => {
resolve();
});
// #endif
} catch (error) {
console.error('创建目录失败:', error);
throw error;
}
};
export default {
getPageCache,
+9
View File
@@ -510,6 +510,15 @@
"titleNView": false
}
}
},
{
"path": "pages/traRecord/index",
"style": {
"navigationBarTitleText": "陪练记录页",
"app-plus": {
"titleNView": false
}
}
}
],
"tabBar": {
+5 -6
View File
@@ -15,15 +15,15 @@
</view>
<view class="title" v-if="fromType === 'tra'">
<image class="title-icon icon-tra" src="@/static/images/sparring/den-icon.png" alt=""> </image>
维度分析22
维度分析
</view>
<view class="total-score" v-if="fromType === 'crs'">
<view class="score-num">{{ score }}</view>
<image class="total-score-icon" src="@/static/images/course/score.png" alt=""></image>
</view>
<view v-for="(item,index) in dataList" :class="['score-item', 'score-item'+(index+1)]">
<view class="score-item-name">维度{{ item }}</view>
<view class="score-item-score">{{ item }}</view>
<view class="score-item-name">{{ item.gradeDimensName || item.gradeDimens }}</view>
<view class="score-item-score">{{ item.anlyGrade }}</view>
</view>
</view>
</template>
@@ -31,7 +31,7 @@
export default{
props:{
dataList:{
default:() => [10,20,30,40,50,60,78],
default:() => [],
type: Array
},
score:{
@@ -54,7 +54,7 @@
},
computed:{
dataValue(){
return this.dataList//.map(t=>t.anlyGrade)
return this.dataList.map(t=>t.anlyGrade)
},
chartWidth(){
return this.size + 'rpx'
@@ -119,7 +119,6 @@
dataY: dataY + '%'
});
}
console.log(vertices)
return vertices;
},
+5 -6
View File
@@ -15,15 +15,15 @@
</view>
<view class="title" v-if="fromType === 'tra'">
<image class="title-icon icon-tra" src="@/static/images/sparring/den-icon.png" alt=""> </image>
维度分析22
维度分析
</view>
<view class="total-score" v-if="fromType === 'crs'">
<view class="score-num">{{ score }}</view>
<image class="total-score-icon" src="@/static/images/course/score.png" alt=""></image>
</view>
<view v-for="(item,index) in dataList" :class="['score-item', 'score-item'+(index+1)]">
<view class="score-item-name">维度维{{ item }}</view>
<view class="score-item-score">{{ item }}</view>
<view class="score-item-name">{{ item.gradeDimensName || item.gradeDimens }}</view>
<view class="score-item-score">{{ item.anlyGrade }}</view>
</view>
</view>
</template>
@@ -31,7 +31,7 @@
export default{
props:{
dataList:{
default:() => [20,30,40,50,60,78],
default:() => [],
type: Array
},
score:{
@@ -54,7 +54,7 @@
},
computed:{
dataValue(){
return this.dataList//.map(t=>t.anlyGrade)
return this.dataList.map(t=>t.anlyGrade)
},
chartWidth(){
return this.size + 'rpx'
@@ -119,7 +119,6 @@
dataY: dataY + '%'
});
}
console.log(vertices)
return vertices;
},
+73 -13
View File
@@ -220,6 +220,7 @@
import VideoPreview from '@/components/image-preview/video-preview.vue';
import {
downloadVoiceFile,
downloadVoiceParams,
uploadVoiceFile,
getAudioByText
} from '@/api/common.js';
@@ -238,7 +239,8 @@
useStorageStore
} from '@/store/storage.js';
import {
onUnload
onUnload,
onHide
} from '@dcloudio/uni-app';
import Subject from '@/components/examination/subject.vue';
@@ -246,6 +248,7 @@
const storageStore = useStorageStore();
const audioCacheObj = computed(() => storageStore.audioObj);
const isGettingId = computed(() => storageStore.getGettingVoiceId);
const props = defineProps({
type: {
default: 0, // 1 userAnswer 0: AIAnswer
@@ -434,12 +437,25 @@
initVoice();
});
onUnload(() => {
pauseVoice();
// pauseVoice();
console.log('onUnload')
talkVoiceObj.value?.destroy()
if(downloadFnc.value && downloadFnc.value.abord) downloadFnc.value?.abord()
downloadFnc.value = null
});
onHide(() => {
console.log('onHide')
voiceLoading.value = false;
if(downloadFnc.value && downloadFnc.value.abord) downloadFnc.value?.abord()
downloadFnc.value = null
});
let prevTime = -1;
let pauseTime = -1;
const startPlay = ref(false)
const voiceIsPlaying = ref(false)
talkVoiceObj.value.onEnded(() => {
console.log('onEnded');
voiceIsPlaying.value = false
if(props.activeVoiceId){
emit('changePlay', '');
}
@@ -449,6 +465,8 @@
});
talkVoiceObj.value.onStop(() => {
console.log('onStop');
voiceLoading.value = false;
voiceIsPlaying.value = false
if(props.activeVoiceId){
emit('changePlay', '');
}
@@ -457,13 +475,22 @@
talkVoiceObj.value.src = '';
});
talkVoiceObj.value.onPause(() => {
voiceIsPlaying.value = false
voiceLoading.value = false;
prevTime = talkVoiceObj.value.currentTime;
pauseTime = prevTime
if(downloadFnc.value && downloadFnc.value.abord) downloadFnc.value?.abord()
downloadFnc.value = null
});
talkVoiceObj.value.onPlay(() => {
console.log('onPlay');
prevTime = talkVoiceObj.value.currentTime;
pauseTime = prevTime
startPlay.value = true
voiceIsPlaying.value = true
setTimeout(()=> {
startPlay.value= false
},300)
emit('changePlay', unref(dataInfo)?.id);
});
@@ -475,7 +502,7 @@
prevTime = talkVoiceObj.value.currentTime;
}
} else {
if (props.isPlaying && (talkVoiceObj.value.currentTime > 0) && props.activeVoiceId) {
if (props.isPlaying && (talkVoiceObj.value.currentTime > 0) && props.activeVoiceId && !startPlay.value) {
console.log(pauseTime , prevTime)
emit('changePlay', '');
}
@@ -491,7 +518,7 @@
// emit('changePlay', '');
});
const pauseVoice = () => {
console.log('手动')
voiceLoading.value = false;
if (!talkVoiceObj.value) return;
talkVoiceObj.value.pause();
emit('changePlay', '');
@@ -510,7 +537,10 @@
playVoice();
}
};
const downloadFnc = ref(null)
const playVoice = (readType = 'pgth') => {
console.log('playVoice11111111')
if(voiceIsPlaying.value) return
let _content = showContent.value;
if (!_content) {
common.msg('没有可以播放的文字信息!');
@@ -518,15 +548,16 @@
}
setTimeout(() => {
emit('changePlay', unref(dataInfo)?.id);
console.log(1)
// readType 阅读内容类型pgrh段落/qns问题
if (talkVoiceObj.value.src && talkVoiceObj.value.src === itemVoice.value) {
if(voiceIsPlaying.value) return
talkVoiceObj.value.play();
return;
}
talkVoiceObj.value.src = '';
if (itemVoice.value) {
setTimeout(() => {
if(voiceIsPlaying.value) return
talkVoiceObj.value.src = itemVoice.value;
talkVoiceObj.value.play();
}, 100);
@@ -578,24 +609,53 @@
if (url) {
voiceLoading.value = false;
itemVoice.value = url;
if(voiceIsPlaying.value) return
talkVoiceObj.value.src = url;
talkVoiceObj.value.play();
} else {
downloadVoiceFile(_url)
.then((res) => {
storageStore.setGettingVoiceId(unref(dataInfo)?.id);
let _paramsObj = downloadVoiceParams(_url)
downloadFnc.value = uni.downloadFile({
..._paramsObj,
success(res) {
if (res?.data) {
common.msg(res.data.message);
return;
}
voiceLoading.value = false;
itemVoice.value = res.tempFilePath;
if(voiceIsPlaying.value) return
talkVoiceObj.value.src = res.tempFilePath;
talkVoiceObj.value.play();
storageStore.setStorage('audio', _url + _params.teachNo, itemVoice.value);
})
.catch((err) => {
voiceLoading.value = false;
});
// 判斷是否在當前頁面
if(unref(isGettingId) === unref(dataInfo)?.id){
talkVoiceObj.value.play();
storageStore.setStorage('audio', _url + _params.teachNo, itemVoice.value);
}
},
complete() {
voiceLoading.value = false;
downloadFnc.value = null
},
});
// downloadVoiceFile(_url)
// .then((res) => {
// if (res?.data) {
// common.msg(res.data.message);
// return;
// }
// voiceLoading.value = false;
// if()
// itemVoice.value = res.tempFilePath;
// talkVoiceObj.value.src = res.tempFilePath;
// // 判斷是否在當前頁面
// if(unref(isGettingId) === unref(dataInfo)?.id){
// talkVoiceObj.value.play();
// storageStore.setStorage('audio', _url + _params.teachNo, itemVoice.value);
// }
// })
// .catch((err) => {
// voiceLoading.value = false;
// });
}
});
+99 -93
View File
@@ -1,54 +1,24 @@
<template>
<view class="talk-btns" ref="talkBtnRef">
<view
class="touch-btn"
@touchstart="getRecordPermission"
@touchend="hideOverlayTalking"
@touchmove="handleMove"
v-if="!keyboardIsShow"
>
<view class="touch-btn" @touchstart="getRecordPermission" @touchend="hideOverlayTalking" @touchmove="handleMove"
v-if="!keyboardIsShow">
按住说话
</view>
<view class="touch-btn" v-if="keyboardIsShow">
<uv-input
class="input-box"
ref="inputRef"
v-model="answerValue"
:maxlength="props.textValueLength"
:adjustPosition="adjustPosition"
placeholderStyle="color:#999999;font-size:26rpx"
placeholder="请输入文字"
></uv-input>
<uv-input class="input-box" ref="inputRef" v-model="answerValue" :maxlength="props.textValueLength"
:adjustPosition="adjustPosition" placeholderStyle="color:#999999;font-size:26rpx"
placeholder="请输入文字"></uv-input>
</view>
<view class="talk-btn-box" v-show="!onlyVoice">
<image
class="talk-btn-icon"
v-show="!keyboardIsShow"
src="/src/static/images/course/jianpan.png"
mode=""
@click="changeBtn"
></image>
<image
class="talk-btn-icon"
v-show="keyboardIsShow && !answerValue"
src="/src/static/images/course/record.png"
mode=""
@click="changeBtn"
></image>
<image
class="talk-btn-icon"
v-show="keyboardIsShow && !!answerValue"
src="/src/static/images/course/send.png"
mode=""
@click.stop="showKeyboard"
></image>
<image class="talk-btn-icon" v-show="!keyboardIsShow" src="/src/static/images/course/jianpan.png" mode=""
@click="changeBtn"></image>
<image class="talk-btn-icon" v-show="keyboardIsShow && !answerValue" src="/src/static/images/course/record.png"
mode="" @click="changeBtn"></image>
<image class="talk-btn-icon" v-show="keyboardIsShow && !!answerValue" src="/src/static/images/course/send.png"
mode="" @click.stop="showKeyboard"></image>
</view>
<uni-popup
ref="talkModelRef"
type="bottom"
@click="showTalkingModel = false"
mask-background-color="rgba(0,0,0,0.9)"
>
<uni-popup ref="talkModelRef" type="bottom" @click="showTalkingModel = false"
mask-background-color="rgba(0,0,0,0.9)">
<view class="overlay-box">
<view class="circle-bg-box">
<view class="icon-box">
@@ -72,11 +42,25 @@
</template>
<script setup>
import { toRefs, ref, computed, unref, onMounted, onUnmounted, nextTick, watch, getCurrentInstance } from 'vue';
import {
toRefs,
ref,
computed,
unref,
onMounted,
onUnmounted,
nextTick,
watch,
getCurrentInstance
} from 'vue';
import common from '../../../common/common';
import permission from '@/common/permission.js';
import { getPhoneEnvBool } from '@/common/common.js';
import { useStorageStore } from '@/store/storage.js';
import {
getPhoneEnvBool
} from '@/common/common.js';
import {
useStorageStore
} from '@/store/storage.js';
const storageStore = useStorageStore();
const proxy = getCurrentInstance();
@@ -115,11 +99,16 @@
type: Boolean
},
formatType: {
default: 'mp3',// 生成语音文件类型
default: 'mp3', // 生成语音文件类型
type: String
},
});
const { isDisabled, onlyVoice, adjustPosition, formatType } = toRefs(props);
const {
isDisabled,
onlyVoice,
adjustPosition,
formatType
} = toRefs(props);
const isLoadingState = computed(() => props.isLoading);
const showTalkingModel = ref(false);
@@ -134,24 +123,21 @@
);
watch(
() => props.isLoading,
() => {},
{
() => {}, {
deep: true,
immediate: true
}
);
watch(
() => props.canAnswer,
() => {},
{
() => {}, {
deep: true,
immediate: true
}
);
watch(
() => props.onlyVoice,
() => {},
{
() => {}, {
deep: true,
immediate: true
}
@@ -162,8 +148,7 @@
if (val) {
keyboardIsShow.value = false;
}
},
{
}, {
deep: true,
immediate: true
}
@@ -265,39 +250,7 @@
// #endif
onMounted(async () => {
initRecord?.();
//#ifdef APP-PLUS
// 安卓端
if (IsAndEnv) {
let _recordPerm = await permission.requestAndroidPermission('android.permission.RECORD_AUDIO');
if (_recordPerm !== 1) {
common.show('提示信息', '当前页面需要使用的录音权限,是否前往开启?', true, '取消', '确定').then((res) => {
if (res) {
// 去开启权限
permission.gotoAppPermissionSetting();
} else {
// 取消
common.navigateBack();
}
});
}
} else {
let _recordPerm = uni.getAppAuthorizeSetting();
let _state = _recordPerm.microphoneAuthorized;
if (_state === 'denied') {
common.show('提示信息', '当前页面需要使用的录音权限,是否前往开启?', true, '取消', '确定').then((res) => {
if (res) {
// 去开启权限
uni.openAppAuthorizeSetting();
} else {
// 取消
common.navigateBack();
}
});
} else if (_state === 'not determined') {
// startRecord()
}
}
// #endif
});
// 录音结束
@@ -317,6 +270,7 @@
});
const recNum = ref(1);
const activeNum = ref(1);
const hasPermission = ref(false)
const getRecordPermission = (obj) => {
if (isDisabled.value) return;
@@ -327,8 +281,59 @@
}, 500);
return;
}
showOverlayTalking(obj);
if(!hasPermission.value){
let _res = getPermission()
if (_res) {
hasPermission.value = true
showOverlayTalking(obj);
}
}else{
showOverlayTalking(obj);
}
};
const getPermission = async () => {
//#ifdef APP-PLUS
// 安卓端
if (IsAndEnv) {
let _recordPerm = await permission.requestAndroidPermission('android.permission.RECORD_AUDIO');
if (_recordPerm !== 1) {
common.show('提示信息', '当前页面需要使用的录音权限,是否前往开启?', true, '取消', '确定').then((res) => {
if (res) {
// 去开启权限
permission.gotoAppPermissionSetting();
} else {
// 取消
common.navigateBack();
}
});
return false
} else {
return true
}
} else {
let _recordPerm = uni.getAppAuthorizeSetting();
let _state = _recordPerm.microphoneAuthorized;
if (_state === 'denied') {
common.show('提示信息', '当前页面需要使用的录音权限,是否前往开启?', true, '取消', '确定').then((res) => {
if (res) {
// 去开启权限
uni.openAppAuthorizeSetting();
} else {
// 取消
common.navigateBack();
}
});
return false
} else if (_state === 'not determined') {
// startRecord()
return true
}
}
// #endif
//#ifndef APP-PLUS
return true
// #endif
}
const isMoving = ref(false);
const isTalking = ref(false);
const showOverlayTalking = (obj) => {
@@ -366,8 +371,8 @@
startRecord();
// #endif
setTimeout(() => {
console.log(_recnum,recNum.value)
if(_recnum === recNum.value) {
console.log(_recnum, recNum.value)
if (_recnum === recNum.value) {
hideOverlayTalking(recNum.value);
}
}, 59 * 1010);
@@ -440,6 +445,7 @@
padding: 25rpx 30rpx 0;
position: relative;
z-index: v-bind(btnZindex);
.touch-btn {
height: 92rpx;
box-shadow: 0 8rpx 20rpx 0 rgba(0, 0, 0, 0.06);
@@ -621,4 +627,4 @@
::v-deep .uv-input__content__field-wrapper__field {
padding-right: 60rpx;
}
</style>
</style>
+1556 -1585
View File
File diff suppressed because it is too large Load Diff
@@ -40,7 +40,7 @@
</view>
<view class="list_small_item">
<image src="@/static/images/courseRecord/duration.png" class="icon_img"></image>
<view class="prompt">练习时长{{ formatDuration(item_list.exrStartTm, item_list.exrEndTm) }}</view>
<view class="prompt">练习时长{{ item_list.stdtTmLenS || '--' }}</view>
</view>
</view>
<view class="fl1"></view>
@@ -61,7 +61,7 @@
import { computed, ref, reactive } from 'vue';
import common from '@/common/common';
import { queryPracticeHis } from '@/api/courseRecord.js';
import { useItemList, formatDuration } from '../useItemList.js';
import { useItemList } from '../useItemList.js';
const props = defineProps({
item: {
@@ -40,7 +40,7 @@
</view>
<view class="list_small_item">
<image src="@/static/images/courseRecord/duration.png" class="icon_img"></image>
<view class="prompt">学习时长{{ formatDuration(item_list.startTm, item_list.endTm) }}</view>
<view class="prompt">学习时长{{ item_list.stdtTmLenS || '--' }}</view>
</view>
</view>
<view class="fl1"></view>
@@ -61,7 +61,7 @@
import { computed } from 'vue';
import common from '@/common/common';
import { queryStdyBatchByCrsId } from '@/api/courseRecord.js';
import { useItemList, formatDuration, secondsToHours } from '../useItemList.js';
import { useItemList, secondsToHours } from '../useItemList.js';
const props = defineProps({
item: {
type: Object,
+19 -19
View File
@@ -7,25 +7,25 @@ import {
// 计算两个标准时间的时间差值
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 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) => {
+1
View File
@@ -1114,6 +1114,7 @@
.talking-list {
height: calc(100% - 772rpx);
min-height: 300rpx;
padding: 12rpx 24rpx;
box-sizing: border-box;
min-height: 300rpx;
+8 -4
View File
@@ -26,6 +26,7 @@ export default {
data() {
return {
fileId: '',
fileApiSrc: '/traask/traFilePreview/',
showUrl: '',
// viewerUrl: '/static/pdf-view/web/viewer.html',
viewerUrl: '/static/view-pdf/pdf.html',
@@ -36,6 +37,9 @@ export default {
},
onLoad(params) {
this.fileId = params.fileId || ''
if(params.fileApiSrc) {
this.fileApiSrc = params.fileApiSrc
}
const userInfo = getUserInfo();
this.userParams = '/' + (userInfo?.loginName??'')
},
@@ -45,8 +49,8 @@ export default {
fileId: this.fileId
})
}else{
const base_url = get_base_url('/traask/traFilePreview/');
let fileUrl = base_url + '/traask/traFilePreview/' + this.fileId + this.userParams;
const base_url = get_base_url(this.fileApiSrc);
let fileUrl = base_url + this.fileApiSrc + this.fileId + this.userParams;
this.showUrl = fileUrl
console.log(this.showUrl,111)
}
@@ -77,9 +81,9 @@ export default {
common.navigateBack()
},
openPdfFn(data) {
const base_url = get_base_url('/traask/traFilePreview/')
const base_url = get_base_url(this.fileApiSrc)
let _id = data?.fileId
let fileUrl = base_url + '/traask/traFilePreview/' + _id + this.userParams
let fileUrl = base_url + this.fileApiSrc + _id + this.userParams
this.showUrl = this.viewerUrl + '?url=' + fileUrl
console.log(this.showUrl)
this.showView = true
+1 -1
View File
@@ -354,7 +354,7 @@
.main_div {
display: flex;
flex-direction: column;
overflow: hidden;
// overflow: hidden;
max-height: 100vh;
}
</style>
+1 -1
View File
@@ -98,7 +98,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>
+3 -1
View File
@@ -68,6 +68,7 @@
} from "@/api/sparring.js"
import CardSwiper from './components/card-swiper.vue';
const traId = ref('')
const talkingType = ref('')
const detailInfo = ref({})
const roleList = ref([])
const currentIndex = ref(0)
@@ -92,10 +93,11 @@
}
const toTip = () => {
common.navigateTo('/pages/sparring/tip?traId=' + traId.value + '&roleId=' + activeRole.value.chaId)
common.navigateTo('/pages/sparring/tip?traId=' + traId.value + '&chaId=' + activeRole.value.chaId + '&type=' + talkingType.value)
}
onLoad((params) => {
traId.value = params.traId;
talkingType.value = params.type
});
onMounted(() => {
getDetailInfo()
+183 -42
View File
@@ -1,20 +1,23 @@
<template>
<view class="answer-content" v-if="props.isLoading">
<view class="answer-content" v-if="props.dataInfo.isLoading">
<view class="talking-cont">
<CommonLoading color="#06f" />
</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.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>
@@ -35,11 +38,24 @@
onMounted,
computed,
ref,
unref,
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({
@@ -47,9 +63,9 @@
default: () => ({}),
type: Object
},
isLoading: {
default: false,
type: Boolean
roleInfo: {
default: () => ({}),
type: Object
},
isAnswering: {
default: false,
@@ -58,31 +74,31 @@
isLast: {
default: false,
type: Boolean
}
},
activeVoiceId: {
default: '',
type: String
},
isStudy: {
default: true,
type: Boolean
},
})
watch(
() => props.dataInfo,
() => {}, {
deep: true,
immediate: true
}
)
watch(
() => props.isLoading,
() => {}, {
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 => {
@@ -110,6 +126,23 @@
const previewCrs = item => {
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) {
@@ -117,25 +150,92 @@
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(() => {
emit('changePlay', '')
talkVoiceObj.value.src = ''
voiceIsPlaying.value = false
})
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
})
}
const pauseVoice = () => {
if (!talkVoiceObj.value) return
talkVoiceObj.value.pause()
@@ -149,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;
@@ -170,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;
}
+16 -18
View File
@@ -37,8 +37,8 @@
nextTick
} from 'vue';
import {
insertTraAskFeedback
} from "@/api/dialog.js"
insertTraPartnerFeedback
} from "@/api/sparring.js"
import {
optionErrorIcon
} from '@/common/imgSvg'
@@ -47,10 +47,6 @@
const emit = defineEmits(['feedbackAnswer'])
const feedback_popup_bottom = ref('0px') // 反馈框下距离
const feedbackPopupRef = ref(null)
// FM("01", "虚假信息"),
// AE("02", "回答不准确"),
// NH("03", "没有帮助"),
// OTHER("04", "其他"),
const feedbackTypeList = reactive([{
value: "01",
label: "场景问题",
@@ -59,11 +55,13 @@
label: "功能问题",
}, {
value: "03",
label: "没有帮助",
}, {
value: "04",
label: "其他",
}])
label: "陪练内容",
},
// {
// value: "04",
// label: "其他",
// },
])
const feedbackInfo = ref({
type: [], // 反馈类型
text: '', // 反馈内容
@@ -86,14 +84,13 @@
common.msg('反馈内容不能为空!')
return
}
// todo 待接口
// todo 待接口refineId=PARTNERINFO02506403215920251024093412000000348&fbkTyp=01&fbkContent=测试
const params = JSON.parse(JSON.stringify({
askId: answerData.value.seqNo,
traId: answerData.value.traId,
fbkTyp: feedbackInfo.value.type.join(','),
reqBatch: answerData.value.reqBatch,
fbkContent: feedbackInfo.value.text
}))
insertTraAskFeedback(params).then(res=>{
insertTraPartnerFeedback(params).then(res=>{
common.msg('反馈提交成功!')
feedbackInfo.value = {
type: [], // 反馈类型
@@ -195,7 +192,7 @@
.feedback_item {
float: left;
width: calc(50% - 20rpx);
width: calc(33.3% - 27rpx);
border: 2rpx solid #F0F0F0;
background-color: #F0F0F0;
border-radius: 16rpx;
@@ -206,8 +203,9 @@
color: #000000;
text-align: center;
margin-bottom: 20rpx;
&:nth-child(2n){
margin-left: 40rpx;
margin-right: 40rpx;
&:nth-child(3n){
margin-right: 0rpx;
}
}
}
@@ -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">
@@ -22,7 +22,7 @@
<ImagePreview class="img-box" :src="itemInfo.ossAddr" :isCache="true" width="225" height="170" mode="scaleToFill">
</ImagePreview>
<view class="item-title">{{itemInfo.traName}}</view>
<scroll-view class="item-tags" :scroll-x="true">
<scroll-view class="item-tags" :scroll-x="true" :show-scrollbar="false">
<view class="item-tag-view">
<view :class="['item-tag', this.activeTag.includes(item.tagId)?'active-tag':'']" v-for="item in tagsShowList">
{{item.tagName}}</view>
@@ -70,9 +70,9 @@
},
computed: {
tagsShowList() {
if (!this.activeTag) return this.itemInfo.tagCataList || []
let _arr = (this.itemInfo.tagCataList || []).filter(t => this.activeTag.includes(t.tagId))
let _arr1 = (this.itemInfo.tagCataList || []).filter(t => !this.activeTag.includes(t.tagId))
if (!this.activeTag) return this.itemInfo.traPartnerTagList || []
let _arr = (this.itemInfo.traPartnerTagList || []).filter(t => this.activeTag.includes(t.tagId))
let _arr1 = (this.itemInfo.traPartnerTagList || []).filter(t => !this.activeTag.includes(t.tagId))
return _arr.concat(_arr1)
}
},
@@ -86,7 +86,7 @@
<style lang="scss" scoped>
.sparring-info {
padding:12rpx 15rpx 8rpx;
padding:0rpx 15rpx 24rpx;
overflow: hidden;
.item-title {
white-space: nowrap;
+85 -11
View File
@@ -1,31 +1,41 @@
<template>
<view class="talking-item">
<!-- 头像信息 -->
<view class="user-info">
<view class="user-info" v-if="talkingType === 'answer'">
<view class="avatar-box">
<ImagePreview class="img-box" :imgId="roleInfo?.ossAddr"></ImagePreview>
<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"
<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"
:isLoading="!!props.talkingInfo.isLoading" :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'">
<view class="talking-tips-btn" @click="getTips()">回答提示</view>
<view class="talking-tips-loading" v-if="loadingTips">
<CommonLoading color="#06f" />
</view>
<view class="talking-tips-cont" v-else>{{tipsContent}}</view>
</view>
</view>
</template>
<script setup>
import {
toRefs,
watch
watch,
ref
} from 'vue'
import questionVue from './question.vue';
import answerVue from './answer.vue';
import ImagePreview from '@/components/image-preview/image-preview.vue';
import { partnerChatPrompt } from "@/api/sparring.js"
const emit = defineEmits(['handleEvents'])
const props = defineProps({
@@ -33,6 +43,14 @@
default: 'question',
type: String
},
traId: {
default: '',
type: String
},
execId: {
default: '',
type: String
},
talkingInfo: {
default: () => ({}),
type: Object
@@ -63,8 +81,13 @@
immediate: true
})
const {
talkingType
talkingType,
talkingInfo,
roleInfo
} = toRefs(props)
const loadingTips = ref(false)
const tipsContent = ref('')
const handleEvents = (type, info = {}) => {
emit('handleEvents', {
type,
@@ -73,6 +96,21 @@
info
})
}
const getTips = () => {
loadingTips.value = true
handleEvents('scrollToBottom')
partnerChatPrompt({
execId: props.execId,
traId: props.traId,
content: props.talkingInfo.talkingText
}).then(res=>{
tipsContent.value = res.body
loadingTips.value = false
handleEvents('scrollToBottom')
}).catch(()=> {
loadingTips.value =false
})
}
const changePlay = (id) => {
emit('handleEvents', {
type: 'changePlay',
@@ -87,7 +125,8 @@
.talking-item {
overflow: hidden;
margin-top: 0rpx;
position: relative;
margin-bottom: 24rpx;
&+.talking-item {
margin-top: 30rpx;
}
@@ -99,16 +138,18 @@
width: 76rpx;
height: 76rpx;
border: 3rpx solid #fff;
background-color: #f1f1f1;
// background-color: #f1f1f1;
overflow: hidden;
border-radius: 10rpx;
margin-right: 16rpx;
margin-bottom: 16rpx;
.img-box {
width: 100%;
height: 100%;
width: 76rpx;
height: 76rpx;
overflow: hidden;
}
}
.ai-name {
@@ -124,5 +165,38 @@
}
}
}
.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>
@@ -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>
+519
View File
@@ -0,0 +1,519 @@
<template>
<view class="talk-btns" ref="talkBtnRef">
<view class="touch-btn" @touchstart="getRecordPermission" @touchend="hideOverlayTalking" @touchmove="handleMove">
<view class="center-text">
<image class="type-icon" src="@/static/images/sparring/talk-icon.png"></image>
{{'语音对话'}}
</view>
</view>
<uni-popup ref="talkModelRef" type="bottom" @click="showTalkingModel = false"
mask-background-color="rgba(0,0,0,0.9)">
<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', isOut ? 'is-out' : '']">
<image src="@/static/images/course/voice-white.gif" mode="" class="gif-box"></image>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import {
toRefs,
ref,
computed,
unref,
onMounted,
onUnmounted,
nextTick,
watch,
getCurrentInstance
} from 'vue';
import common from '../../../common/common';
import permission from '@/common/permission.js';
import {
getPhoneEnvBool
} from '@/common/common.js';
import {
useStorageStore
} from '@/store/storage.js';
const storageStore = useStorageStore();
const proxy = getCurrentInstance();
const emit = defineEmits(['uploadVoice', 'startRecord']);
const props = defineProps({
isDisabled: {
default: true,
type: Boolean
},
isLoading: {
default: false,
type: Boolean
},
loadingText: {
default: '发送回答中,请稍后再试!',
type: String
},
formatType: {
default: 'mp3', // 生成语音文件类型
type: String
},
});
const {
isDisabled,
onlyVoice,
formatType
} = toRefs(props);
const isLoadingState = computed(() => props.isLoading);
const showTalkingModel = ref(false);
const keyboardIsShow = ref(false);
const talkModelRef = ref();
storageStore.setTouchBtnZIndex(12);
const btnZindex = computed(() => storageStore.getTouchBtnZIndex);
watch(
() => showTalkingModel.value,
(val) => (val ? talkModelRef.value.open() : talkModelRef.value.close())
);
watch(
() => props.isLoading,
() => {}, {
deep: true,
immediate: true
}
);
watch(
() => props.isDisabled,
(val) => {
if (val) {
keyboardIsShow.value = false;
}
}, {
deep: true,
immediate: true
}
);
const answerValue = ref('');
const talkBtnRef = ref();
// 录音
const audioPath = ref('');
const audioObj = uni.createInnerAudioContext();
//#ifdef APP-PLUS
// app端
const recordObjCom = ref({});
const IsAndEnv = getPhoneEnvBool('AND');
const isStartNum = ref(0);
const isStopNum = ref(0);
const initRecord = () => {
recordObjCom.value = uni.getRecorderManager();
unref(recordObjCom)?.onStop((res, duration) => {
audioPath.value = res.tempFilePath;
if (!isOut.value) {
if (startFlag.value !== 2) {
closeModel(() => {
nextTick(() => {
common.msg('说话时间太短,没有听清!');
});
});
} else {
closeModel();
emit('uploadVoice', audioPath.value);
}
} else {
closeModel();
}
});
unref(recordObjCom).onStart(() => {
startFlag.value = 1;
setTimeout(() => {
startFlag.value = 2;
}, 1000);
let _recordPerm = uni.getAppAuthorizeSetting();
let _state = _recordPerm.microphoneAuthorized;
if (_state === 'authorized') {
if (isStopNum.value === isStartNum.value) {
stopRecord();
} else {
showTalkingModel.value = true;
}
}
});
};
const startFlag = ref(1);
const startRecord = () => {
isStartNum.value += 1;
unref(recordObjCom).start({
format: formatType.value
});
};
const stopRecord = () => {
isStopNum.value = isStartNum.value;
try {
if (unref(recordObjCom).stop) {
unref(recordObjCom).stop();
} else {
closeModel();
}
} catch {
closeModel();
}
};
const closeModel = (cb = () => {}) => {
isStopNum.value = isStartNum.value;
setTimeout(() => {
showTalkingModel.value = false;
cb && cb();
}, 350);
setTimeout(() => {
isTalking.value = false;
}, 800);
};
// #endif
//#ifndef APP-PLUS
const recordObjCom = ref({});
const initRecord = () => {};
// #endif
onMounted(async () => {
initRecord?.();
});
// 录音结束
// 记录 按下移动位置
const touchX = ref(0);
const touchY = ref(0);
const baseY = ref(0);
const windowInfo = uni.getWindowInfo();
// 计算是否超出按下盒子位置
const isOut = computed(() => {
let _btnHeight = (windowInfo.screenWidth / 750) * 234;
let moveVal = windowInfo.screenHeight - touchY.value > _btnHeight;
return moveVal;
});
const outScreen = computed(() => {
windowInfo.screenHeight - touchY.value <= 0;
});
const recNum = ref(1);
const activeNum = ref(1);
const hasPermission = ref(false)
const getRecordPermission = (obj) => {
if (isDisabled.value) return;
if (isTalking.value) {
common.msg('操作频繁,请稍后重试!');
setTimeout(() => {
isTalking.value = false;
}, 500);
return;
}
if (!hasPermission.value) {
let _res = getPermission()
if (_res) {
hasPermission.value = true
showOverlayTalking(obj);
}
} else {
showOverlayTalking(obj);
}
};
const getPermission = async () => {
//#ifdef APP-PLUS
// 安卓端
if (IsAndEnv) {
let _recordPerm = await permission.requestAndroidPermission('android.permission.RECORD_AUDIO');
if (_recordPerm !== 1) {
common.show('提示信息', '当前页面需要使用的录音权限,是否前往开启?', true, '取消', '确定').then((res) => {
if (res) {
// 去开启权限
permission.gotoAppPermissionSetting();
} else {
// 取消
common.navigateBack();
}
});
return false
} else {
return true
}
} else {
let _recordPerm = uni.getAppAuthorizeSetting();
let _state = _recordPerm.microphoneAuthorized;
if (_state === 'denied') {
common.show('提示信息', '当前页面需要使用的录音权限,是否前往开启?', true, '取消', '确定').then((res) => {
if (res) {
// 去开启权限
uni.openAppAuthorizeSetting();
} else {
// 取消
common.navigateBack();
}
});
return false
} else if (_state === 'not determined') {
// startRecord()
return true
}
}
// #endif
//#ifndef APP-PLUS
return true
// #endif
}
const isMoving = ref(false);
const isTalking = ref(false);
const showOverlayTalking = (obj) => {
if (showTalkingModel.value) return;
if (isLoadingState.value) {
uni.showToast({
title: props.loadingText,
icon: 'none',
duration: 1000
});
return;
}
isMoving.value = true;
isTalking.value = true;
emit('startRecord');
recNum.value += 1;
let _recnum = recNum.value
if (!isDisabled.value) {
touchX.value = obj.changedTouches[0].pageX;
touchY.value = obj.changedTouches[0].pageY;
baseY.value = obj.currentTarget.offsetTop;
//#ifdef APP-PLUS
// canStart.value = false
startRecord();
// #endif
setTimeout(() => {
console.log(_recnum, recNum.value)
if (_recnum === recNum.value) {
hideOverlayTalking(recNum.value);
}
}, 59 * 1010);
} else {
showTalkingModel.value = false;
uni.showToast({
title: '暂无问题需要回答!',
icon: 'none',
duration: 1000
});
}
};
const hideOverlayTalking = (obj) => {
isMoving.value = false;
if (!showTalkingModel.value) return;
//#ifdef APP-PLUS
stopRecord();
// #endif
};
const handleMove = (obj) => {
if (isMoving.value) {
touchX.value = obj.changedTouches[0].pageX;
touchY.value = obj.changedTouches[0].pageY;
if (outScreen.value) {
hideOverlayTalking(obj);
}
}
};
</script>
<style lang="scss" scoped>
.talk-btns {
height: 150rpx;
background-color: #fff;
padding: 25rpx 30rpx 0;
position: relative;
z-index: v-bind(btnZindex);
.touch-btn {
height: 92rpx;
// box-shadow: 0 8rpx 20rpx 0 rgba(0, 0, 0, 0.06);
text-align: center;
line-height: 92rpx;
background-color: #06f;
font-weight: 400;
position: relative;
;
color: #fff;
font-size: 29rpx;
border-radius: 23rpx;
.center-text {
height: 35rpx;
line-height: 35rpx;
width: 100%;
position: absolute;
top: 50%;
transform: translateY(-50%);
display: flex;
justify-content: center;
}
.type-icon {
width: 35rpx;
height: 35rpx;
margin-right: 10rpx;
}
}
}
.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;
border-radius: 32rpx;
position: relative;
&.is-out {
background-color: #ff3e49;
&::after {
background-color: #ff3e49;
}
}
.gif-box {
position: absolute;
width: 200rpx;
height: 80rpx;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
&::after {
content: '';
width: 44rpx;
height: 44rpx;
clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
background-color: #06f;
overflow: hidden;
position: absolute;
bottom: -22rpx;
left: 50%;
margin-left: -22rpx;
}
}
.white-bg-box {
height: 365rpx;
background-color: #fff;
overflow: hidden;
border-radius: 32rpx 32rpx 0 0;
padding: 58rpx 36rpx;
.box-title {
font-size: 32rpx;
font-weight: bold;
line-height: 46rpx;
margin-bottom: 46rpx;
}
.box-btns {
height: 46rpx;
line-height: 46rpx;
display: flex;
justify-content: space-between;
color: #999;
.is-active {
color: #06f;
}
}
}
}
::v-deep .uv-input__content__field-wrapper__field {
padding-right: 60rpx;
}
</style>
+57 -116
View File
@@ -12,21 +12,15 @@
<view class="body-top">
<view class="body-top-title">
{{detailInfo.traName}}
</view>
<view class="body-top-item">
已练习次数{{detailInfo.practiceTimes}}
</view>
<view class="body-top-item">
类型{{detailInfo.traInterTypName}}
</view>
<view class="body-top-item">
截止时间{{detailInfo.endTm}}
{{ detailInfo.traName }}
</view>
<view class="body-top-item"> 已练习次数{{ detailInfo.practiceTimes }} </view>
<view class="body-top-item"> 类型{{ detailInfo.traInterTypDesc }} </view>
<view class="body-top-item"> 截止时间{{ detailInfo.endTm }} </view>
<scroll-view class="body-top-tags" :scroll-x="true">
<view class="item-tag-view">
<view class="item-tag active-tag" v-for="item in tagsShowList">
{{item.tagName}}
<view class="item-tag active-tag" v-for="item in detailInfo.traPartnerTagList || []">
{{ item.tagName }}
</view>
</view>
</scroll-view>
@@ -37,7 +31,7 @@
陪练介绍
</view>
<view class="body-item-detail">
{{detailInfo.introduction}}
{{ detailInfo.introduction }}
</view>
</view>
<view class="body-item">
@@ -46,7 +40,7 @@
陪练目标
</view>
<view class="body-item-detail">
{{detailInfo.traTarget}}
{{ detailInfo.traTarget }}
</view>
</view>
<view class="body-item">
@@ -54,22 +48,22 @@
<image class="title-icon" src="@/static/images/sparring/detail-icon-3.png"></image>
陪练预计时长
</view>
<view class="body-item-detail">
{{detailInfo.traStartLimit+ ' ~ ' + detailInfo.traEndLimit}} 分钟
</view>
<view class="body-item-detail"> {{ detailInfo.traStartLimit + ' ~ ' + detailInfo.traEndLimit }} 分钟 </view>
</view>
<view class="body-item">
<view class="body-item" v-if="detailInfo.traPartnerDataVoList?.length > 0">
<view class="body-item-title">
<image class="title-icon" src="@/static/images/sparring/detail-icon-4.png"></image>
陪练参考资料
</view>
<view class="body-item-detail">
<view class="body-item-detail-file" v-for="item in 6" @click="previewFile(item)">
文件名称{{item}}.docx
</view>
<view class="body-item-detail-file" v-for="item in detailInfo.traPartnerDataVoList || []"
@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>
@@ -86,111 +80,58 @@
onShow,
onLoad,
onUnload
} from '@dcloudio/uni-app';
import common from '@/common/common';
import ImagePreview from '@/components/image-preview/image-preview.vue';
} from '@dcloudio/uni-app'
import common from '@/common/common'
import ImagePreview from '@/components/image-preview/image-preview.vue'
import {
queryTraPartnerInfoById
} from "@/api/sparring.js"
} from '@/api/sparring.js'
import {
getPhoneEnvBool
} from '@/common/common';
const traId = ref('')
const detailInfo = ref({})
const isDownloading = ref(false)
const tagsShowList = ref([{
tagName: '公金业务'
}, {
tagName: '初级'
}, {
tagName: '销售类'
}, {
tagName: '公金业务'
}, {
tagName: '初级'
}, {
tagName: '销售类'
}, {
tagName: '公金业务'
}, {
tagName: '初级'
}, {
tagName: '销售类'
}])
previewFileFnc
} from '@/common/common'
const traId = ref('')
const detailInfo = ref({
traPartnerDataVoList: []
})
const isLoading = ref(false)
const getDetailInfo = () => {
if (isLoading.value) return
isLoading.value = true
queryTraPartnerInfoById({
id: traId.value
}).then(res => {
console.log(res)
detailInfo.value = res.body || {}
traId: traId.value
})
.then(res => {
detailInfo.value = res.body || {}
})
.catch(err => {
console.log(err)
}).finally(() => {
isLoading.value = false
})
}
const previewFile = info => {
previewFileFnc({
fileId: info.dataId,
getFileSrc: '/trapractice/traFilePreview/',
fileName: info.dataName
})
}
const previewFile = (info) => {
if (getPhoneEnvBool('AND')) {
common.navigateTo('/pages/index/preview?fileId=' + info.docId);
} else {
if (isDownloading.value) return
const base_url = get_base_url('/traask/traFilePreview/');
let fileUrl = base_url + '/traask/traFilePreview/' + info.docId + userParams;
const localFilePath = '_doc/pdf_view/' + info.docName + '.pdf';
isDownloading.value = true
downloadFileFnc(fileUrl, localFilePath).then((res) => {
uni.openDocument({
filePath: res,
showMenu: true,
success: function() {
console.log('打开文档成功');
}
})
}).finally(() => {
setTimeout(() => {
isDownloading.value = false
}, 1000)
});
}
}
const downloadFileFnc = async (fileUrl, localFilePath) => {
try {
//#ifdef APP-PLUS
return new Promise((resolve, reject) => {
console.log('开始下载:', fileUrl);
const dtask = plus.downloader.createDownload(
fileUrl, {
filename: localFilePath
},
(d, status) => {
if (status === 200) {
console.log('保存路径:', d.filename);
resolve(d.filename);
} else {
console.error('文件下载失败:', status);
reject(new Error(`下载失败,状态码: ${status}`));
}
}
);
dtask.start();
});
// #endif
//#ifdef H5
return new Promise((resolve, reject) => {
resolve();
});
// #endif
} catch (error) {
console.error('创建目录失败:', error);
throw error;
}
};
const toChooseRole = (type) => {
const toChooseRole = type => {
common.navigateTo('/pages/sparring/chooseRole?traId=' + traId.value + '&type=' + type)
}
onLoad((params) => {
traId.value = params.traId;
});
const isPreview = ref(false)
onLoad(params => {
traId.value = params.traId
isPreview.value = params.isPreview === '1'
console.log(params)
})
onMounted(() => {
getDetailInfo()
})
onShow(() => {
getDetailInfo()
})
</script>
<style lang="scss" scoped>
@@ -230,7 +171,6 @@
margin: 0 auto;
border-radius: 23rpx;
overflow: hidden;
}
.top-title {
@@ -363,7 +303,8 @@
color: #06f;
font-size: 29rpx;
border-radius: 23rpx;
&:last-child{
&:last-child {
margin-left: 36rpx;
background: #06f;
color: #fff;
+2
View File
@@ -338,7 +338,9 @@
}
// 跳转 课程详情
const toSparringDetail = item => {
if(item.traId){
common.navigateTo('/pages/sparring/detail?traId=' + item.traId)
}
}
onMounted(() => {
queryTraPartnerCatelogList().then(res => {
+309 -75
View File
@@ -1,5 +1,9 @@
<template>
<view class="result-cont">
<view class="sparring-result-loading" v-show="showLoading">
<view class="loading-img">
</view>
</view>
<view class="top-box">
<view class="top-title">
<view class="back-icon" @click="common.navigateBack()"></view>
@@ -10,62 +14,58 @@
<view class="body-content">
<view class="body-top">
<view class="body-top-title">
{{'陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称'}}
{{ traInfo.traName}}
</view>
<view class="body-top-time">
陪练时长{{'12分 15秒'}}
陪练时长{{traTm}}
</view>
<view class="body-top-tip">陪练详情</view>
<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">
<view class="item-title">话术完整性 {{ 88 }}
<image class="item-title-icon" src="@/static/images/sparring/info-icon.png"></image>
<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> -->
</view>
<view class="item-cont">逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性</view>
<view class="item-cont">{{item.settDesc}}</view>
</view>
<view class="body-bottom-item">
<view class="item-title">话术完整性 {{ 88 }}
<image class="item-title-icon" src="@/static/images/sparring/info-icon.png"></image>
</view>
<view class="item-cont">逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性</view>
</view>
<view class="body-bottom-item">
<view class="item-title">话术完整性 {{ 88 }}
<image class="item-title-icon" src="@/static/images/sparring/info-icon.png"></image>
</view>
<view class="item-cont">逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性</view>
</view>
<view class="body-bottom-item">
<view class="item-title">话术完整性 {{ 88 }}
<image class="item-title-icon" src="@/static/images/sparring/info-icon.png"></image>
</view>
<view class="item-cont">逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性逻辑贯穿性</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">{{ 97 }}</view>
<view class="static-desc">革命尚未成功同志仍需努力</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 fromType="tra" />
<!-- <ChartSeven v-if="length === 7" 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" />
</view>
</view>
</view>
@@ -85,19 +85,28 @@
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 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()
const roleInfo = ref({})
const showAllTalk = ref(false)
const length = ref(6)
const tabList = ref([
{
const traInfo = ref({})
const traTm = ref('')
const activeVoiceTalkingItemId = ref('')
const tabList = ref([{
name: '全部',
value: ''
},
@@ -110,25 +119,140 @@
value: '02'
}
])
const evalDimens = [{
gradeDimensName: '维度1',
anlyGrade: 60
}, {
gradeDimensName: '维度2',
anlyGrade: 80
}, {
gradeDimensName: '维度3',
anlyGrade: 30
}]
const feedbackNow = () => {
feedbackRef.value.open({});
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 = () => {
partnerChatReport({
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 || '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 || '维度'
}
})
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)
}
})
}
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
showLoading.value = true
getData()
});
onMounted(() => {})
</script>
@@ -148,6 +272,45 @@
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 {
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;
left: 0%;
width: 100%;
bottom: -50rpx;
height: 50rpx;
line-height: 50rpx;
text-align: center;
font-size: 28rpx;
}
}
}
.top-box {
@@ -257,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;
@@ -272,6 +478,7 @@
height: 46rpx;
border: 2rpx solid #d4dce8;
border-radius: 23rpx;
z-index: 10;
&.show-all {
&::after {
@@ -314,21 +521,41 @@
&-title {
font-size: 32rpx;
line-height: 64rpx;
font-weight: 600;
}
&-item {
padding: 23rpx 0;
.item-title {
// line-height: 40rpx;
// margin-bottom: 24rpx;
// font-weight: 500;
// .item-title-icon{
// width: 24rpx;
// height: 26rpx;
// position: relative;
// top: 2rpx;
// left: 10rpx;
// }
line-height: 40rpx;
margin-bottom: 24rpx;
font-weight: 500;
.item-title-icon{
width: 24rpx;
height: 26rpx;
position: relative;
top: 2rpx;
left: 10rpx;
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;
}
}
@@ -337,31 +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;
@@ -371,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) {
+428 -90
View File
@@ -8,72 +8,77 @@
<view class="top-box">
<view class="top-title">
<view class="back-icon" @click="common.navigateBack()"></view>
{{ '陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称' }}
{{ detailInfo.traName }}
</view>
</view>
<view class="body-content">
<scroll-view :class="[' body-content index-dialog-body', detailInfo.traInterTyp === '01'?'talking-type1':'']" :scroll-y="true" refresher-default-style="none"
:scroll-top="scrollTop" :show-scrollbar="false" :scroll-with-animation="false">
<view :class="['body-content-role', showRoleInfo?'':'hide-info']">
<view :class="['role-title']">
<ImagePreview class="role-title-avatar" :src="'/traoss/tras3/show/S3F0250181220722025110316545500000431780'" :isCache="true">
<ImagePreview class="role-title-avatar" :src="detailInfo.ossAddr" :isCache="true">
</ImagePreview>
<view class="role-title-info" @click="showRoleInfo = !showRoleInfo">
<view class="arrow-r-icon"></view>
<view class="role-name card-title">
<text class="card-title-name">{{ '李想' }}</text>
<text class="card-title-age">{{ 128 }}</text>
<text class="card-title-img">
<image class="card-title-icon" v-if="gender === 'm'" src="@/static/images/me/man.png"></image>
<image class="card-title-icon" v-if="gender === 'f'" src="@/static/images/me/wuman.png"></image>
</text>
<text class="card-title-name">{{ detailInfo.chaName }}</text>
<text class="card-title-age">{{ detailInfo.chaAge }}</text>
<text class="card-title-img">
<image class="card-title-icon" v-if="detailInfo.gender === 'm'" src="@/static/images/me/man.png">
</image>
<image class="card-title-icon" v-if="detailInfo.gender === 'f'" src="@/static/images/me/wuman.png">
</image>
</text>
</view>
<view class="role-desc">预计时间 {{10}} ~ {{15}} 分钟</view>
<view class="role-desc">预计时间 {{detailInfo.traStartLimit}} ~ {{detailInfo.traEndLimit}} 分钟</view>
</view>
</view>
<view class="role-content">
<view class="role-content-item">
身份情况
<!-- {{ item.occupation }} -->
<!-- 身份情况 -->
{{ detailInfo.occupation }}
</view>
<view class="role-content-item">
性格特点
<!-- {{ item.personality }} -->
<!-- 性格特点 -->
{{ detailInfo.personality }}
</view>
<view class="role-content-item">
<!-- {{ item.thoughtProcess }} -->
提问思路
<!-- 提问思路 -->
{{ detailInfo.thoughtProcess }}
</view>
<view class="role-content-item">
<!-- {{ item.chaDesc }} -->
人物描述
<!-- 人物描述 -->
{{ detailInfo.chaDesc }}
</view>
</view>
</view>
<view class="body-scene">
<view class="body-scene-title">场景描述</view>
<!-- sceneDesc -->
{{ '陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称' }}
{{ detailInfo.sceneDesc }}
</view>
<view class="talking-box">
<TalkingItem v-for="(item, index) in talkingList" :talkingType="item.type" :talkingInfo="item" :roleInfo="detailInfo"
:traId="traId" :execId="execId"
:isLast="index + 1 === talkingList.length" :isAnswering="!answerIsOver"
:activeVoiceId="activeVoiceTalkingItemId" @handleEvents="talkItemEvents"></TalkingItem>
</view>
</scroll-view>
<view class="over-btn-box" v-if="detailInfo.traInterTyp === '01'">
<view class="over-btn" @click="overTalking">
结束陪练
</view>
</view>
<view class="body-bottom">
<view class="body-bottom-btn" @click="toTip">
<!--
<image
class="type-icon"
src="@/static/images/sparring/video-icon.png"
v-if="itemInfo.traInterTyp === '03'"
></image>
<image
class="type-icon"
src="@/static/images/sparring/phone-icon.png"
v-if="itemInfo.traInterTyp === '02'"
></image>
v-if="itemInfo.traInterTyp === '01'"-->
<TouchBtn class="body-bottom" @uploadVoice="uploadRecordNow" @startRecord="changePlayItemId('')"
:isLoading="!answerIsOver" :isDisabled="false" v-if="detailInfo.traInterTyp === '01'"/>
<view class="body-bottom" v-else>
<view class="body-bottom-btn" @click="toTalking">
<view class="center-text">
<image
class="type-icon"
src="@/static/images/sparring/talk-icon.png"
></image>
{{'语音通话'}}
<image class="type-icon" src="@/static/images/sparring/video-icon.png"
v-if="detailInfo.traInterTyp === '03'"></image>
<image class="type-icon" src="@/static/images/sparring/phone-icon.png"
v-else-if="detailInfo.traInterTyp === '02'"></image>
<image v-else class="type-icon" src="@/static/images/sparring/talk-icon.png"></image>
{{detailInfo.traInterTypDesc}}
</view>
</view>
</view>
@@ -84,8 +89,10 @@
<script setup>
import {
ref,
unref,
onMounted,
computed
computed,
nextTick
} from 'vue'
import {
onShow,
@@ -93,49 +100,335 @@
onUnload
} from '@dcloudio/uni-app';
import common from '@/common/common';
import TouchBtn from './components/touchBtn.vue';
import ImagePreview from '@/components/image-preview/image-preview.vue';
import {
queryTraPartnerCharacterInfoList
commonUploadVoiceFile
} from '@/api/common.js';
import {
queryTraPartnerCharacterInfoById,
partnerChatReportTrigger
} from "@/api/sparring.js"
import CardSwiper from './components/card-swiper.vue';
import {
useSocketStore
} from '@/store/socket.js';
import {
useStorageStore
} from '@/store/storage.js';
import {
storeToRefs
} from 'pinia';
import TalkingItem from './components/talking-item.vue'
const socketStore = useSocketStore();
const {
SocketObj
} = storeToRefs(socketStore);
const traId = ref('')
const chaId = ref('')
const talkingType = ref('')
const detailInfo = ref({})
const roleList = ref([])
const currentIndex = ref(0)
const showRoleInfo = ref(false)
const showLoading = ref(true)
const gender = ref('m')
const activeRole = computed(()=>{
if(roleList.value.length>0){
return roleList.value[currentIndex.value]
}else{
return {}
}
})
const talkingList = ref([])
// {
// type: 'answer',
// talkingText: 'asad我是回答内容',
// isLoading: false,
// id: new Date().getTime(),
// },
const askData = ref({})
const execId = ref('')
const answerIsOver = ref(true);
const getDetailInfo = () => {
queryTraPartnerCharacterInfoList({
traId: traId.value
queryTraPartnerCharacterInfoById({
traId: traId.value,
chaId: chaId.value
}).then(res => {
roleList.value = res.body || []
setTimeout(()=> {
console.log(res)
detailInfo.value = {
...res.body
}
if (detailInfo.value.traInterTyp === '01') {
initsocketTask()
}
setTimeout(() => {
showLoading.value = false
}, Math.random() * 1000 + 500)
})
}
const onSwiperChange = (data) => {
console.log('轮播图切换:', e);
currentIndex.value = data.index;
const scrollTop = ref(99999);
const scrollToBottomNow = (time = 100) => {
nextTick(() => {
setTimeout(() => {
scrollTop.value += 100;
scrollTop.value += 100;
}, time);
});
};
const uploadRecordNow = (voicePath) => {
//#ifndef APP-PLUS
// h5 假数据
// #endif
//#ifdef APP-PLUS
talkingList.value.push({
type: 'question',
talkingText: '',
isLoading: true
});
scrollToBottomNow();
commonUploadVoiceFile(voicePath, '/trapractice/traPartnerChat/voiceTrans', {})
.then((_res) => {
console.log(_res)
if (!!(_res.body.transContent || '').trim()) {
askData.value = {
execId: execId.value,
intrctWay: '02', //01-文本;02-语音;03-图片
intrctContent: _res.body.transContent,
bucketKey: _res.body.fileId,
ossFileAddr: _res.body.ossFileAddr,
talkingVoice: voicePath
};
sendMessage({
commond: 'ASK',
body: askData.value
});
} else {
talkingList.value.pop();
uni.showToast({
title: '未识别到文字',
icon: 'none',
duration: 1000
});
}
})
.catch((err) => {
talkingList.value.pop();
uni.showToast({
title: '系统异常,请联系管理员',
icon: 'none',
duration: 1000
});
});
// #endif
}
const toTalking = () => {
common.navigateTo('/pages/sparring/chooseRole?traId=' + traId.value + '&chaId=' + chaId)
}
const activeVoiceTalkingItemId = ref('')
// 关闭播放状态
const changePlayItemId = (id = '') => {
activeVoiceTalkingItemId.value = id
}
const toTip = () => {
common.navigateTo('/pages/sparring/tip?traId=' + traId.value + '&roleId=' + activeRole.value.chaId)
const talkItemEvents = ({
type,
data,
id,
info = {}
}) => {
switch (type) {
case 'changePlay':
activeVoiceTalkingItemId.value = id;
break;
case 'scrollToBottom':
scrollToBottomNow();
break;
default:
break;
}
};
const watchFlag = ref(1);
const showModelComfirm = ref(false)
const reconcatNum = ref(0)
const answerText = ref('')
const touchBtnCallBack = ref(null)
const initsocketTask = () => {
let _src = talkingType.value === 'preview'?'/trapractice/partnersocket/open':'/trapractice/partnersocket/open'
socketStore.creatSocket(_src + '?summary=');
// unref(SocketObj).on('open', (res)=>{
// if(!answerIsOver.value){
// sendMessage({
// "commond" : 'ASK-RE-START',
// "body" : {
// execId: execId.value,
// seqNo: seqNo.value,
// answerContent: answerText.value
// }
// })
// }
// })
unref(SocketObj).on('error', () => {
if (watchFlag.value === 1 || showModelComfirm.value) return;
if (reconcatNum.value === 5) {
common.msg('系统网络异常,请稍后再试!');
return;
}
showModelComfirm.value = true;
common
.show('提示信息', '网络环境不稳定,请重试!', false, '', '确认')
.then((res) => {
reconcatNum.value += 1;
common.redirectTo(`/pages/index/dialog`);
})
.finally(() => {
showModelComfirm.value = false;
});
});
unref(SocketObj).on('message', (res) => {
const {
rtnCode,
body,
commond
} = JSON.parse(res.data);
let _id = new Date().getTime() + 'id';
if (rtnCode === '0000') {
if (commond === 'ASK-OPEN') {
// 新连接
sendMessage({
commond: 'ASK-START',
body: {
traId: traId.value,
chaId: chaId.value,
talkingType: talkingType.value
}
});
return;
}
if (commond === 'ASK-START') {
// console.log('开启对话!')
execId.value = body.execId;
return;
}
if (commond === 'ASK') {
console.log('发送问题成功!');
console.log(body);
console.log(talkingList.value[talkingList.value.length - 1].isLoading);
if (talkingList.value.length > 0 && talkingList.value[talkingList.value.length - 1].isLoading) {
talkingList.value.pop();
}
// seqNo.value = body.seqNo;
// touchBtnCallBack.value && touchBtnCallBack.value();
// touchBtnCallBack.value = null;
talkingList.value.push({
type: 'question',
talkingText: body.content,
isLoading: false,
intrctWay: '02',
id: _id,
...body,
talkingVoice: askData.value.talkingVoice
});
talkingList.value.push({
type: 'answer',
isLoading: true,
id: _id + 'loading'
});
scrollToBottomNow();
return;
}
if (commond === 'ASK-STOP') {
console.log('停止回答标记!');
stopAnswerCallBack.value && stopAnswerCallBack.value(askData.value);
setTimeout(() => {
stopAnswerCallBack.value = null;
answerIsOver.value = true;
}, 10);
return;
}
if (commond === 'ASK-ANSWER') {
console.log('ASK-ANSWER',body);
if (body.event === 'start') {
console.log('开始回答!');
answerIsOver.value = false;
answerText.value = '';
talkingList.value.pop();
talkingList.value.push({
type: 'answer',
talkingText: answerText.value,
isLoading: false,
execLogId: body.execLogId||'',
id: _id,
...body,
askData: {
...askData.value
},
});
} 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') {
// console.log('开始继续回答!')
if (body.event === 'start') {
answerIsOver.value = false;
answerText.value = '';
} else if (body.event === 'token') {
answerIsOver.value = false;
answerText.value = body.data;
} else if (body.event === 'end') {
answerIsOver.value = true;
}
return;
}
if (commond === 'ASK-ERR') {
console.log('提问失败,系统异常!');
return;
}
} else {
if (['0005', 'QQ0005'].includes(res.rtnCode)) {
goto_login_fun();
}
}
});
watchFlag.value += 1;
};
const overTalking = () => {
partnerChatReportTrigger({
execId: execId.value,
traId: traId.value
}).then(res=>{
watchFlag.value = 1;
socketStore?.closeSocket();
common.navigateTo('/pages/sparring/result?traId=' + traId.value + '&execId=' + execId.value)
})
}
const sendMessage = (data) => {
unref(SocketObj).send(data);
};
//#ifdef APP-PLUS
onShow(() => {});
// #endif
onLoad((params) => {
traId.value = params.traId;
chaId.value = params.chaId;
talkingType.value = params.type;
getDetailInfo()
});
onUnload(() => {
watchFlag.value = 1;
socketStore?.closeSocket();
});
onMounted(() => {
getDetailInfo()
// getDetailInfo()
})
</script>
@@ -147,14 +440,16 @@
flex-direction: column;
background-image: url(@/static/images/sparring/bg.png);
background-size: cover;
// background-color: #f2f6fd;
.sparring-detail-loading{
.sparring-detail-loading {
position: fixed;
z-index: 20;
height: 100vh;
width: 100vw;
background-image: url(@/static/images/sparring/bg.png);
.loading-img{
.loading-img {
position: absolute;
width: 380rpx;
height: 590rpx;
@@ -170,6 +465,7 @@
background-size: cover;
}
}
.sparring-detail-body {
position: absolute;
overflow-x: hidden;
@@ -205,6 +501,7 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
// margin-bottom: 23rpx;
.arrow-icon {
position: absolute;
@@ -242,30 +539,38 @@
}
}
.body-content{
.body-content {
height: 100%;
padding: 10rpx 24rpx 20rpx;
box-sizing: border-box;
overflow-y: auto;
.body-content-role{
&.talking-type1{
padding: 10rpx 24rpx 140rpx;
}
.body-content-role {
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
margin-bottom: 30rpx;
&.hide-info{
&.hide-info {
.arrow-r-icon::after {
transform: rotate(225deg) !important;
}
.role-content{
.role-content {
max-height: 0rpx !important;
padding-bottom: 0rpx !important;
}
}
.role-title{
.role-title {
height: 154rpx;
position: relative;
padding-left: 180rpx;
&-avatar{
&-avatar {
width: 92rpx;
height: 92rpx;
position: absolute;
@@ -275,14 +580,16 @@
border-radius: 50%;
overflow: hidden;
}
&-info{
&-info {
position: relative;
height: 100%;
box-sizing: border-box;
padding-top: 34rpx;
.role-name{
.role-name {
width: 100%;
&.card-title {
display: flex;
font-size: 29rpx;
@@ -293,33 +600,38 @@
text-align: left;
justify-content: start;
margin-bottom: 8rpx;
.card-title-name{
.card-title-name {
margin-right: 20rpx;
line-height: 44rpx;
}
.card-title-age{
.card-title-age {
margin-right: 20rpx;
line-height: 44rpx;
}
.card-title-img{
.card-title-img {
height: 44rpx;
width: 30rpx;
padding: 5rpx 0;
box-sizing: border-box;
}
.card-title-icon{
.card-title-icon {
height: 30rpx;
width: 30rpx;
}
}
}
.role-desc{
.role-desc {
width: 100%;
font-size: 25rpx;
color: #666;
}
.arrow-r-icon {
position: absolute;
width: 36rpx;
@@ -329,14 +641,14 @@
top: 50%;
transform: translateY(-50%);
}
.arrow-r-icon::before,
.arrow-r-icon::after {
content: '';
position: absolute;
transition: all 0.3s ease;
}
.arrow-r-icon::after {
top: 25%;
left: 25%;
@@ -348,16 +660,19 @@
}
}
}
.role-content{
.role-content {
max-height: 800rpx;
transition: all 0.3s ease;
padding-bottom: 12rpx;
&-item{
&-item {
position: relative;
padding: 8rpx 46rpx 8rpx 76rpx;
line-height: 42rpx;
font-size: 29rpx;
&::before{
&::before {
content: '';
display: block;
background-color: #000;
@@ -371,17 +686,18 @@
}
}
}
.body-scene{
.body-scene {
background: linear-gradient(209deg, #fefeff 0%, #d7dfff 100%);
border: 2rpx solid rgba(255, 255, 255, .65);
border: 2rpx solid rgba(255, 255, 255, .65);
position: relative;
line-height: 42rpx;
font-size: 29rpx;
border-radius: 16rpx;
overflow: hidden;
padding: 108rpx 46rpx 30rpx;
.body-scene-title{
.body-scene-title {
position: absolute;
left: 0;
top: 0;
@@ -394,8 +710,28 @@
text-align: center;
}
}
.talking-box{
padding-top: 24rpx;
}
}
.over-btn-box{
position: absolute;
bottom: 180rpx;
height: 140rpx;
padding: 0 23rpx;
width: 100%;
.over-btn{
float: right;
width: 194rpx;
height: 76rpx;
line-height: 72rpx;
border: 2rpx solid #06f;
border-radius: 15rpx;
text-align: center;
color:#06f;
margin-top: 34rpx;
}
}
.body-bottom {
height: 180rpx;
background-color: #fff;
@@ -415,7 +751,8 @@
font-size: 29rpx;
border-radius: 23rpx;
position: relative;
.center-text{
.center-text {
height: 35rpx;
line-height: 35rpx;
width: 100%;
@@ -425,7 +762,8 @@
display: flex;
justify-content: center;
}
.type-icon{
.type-icon {
width: 35rpx;
height: 35rpx;
margin-right: 10rpx;
@@ -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
}
}
},
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 713 B

+8 -1
View File
@@ -10,7 +10,8 @@ export const useStorageStore = defineStore('storageStore',{
showPreviewDetail: false,
fileType: 'iamge',
fileId: '',
fileSrc: ''
fileSrc: '',
gettingVoiceId: ''
}),
getters:{
imgObj(state){
@@ -37,8 +38,14 @@ export const useStorageStore = defineStore('storageStore',{
getFileSrc(state){
return state.fileSrc
},
getGettingVoiceId(state){
return state.gettingVoiceId
},
},
actions:{
setGettingVoiceId(id){
return this.gettingVoiceId = id
},
setShowPreviewDetail({ status = false, fileType = 'image', fileId = '', fileSrc = '' }){
this.showPreviewDetail = status
this.fileId = fileId