feat: 陪练选择角色

This commit is contained in:
杨航
2025-11-04 14:08:08 +08:00
parent 68af8c9da6
commit 466516ea16
15 changed files with 865 additions and 48 deletions
+1 -1
View File
@@ -4,6 +4,6 @@ ENV = 'development.ty'
VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7001'
VITE_APP_BASE_H5_API_Url_TRAEXAM = 'http://192.168.108.129'
# VITE_APP_BASE_H5_API_Url_TRAEXAM = 'http://192.168.108.129'
+8
View File
@@ -33,4 +33,12 @@ export const queryTraPartnerInfoById = (data) => {
method: 'post',
data
});
};
// 陪练角色列表 traId
export const queryTraPartnerCharacterInfoList = (data) => {
return request({
url: base_url + '/traPartnerCharacterInfo/queryTraPartnerCharacterInfoList',
method: 'post',
data
});
};
+18
View File
@@ -456,6 +456,24 @@
"titleNView": false
}
}
},
{
"path": "pages/sparring/tip",
"style": {
"navigationBarTitleText": "AI陪练提示",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/sparring/chooseRole",
"style": {
"navigationBarTitleText": "AI陪练选择角色",
"app-plus": {
"titleNView": false
}
}
}
],
"tabBar": {
+316
View File
@@ -0,0 +1,316 @@
<template>
<view class="sparring-detail max_page">
<view class="sparring-detail-body">
<view class="top-box">
<view class="top-title">
<view class="back-icon" @click="common.navigateBack()"></view>
</view>
</view>
<view class="body-swiper">
<view class="body-swiper-title">选择本次陪练的对象</view>
<view class="body-swiper-box">
<CardSwiper @changeChoose="onSwiperChange" :list="roleList" />
</view>
</view>
<!-- <view class="body-card">
<view class="card">
<ImagePreview class="img-box" :src="activeRole.ossAddr" :isCache="true">
</ImagePreview>
<view class="card-title">
<text class="card-title-name">{{ activeRole.chaName }}</text>
<text class="card-title-age">{{ activeRole.chaAge }}</text>
<text class="card-title-img">
<image class="card-title-icon" v-if="activeRole.gender === 'm'" src="@/static/images/me/man.png"></image>
<image class="card-title-icon" v-if="activeRole.gender === 'f'" src="@/static/images/me/wuman.png"></image>
</text>
</view>
<view class="card-desc">
{{ activeRole.occupation }}
</view>
<view class="card-item">
<view class="card-item-title">
<image class="card-item-title-icon" src="@/static/images/sparring/role-icon1.png"></image>
身份情况
</view>
<text class="card-item-desc">{{ activeRole.occupation }}</text>
</view>
<view class="card-item">
<view class="card-item-title">
<image class="card-item-title-icon" src="@/static/images/sparring/role-icon2.png"></image>
性格特点
</view>
<text class="card-item-desc">{{ activeRole.personality }}</text>
</view>
</view>
</view> -->
<view class="body-bottom">
<view class="body-bottom-btn" @click="toTip">开始陪练</view>
</view>
</view>
</view>
</template>
<script setup>
import {
ref,
onMounted,
computed
} from 'vue'
import {
onShow,
onLoad,
onUnload
} from '@dcloudio/uni-app';
import common from '@/common/common';
import ImagePreview from '@/components/image-preview/image-preview.vue';
import {
queryTraPartnerCharacterInfoList
} from "@/api/sparring.js"
import CardSwiper from './components/card-swiper.vue';
const traId = ref('')
const detailInfo = ref({})
const roleList = ref([])
const currentIndex = ref(0)
const activeRole = computed(()=>{
if(roleList.value.length>0){
return roleList.value[currentIndex.value]
}else{
return {}
}
})
const getDetailInfo = () => {
queryTraPartnerCharacterInfoList({
traId: traId.value
}).then(res => {
roleList.value = res.body || []
})
}
const onSwiperChange = (data) => {
console.log('轮播图切换:', e);
currentIndex.value = data.index;
}
const toTip = () => {
common.navigateTo('/pages/sparring/tip?traId=' + traId.value + '&roleId=' + activeRole.value.chaId)
}
onLoad((params) => {
traId.value = params.traId;
});
onMounted(() => {
getDetailInfo()
})
</script>
<style lang="scss" scoped>
.sparring-detail {
position: relative;
overflow-x: hidden;
overflow-y: auto;
display: flex;
flex-direction: column;
background-image: url(@/static/images/sparring/bg.png);
background-size: cover;
.sparring-detail-body {
position: absolute;
overflow-x: hidden;
overflow-y: hidden;
left: 0;
top: 0;
min-height: 100%;
width: 100%;
box-sizing: border-box;
// background-color: #f6f8ff;
display: flex;
z-index: 1;
padding: calc(var(--status-bar-height) + 90rpx) 0 180rpx;
.top-box {
height: 90rpx;
width: 100%;
position: absolute;
top: var(--status-bar-height);
left: 0;
z-index: 2;
.top-title {
height: 90rpx;
// background-color: red;
text-align: center;
font-size: 33rpx;
padding-top: 10rpx;
line-height: 46rpx;
box-sizing: border-box;
font-weight: 500;
color: #fff;
position: relative;
// margin-bottom: 23rpx;
.arrow-icon {
position: absolute;
left: 36rpx;
top: 20rpx;
color: #fff !important;
}
.back-icon {
position: absolute;
width: 40rpx;
height: 40rpx;
cursor: pointer;
left: 50rpx;
top: 33rpx;
transform: translateY(-50%);
}
.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 #000;
border-left: 4rpx solid #000;
transform: rotate(-45deg);
}
}
}
.body-swiper {
flex: 1;
&-title {
height: 90rpx;
line-height: 90rpx;
text-align: center;
font-size: 33rpx;
margin-bottom: 23rpx;
}
&-box {
height: calc(100% - 160rpx);
overflow: hidden;
}
}
// .body-card {
// flex: 1;
// .card {
// width: calc(100% - 90rpx);
// margin: 0 auto;
// height: 100%;
// border-radius: 24rpx;
// background: #e5eafd;
// overflow-x: hidden;
// overflow-Y: auto;
// transition: all 0.3s ease;
// position: relative;
// padding: 50rpx 23rpx 23rpx;
// .img-box {
// width: 300rpx;
// height: 300rpx;
// border-radius: 50%;
// overflow: hidden;
// margin: 0 auto;
// }
// .card-content {
// padding: 24rpx;
// }
// .card-title {
// display: flex;
// font-size: 29rpx;
// line-height: 44rpx;
// font-weight: 500;
// color: #000;
// overflow: hidden;
// position: relative;
// margin-bottom: 12rpx;
// text-align: center;
// margin: 33rpx auto;
// justify-content: center;
// &-name{
// margin-right: 20rpx;
// line-height: 44rpx;
// }
// &-age{
// margin-right: 20rpx;
// line-height: 44rpx;
// }
// &-img{
// height: 44rpx;
// width: 30rpx;
// padding: 7rpx 0;
// box-sizing: border-box;
// }
// &-icon{
// height: 30rpx;
// width: 30rpx;
// }
// }
// .card-item {
// width: 100%;
// display: block;
// background: linear-gradient(209deg, #fefeff 0%, #ddeafc 100%);
// border: 2rpx solid rgba(255, 255, 255, .65);
// margin-bottom: 15rpx;
// border-radius: 15rpx;
// box-sizing: border-box;
// padding: 23rpx;
// font-size: 28rpx;
// line-height: 40rpx;
// &-title{
// color: #06f;
// padding-left: 38rpx;
// position: relative;
// margin-bottom: 8rpx;
// &-icon{
// width: 30rpx;
// height: 30rpx;
// position: absolute;
// left: 0;
// top: 50%;
// transform: translateY(-50%);
// }
// }
// &-desc{
// }
// }
// }
// }
.body-bottom {
height: 180rpx;
// background-color: #fff;
padding: 36rpx 23rpx 0;
position: fixed;
width: 100vw;
bottom: 0;
left: 0;
&-btn {
width: 100%;
height: 92rpx;
line-height: 92rpx;
background-color: #06f;
text-align: center;
color: #fff;
font-size: 29rpx;
border-radius: 23rpx;
}
}
}
}
</style>
@@ -0,0 +1,267 @@
<template>
<view class="container">
<!-- 堆叠轮播区域 -->
<view class="swiper-wrapper">
<swiper class="stack-swiper" :current="currentIndex" :circular="circular" :previous-margin="previousMargin"
:next-margin="nextMargin" @change="onSwiperChange" :autoplay="autoplay" :interval="interval"
:duration="duration">
<swiper-item v-for="(item, index) in list" :key="index" class="swiper-item">
<view class="card" :class="{ 'active': currentIndex === index }" @click="onCardClick(item, index)">
<ImagePreview class="img-box" :src="item.ossAddr" :isCache="true">
</ImagePreview>
<view class="card-title">
<text class="card-title-name">{{ item.chaName }}</text>
<text class="card-title-age">{{ item.chaAge }}</text>
<text class="card-title-img">
<image class="card-title-icon" v-if="item.gender === 'm'" src="@/static/images/me/man.png"></image>
<image class="card-title-icon" v-if="item.gender === 'f'" src="@/static/images/me/wuman.png"></image>
</text>
</view>
<view class="card-item">
<view class="card-item-title">
<image class="card-item-title-icon" src="@/static/images/sparring/role-icon1.png"></image>
身份情况
</view>
<text class="card-item-desc">{{ item.occupation }}</text>
</view>
<view class="card-item">
<view class="card-item-title">
<image class="card-item-title-icon" src="@/static/images/sparring/role-icon2.png"></image>
性格特点
</view>
<text class="card-item-desc">{{ item.personality }}</text>
</view>
<view class="card-item">
<view class="card-item-title">
<image class="card-item-title-icon" src="@/static/images/sparring/role-icon3.png"></image>
提问思路
</view>
<text class="card-item-desc">{{ item.thoughtProcess }}</text>
</view>
<view class="card-item">
<view class="card-item-title">
<image class="card-item-title-icon" src="@/static/images/sparring/role-icon4.png"></image>
人物描述
</view>
<text class="card-item-desc">{{ item.chaDesc }}</text>
</view>
</view>
</swiper-item>
</swiper>
</view>
</view>
</template>
<script>
import ImagePreview from '@/components/image-preview/image-preview.vue';
export default {
name: 'CardSwiper',
components: {
ImagePreview
},
props: {
list: {
type: Array,
default: () => [{
image: 'https://picsum.photos/300/400?random=1',
title: '卡片标题1',
desc: '这是卡片描述信息1'
},
{
image: 'https://picsum.photos/300/400?random=2',
title: '卡片标题2',
desc: '这是卡片描述信息2'
},
{
image: 'https://picsum.photos/300/400?random=3',
title: '卡片标题3',
desc: '这是卡片描述信息3'
}
]
},
autoplay: {
type: Boolean,
default: false
},
interval: {
type: Number,
default: 3000
},
duration: {
type: Number,
default: 500
},
circular: {
type: Boolean,
default: true
}
},
data() {
return {
currentIndex: 0,
previousMargin: '64rpx',
nextMargin: '64rpx'
};
},
methods: {
onSwiperChange(e) {
this.currentIndex = e.detail.current;
this.$emit('changeChoose', {
index: this.currentIndex,
item: this.list[this.currentIndex]
});
},
onCardClick(item, index) {
this.$emit('cardClick', {
item: item,
index: index
});
if (this.currentIndex === index) return
this.swipeTo(index)
},
// 手动切换到指定索引
swipeTo(index) {
if (index >= 0 && index < this.list.length) {
this.currentIndex = index;
}
},
// 切换到下一张
next() {
if (this.currentIndex === index) return
let nextIndex = this.currentIndex + 1;
if (nextIndex >= this.list.length) {
nextIndex = this.circular ? 0 : this.list.length - 1;
}
this.swipeTo(nextIndex);
},
// 切换到上一张
prev() {
if (this.currentIndex === index) return
let prevIndex = this.currentIndex - 1;
if (prevIndex < 0) {
prevIndex = this.circular ? this.list.length - 1 : 0;
}
this.swipeTo(prevIndex);
}
}
};
</script>
<style scoped lang="scss">
.container {
padding: 0rpx 0;
height: 100%;
}
.swiper-wrapper {
position: relative;
height: 100%;
}
.stack-swiper {
height: 100%;
}
.swiper-item {
display: flex;
box-sizing: border-box;
justify-content: center;
align-items: center;
}
.card {
width: 590rpx;
height: 100%;
border-radius: 24rpx;
background: #e5eafd;
overflow-x: hidden;
overflow-Y: auto;
transition: all 0.3s ease;
position: relative;
padding: 50rpx 23rpx 23rpx;
.card-content {
padding: 24rpx;
}
.card-title {
display: flex;
font-size: 29rpx;
line-height: 44rpx;
font-weight: 500;
color: #000;
overflow: hidden;
position: relative;
margin-bottom: 12rpx;
text-align: center;
margin: 33rpx auto;
justify-content: center;
&-name{
margin-right: 20rpx;
line-height: 44rpx;
}
&-age{
margin-right: 20rpx;
line-height: 44rpx;
}
&-img{
height: 44rpx;
width: 30rpx;
padding: 7rpx 0;
box-sizing: border-box;
}
&-icon{
height: 30rpx;
width: 30rpx;
}
}
.card-item {
width: 100%;
display: block;
background: linear-gradient(209deg, #fefeff 0%, #ddeafc 100%);
border: 2rpx solid rgba(255, 255, 255, .65);
margin-bottom: 15rpx;
border-radius: 15rpx;
box-sizing: border-box;
padding: 23rpx;
font-size: 28rpx;
line-height: 40rpx;
&-title{
color: #06f;
padding-left: 38rpx;
position: relative;
margin-bottom: 8rpx;
&-icon{
width: 30rpx;
height: 30rpx;
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
}
}
&-desc{
}
}
}
.card:not(.active) {
transform: scaleY(0.8) scaleX(1);
}
.img-box {
width: 300rpx;
height: 300rpx;
border-radius: 50%;
overflow: hidden;
margin: 0 auto;
}
</style>
+62 -47
View File
@@ -1,14 +1,15 @@
<template>
<view class="sparring-detail max_page"><view class="sparring-detail-body">
<view class="top-box">
<view class="top-title">
<view class="back-icon" @click="common.navigateBack()"></view>
<!-- <uv-icon class="arrow-icon" name="arrow-left" @click="common.switchTab('/pages/index/index')" /> -->
陪练详情
<view class="sparring-detail max_page">
<view class="sparring-detail-body">
<view class="top-box">
<view class="top-title">
<view class="back-icon" @click="common.navigateBack()"></view>
<!-- <uv-icon class="arrow-icon" name="arrow-left" @click="common.switchTab('/pages/index/index')" /> -->
陪练详情
</view>
<ImagePreview class="img-box" :src="detailInfo.ossAddr" :isCache="true" width="704" height="350" />
</view>
<ImagePreview class="img-box" :src="detailInfo.ossAddr" :isCache="true" width="704" height="350" />
</view>
<view class="body-top">
<view class="body-top-title">
{{detailInfo.traName}}
@@ -69,7 +70,7 @@
</view>
</view>
<view class="body-bottom">
<view class="body-bottom-btn">AI陪练</view>
<view class="body-bottom-btn" @click="toChooseRole">AI陪练</view>
</view>
</view>
</view>
@@ -80,11 +81,19 @@
ref,
onMounted
} from 'vue'
import { onShow, onLoad, onUnload } from '@dcloudio/uni-app';
import {
onShow,
onLoad,
onUnload
} from '@dcloudio/uni-app';
import common from '@/common/common';
import ImagePreview from '@/components/image-preview/image-preview.vue';
import { queryTraPartnerInfoById } from "@/api/sparring.js"
import { getPhoneEnvBool } from '@/common/common';
import {
queryTraPartnerInfoById
} from "@/api/sparring.js"
import {
getPhoneEnvBool
} from '@/common/common';
const traId = ref('')
const detailInfo = ref({})
const isDownloading = ref(false)
@@ -107,39 +116,39 @@
}, {
tagName: '销售类'
}])
const getDetailInfo = () => {
queryTraPartnerInfoById({
id: traId.value
}).then(res=> {
}).then(res => {
console.log(res)
detailInfo.value = res.body||{}
detailInfo.value = res.body || {}
})
}
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)
});
}
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 {
@@ -147,8 +156,7 @@
return new Promise((resolve, reject) => {
console.log('开始下载:', fileUrl);
const dtask = plus.downloader.createDownload(
fileUrl,
{
fileUrl, {
filename: localFilePath
},
(d, status) => {
@@ -174,10 +182,13 @@
throw error;
}
};
const toChooseRole = () => {
common.navigateTo('/pages/sparring/chooseRole?traId=' + traId.value)
}
onLoad((params) => {
traId.value = params.traId;
});
onMounted(()=> {
onMounted(() => {
getDetailInfo()
})
</script>
@@ -209,7 +220,7 @@
clip-path: ellipse(650rpx 327rpx at 375rpx 109rpx);
width: 100%;
position: absolute;
top: 0;
top: var(--status-bar-height);
left: 0;
z-index: 2;
@@ -219,7 +230,7 @@
margin: 0 auto;
border-radius: 23rpx;
overflow: hidden;
}
.top-title {
@@ -332,7 +343,8 @@
}
}
}
.body-bottom{
.body-bottom {
height: 180rpx;
background-color: #fff;
padding: 36rpx 23rpx 0;
@@ -340,7 +352,8 @@
width: 100vw;
bottom: 0;
left: 0;
&-btn{
&-btn {
width: 100%;
height: 92rpx;
line-height: 92rpx;
@@ -351,6 +364,7 @@
border-radius: 23rpx;
}
}
.body-item {
padding: 28rpx;
background-color: #fff;
@@ -377,7 +391,8 @@
font-size: 28rpx;
line-height: 52rpx;
color: #333;
&-file{
&-file {
color: #06f;
}
}
+175
View File
@@ -0,0 +1,175 @@
<template>
<view class="sparring-detail max_page">
<view class="sparring-detail-body">
<view class="top-box">
<view class="top-title">
<view class="back-icon" @click="common.navigateBack()"></view>
{{ '陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称陪练名称' }}
</view>
</view>
<view class="body-bottom">
<view class="body-bottom-btn" @click="toTip">开始陪练</view>
</view>
</view>
</view>
</template>
<script setup>
import {
ref,
onMounted,
computed
} from 'vue'
import {
onShow,
onLoad,
onUnload
} from '@dcloudio/uni-app';
import common from '@/common/common';
import ImagePreview from '@/components/image-preview/image-preview.vue';
import {
queryTraPartnerCharacterInfoList
} from "@/api/sparring.js"
import CardSwiper from './components/card-swiper.vue';
const traId = ref('')
const detailInfo = ref({})
const roleList = ref([])
const currentIndex = ref(0)
const activeRole = computed(()=>{
if(roleList.value.length>0){
return roleList.value[currentIndex.value]
}else{
return {}
}
})
const getDetailInfo = () => {
queryTraPartnerCharacterInfoList({
traId: traId.value
}).then(res => {
roleList.value = res.body || []
})
}
const onSwiperChange = (data) => {
console.log('轮播图切换:', e);
currentIndex.value = data.index;
}
const toTip = () => {
common.navigateTo('/pages/sparring/tip?traId=' + traId.value + '&roleId=' + activeRole.value.chaId)
}
onLoad((params) => {
traId.value = params.traId;
});
onMounted(() => {
getDetailInfo()
})
</script>
<style lang="scss" scoped>
.sparring-detail {
position: relative;
overflow-x: hidden;
overflow-y: auto;
display: flex;
flex-direction: column;
background-image: url(@/static/images/sparring/bg.png);
background-size: cover;
.sparring-detail-body {
position: absolute;
overflow-x: hidden;
overflow-y: hidden;
left: 0;
top: 0;
min-height: 100%;
width: 100%;
box-sizing: border-box;
// background-color: #f6f8ff;
display: flex;
z-index: 1;
padding: calc(var(--status-bar-height) + 90rpx) 0 180rpx;
.top-box {
height: 90rpx;
width: 100%;
position: absolute;
top: var(--status-bar-height);
left: 0;
z-index: 2;
.top-title {
height: 90rpx;
// background-color: red;
text-align: center;
font-size: 33rpx;
padding: 10rpx 100rpx 0;
line-height: 46rpx;
box-sizing: border-box;
font-weight: 500;
color: #000;
position: relative;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
// margin-bottom: 23rpx;
.arrow-icon {
position: absolute;
left: 36rpx;
top: 20rpx;
color: #fff !important;
}
.back-icon {
position: absolute;
width: 40rpx;
height: 40rpx;
cursor: pointer;
left: 50rpx;
top: 33rpx;
transform: translateY(-50%);
}
.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 #000;
border-left: 4rpx solid #000;
transform: rotate(-45deg);
}
}
}
.body-bottom {
height: 180rpx;
// background-color: #fff;
padding: 36rpx 23rpx 0;
position: fixed;
width: 100vw;
bottom: 0;
left: 0;
&-btn {
width: 100%;
height: 92rpx;
line-height: 92rpx;
background-color: #06f;
text-align: center;
color: #fff;
font-size: 29rpx;
border-radius: 23rpx;
}
}
}
}
</style>
+18
View File
@@ -72,5 +72,23 @@
}
}
},
{
"path": "pages/sparring/tip",
"style": {
"navigationBarTitleText": "AI陪练提示",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/sparring/chooseRole",
"style": {
"navigationBarTitleText": "AI陪练选择角色",
"app-plus": {
"titleNView": false
}
}
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 925 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 905 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB