三期pk功能开发以及积分两个页

This commit is contained in:
田岩
2025-10-24 14:53:19 +08:00
parent 7bffe0c9d7
commit 7107dade1a
70 changed files with 4521 additions and 2204 deletions
@@ -0,0 +1,60 @@
<template>
<image class="img_100" v-if="showLocalImage" :src="imagePath"></image>
<image-preview v-else-if="showNetworkImage" class="img_100" :src="imageAddr"></image-preview>
<image class="img_100" v-else :src="defaultAvatar"></image>
</template>
<script setup>
import { ref, reactive, watch, onMounted } from 'vue';
import { defaultAvatar } from '@/enum.js';
import { getUserInfo } from '@/common/common';
const imagePath = ref('');
const imageAddr = ref('');
// 状态标识
const showLocalImage = ref(false);
const showNetworkImage = ref(false);
// 初始化用户信息
const initUserInfo = () => {
const info = getUserInfo();
imagePath.value = info.imagePath || '';
imageAddr.value = info.imageAddr || '';
validateAvatarSources();
};
// 验证头像来源
const validateAvatarSources = () => {
if (imagePath.value) {
showLocalImage.value = true;
showNetworkImage.value = false;
} else if (imageAddr.value) {
showLocalImage.value = false;
showNetworkImage.value = true;
} else {
showLocalImage.value = false;
showNetworkImage.value = false;
}
};
// 强制刷新方法 - 重新获取用户信息并刷新头像
const forceRefreshAvatar = () => {
// 清空当前图片信息
imagePath.value = '';
imageAddr.value = '';
// 重新获取用户信息
initUserInfo();
};
onMounted(() => {
initUserInfo();
});
// 暴露方法给父组件
defineExpose({
forceRefreshAvatar
});
</script>
<style scoped lang="scss"></style>
+7 -8
View File
@@ -2,10 +2,11 @@
<view class="item">
<view class="top">
<view class="type">{{ subject_type_text }}</view>
<view class="score" v-if="['settlement', 'examination'].includes(props.mode)">{{ item.tgtGrade }}</view>
<view class="score" v-if="['settlement', 'examination', 'pk_in'].includes(props.mode)">{{ item.tgtGrade }}</view>
<view class="fl1"></view>
<view class="right">
<!-- 反馈中心 -->
<slot name="title-right"></slot>
<view class="mark_and_feedback_div" v-if="['examination', 'practice'].includes(props.mode)">
<image
v-if="props.mode === 'examination'"
@@ -23,12 +24,10 @@
<template v-if="['mistake'].includes(props.mode)">
<view class="expand_button_div" @click="toggleExpand()">
<!-- <view class="right_text">{{animationState?'收起':'展开'}}</view> -->
<view class="toggle-container">
<view class="toggle-text" :class="{ active: !animationState }">展开</view>
<view class="toggle-text" :class="{ active: animationState }">收起</view>
</view>
<uv-icon
class="click_text_icon"
name="arrow-right"
@@ -52,7 +51,7 @@
:isPreview="props.isPreview"
:activeVoiceId="subject_data.qnsId"
@buutton_click="questionButtonClick"
:button_complete_show="props.mode === 'practice'"
:button_complete_show="['practice', 'pk_in'].includes(props.mode)"
></questionVue>
</template>
<template v-else>
@@ -67,7 +66,7 @@
</view>
<view
class="option_div_button"
v-if="qnsTyp === 'MU' && ['practice', 'study'].includes(props.mode) && !props.isPreview && clearResult"
v-if="qnsTyp === 'MU' && ['practice', 'study', 'pk_in'].includes(props.mode) && !props.isPreview && clearResult"
>
<uv-button
type="primary"
@@ -85,7 +84,7 @@
class="analysis_div"
v-if="
!clearResult &&
['study', 'mistake', 'settlement', 'practice', 'practice_record'].includes(props.mode)
['study', 'mistake', 'settlement', 'practice', 'practice_record', 'pk_in'].includes(props.mode)
"
>
<view class="analysis_title" v-if="qnsTyp !== 'ES'">
@@ -153,8 +152,8 @@
// 错题本: mistake
// 学习: study
// 考试结算页 settlement
// 练习记录页 practice_record
return ['examination', 'practice', 'mistake', 'study', 'settlement', 'practice_record'].includes(value);
// 练习记录页 practice_record, 'practice_record'
return ['examination', 'practice', 'mistake', 'study', 'settlement', 'practice_record', 'pk_in'].includes(value);
}
},
// 是否预览模式
@@ -1,5 +1,5 @@
<template>
<view>
<view style="line-height: 0;">
<image :src="imgUrlShow" :mode="mode" @click="showDetail" style="width: 100%; height: 100%"></image>
<uni-popup ref="PopupRef" class="popup" mask-background-color="rgba(0,0,0,.2)" background-color="rgba(0,0,0,.2)" @change="changeShow">
<view class="overlay-box" @click="closePopup" @touchmove.stop>
@@ -167,6 +167,7 @@
</script>
<style lang="scss" scoped>
.overlay-box {
position: fixed;
left: 0;
@@ -182,6 +183,7 @@
width: 100vw;
max-height: 100%;
transform: translate(-50%, -50%);
}
}
</style>
@@ -0,0 +1,197 @@
<template>
<uni-popup ref="popupRef" class="popup">
<view class="tree-selector-div">
<view class="title">
<view class="title-text">机构选择</view>
<view class="back_div" @click="openDialog(false)">
<image :src="optionErrorIcon('#000000')" class="img"></image>
</view>
<uv-tabs
class="uvTabs"
:list="selectedPaths"
:lineWidth="40"
lineColor="#0066FF"
:activeStyle="{ color: '#0066FF', fontSize: '30rpx' }"
keyName="orgName"
:current="selectedPaths.length - 1"
@click="tabsClick"
></uv-tabs>
</view>
<scroll-view
ref="scrollRef"
scroll-with-animation
:scroll-into-view="scrollIntoId"
class="tree-selector-scroll"
scroll-x="true"
:show-scrollbar="false"
>
<treeSelector :node="orgTree"></treeSelector>
</scroll-view>
<view class="button-div">
<view class="button">
<image class="img" :src="resetIcon()"></image>
<view class="" @click="click_button('reset')">重置</view>
</view>
<view class="button" @click="click_button('all')">全部</view>
<view class="button" @click="click_button('confirm')">确定</view>
</view>
</view>
</uni-popup>
</template>
<script setup>
import { ref, reactive, provide, nextTick } from 'vue';
import common, { getUserInfo } from '@/common/common';
import { resetIcon, optionErrorIcon } from '@/common/imgSvg';
import treeSelector from './treeSelector';
const popupRef = ref(null);
// 滚动容器的 ref
const scrollRef = ref(null);
// 控制滚动位置的属性(scroll-top)
const scrollIntoId = ref('');
const openDialog = (status) => {
if (status) {
popupRef.value.open('bottom');
} else {
popupRef.value.close();
}
};
const tabsClick = (item) => {
if (item.index === selectedPaths.value.length - 1) {
return;
}
updateSelectedPaths(
{
orgName: item.orgName,
orgId: item.orgId,
parentId: item.parentId
},
item.index
);
};
const orgTreeInit = common.getValue('orgTree');
// 初始选中的
const initSelect = {
orgName: orgTreeInit[0].orgName,
orgId: orgTreeInit[0].orgId,
parentId: orgTreeInit[0].parentId
};
const orgTree = ref(orgTreeInit);
// 把全行信息插入
const selectedPaths = ref([initSelect]);
const updateSelectedPaths = (node, level) => {
console.log('node', node);
const newPaths = [...selectedPaths.value];
newPaths.splice(level);
newPaths.push(
reactive({
orgName: node['orgName'],
orgId: node['orgId'],
parentId: node['parentId']
})
);
selectedPaths.value = newPaths;
nextTick(() => {
scrollIntoId.value = `item-${selectedPaths.value.length}`;
});
};
const click_button = (type) => {
if (type === 'reset') {
updateSelectedPaths(initSelect, 0);
} else if (type === 'confirm') {
emit('submit', JSON.parse(JSON.stringify(selectedPaths.value)));
} else if (type === 'all') {
emit('submit', [{ orgName: '全部', orgId: 'A', parentId: '' }]);
}
};
provide('treeContextKey', {
selectedPaths,
updateSelectedPaths
});
const emit = defineEmits(['submit']);
defineExpose({
open: () => openDialog(true),
close: () => openDialog(false)
});
</script>
<style lang="scss" scoped>
.tree-selector-div {
display: flex;
flex-direction: column;
width: 100%;
background-color: #fff;
border-radius: 16rpx;
.title {
font-weight: 600;
font-size: 34rpx;
color: #333333;
text-align: center;
padding: 40rpx 0 0 0;
border-bottom: 4rpx solid #efefef;
.back_div {
width: 70rpx;
height: 70rpx;
position: absolute;
top: 20rpx;
right: 20rpx;
display: flex;
align-items: center;
justify-content: center;
.img {
width: 30rpx;
height: 30rpx;
}
}
.uvTabs {
margin-bottom: -6rpx;
:deep(.uv-tabs__wrapper__nav__item) {
padding: 0 44rpx 0 1rem;
}
}
}
.button-div {
display: flex;
justify-content: space-between;
background: #ffffff;
// box-shadow: 0px -6 15px 0px rgba(0,0,0,0.05);
.button {
flex: 1;
border: none;
display: flex;
height: 124rpx;
justify-content: center;
align-items: center;
font-weight: 600;
font-size: 30rpx;
.img {
width: 38rpx;
height: 38rpx;
}
&:first-child {
color: #333333;
}
&:last-child {
color: #0066ff;
}
&:active {
background: #eaeaea;
// transform: scale(0.98); /* 按下时轻微缩小,增强反馈 */
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2); /* 内阴影模拟按下凹陷感 */
}
}
}
}
.tree-selector-scroll {
white-space: nowrap;
width: 100%;
overflow: hidden;
background: #f0f0f0;
}
</style>
@@ -0,0 +1,134 @@
<template>
<view class="tree-selector" :id="`item-${props.level}`">
<view class="tree-child-selector" :class="{ fl1: props.level === 0}">
<view class="level" v-for="item in props.node" @click="click_node(item)">
<view class="node" :class="classList(item.orgId)">
{{ item.orgName }}
</view>
<view class="fl1"></view>
</view>
</view>
<treeSelector v-if="childNode && childNode.length" :node="childNode" :level="props.level + 1"></treeSelector>
</view>
</template>
<script setup>
import { defineOptions, ref, inject, reactive } from 'vue';
import { computed } from 'vue';
const treeContextKey = 'treeContextKey';
const { selectedPaths, updateSelectedPaths } = inject(treeContextKey);
const props = defineProps({
// 当前节点数据
node: {
type: Array, // 改为数组类型
required: true,
// 验证数组中的每个元素是否符合结构要求
validator: (value) => {
// 先判断是否为数组
if (!Array.isArray(value)) return false;
// 验证数组中的每个对象
return value.every((item) => {
// 每个元素必须是对象
if (typeof item !== 'object' || item === null) return false;
// 必须包含 orgId(字符串类型)
if (typeof item.orgId !== 'string') return false;
// 必须包含 orgName(字符串类型)
if (typeof item.orgName !== 'string') return false;
return true;
});
}
},
level: {
type: Number,
default: 0
}
});
// 把选择的层级树处理成id列表
const selectedPathsOrgCodeList = computed(() => selectedPaths.value.map((res) => res.orgId));
// 设置子列表的数据
const childNode = computed(() => {
const lastItem = selectedPaths.value.at(-1);
const selectItem = props.node.find((res) => selectedPathsOrgCodeList.value.includes(res.orgId));
if (selectItem) return selectItem['children'] || [];
return [];
});
// 判断需要的样式class
const classList = (orgId) => {
let a = [];
if (selectedPathsOrgCodeList.value.includes(orgId) || props.level === selectedPathsOrgCodeList.value.length) {
a.push('select-bg');
}
if (selectedPathsOrgCodeList.value.at(-1) === orgId) {
a.push('select-text');
}
return a;
};
const emit = defineEmits(['update:selectedKeys']);
// 处理当前节点被点击
const click_node = (node) => {
updateSelectedPaths(node, props.level);
};
defineOptions({
name: 'treeSelector'
});
</script>
<style scoped lang="scss">
.tree-selector {
border-left: 1rpx solid #eaeaea;
border-right: 1rpx solid #eaeaea;
display: flex;
min-height: 600rpx;
max-height: 800rpx;
display: flex;
transition: all 0.3s ease 2s; /* 关键配置 */
}
.tree-child-selector {
display: inline-block;
// 每一级树形容器
min-width: 6rem;
overflow-y: scroll;
scrollbar-width: none; /* Firefox 特有:直接隐藏滚动条 */
-ms-overflow-style: none; /* IE 和 Edge 特有:隐藏滚动条 */
&::-webkit-scrollbar {
display: none; /* 隐藏滚动条 */
}
.level {
display: flex;
flex-direction: column;
}
.node {
padding: 0 1rem;
background: #f7f7f7;
font-weight: 700;
font-size: 30rpx;
color: #333333;
text-align: left;
height: 80rpx;
line-height: 80rpx;
white-space: nowrap;
/* 强制不换行 */
overflow: hidden;
/* 超出部分隐藏 */
text-overflow: ellipsis;
/* 超出部分显示省略号 */
&.select-bg {
background: #fff;
}
&.select-text {
color: #0066ff;
}
}
}
</style>