Files
tra-app/src/components/examination/subject.vue
T
2025-09-08 20:40:49 +08:00

509 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="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="fl1"></view>
<view class="right">
<!-- 反馈中心 -->
<view class="mark_and_feedback_div" v-if="['examination', 'practice'].includes(props.mode)">
<image
v-if="props.mode === 'examination'"
:src="markIcon(props.item.mark ? '#e8b531' : '#ABABAB')"
class="img"
@click="mark_click()"
></image>
<image :src="feedbackIcon()" class="img" @click="feedback_click()"></image>
</view>
<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"
:class="{ click_text_icon_action: animationState }"
color="#0066FF"
></uv-icon>
</view>
</template>
</view>
</view>
<view class="question_text">
{{ subject_data.qnsContent }}
</view>
<view :style="{ height: animationHeight }" :class="{ collapse_wrapper: props.mode === 'mistake' }" ref="wrapperRef">
<view class="collapse-content" ref="contentRef">
<view class="option_div">
<template v-if="qnsTyp === 'ES'">
<questionVue
:status="subject_data.es_status"
:dataInfo="props.item?.my_answer"
:isPreview="props.isPreview"
:activeVoiceId="subject_data.qnsId"
@buutton_click="questionButtonClick"
:button_complete_show="props.mode === 'practice'"
></questionVue>
</template>
<template v-else>
<radioSubjectItem
v-for="(analysisVo, index) in subject_data.itemVos"
@click_option="click_option"
:key="index"
:analysisVo="analysisVo"
:my_answer="item_answer"
></radioSubjectItem>
</template>
</view>
<view
class="option_div_button"
v-if="qnsTyp === 'MU' && ['practice', 'study'].includes(props.mode) && !props.isPreview && clearResult"
>
<uv-button
type="primary"
class="send_evaluate"
:custom-style="customStyle"
:throttleTime="300"
@click="muBtnClick"
:hairline="false"
>
确定
</uv-button>
</view>
<view
class="analysis_div"
v-if="!clearResult && ['study', 'mistake', 'settlement', 'practice', 'practice_record'].includes(props.mode)"
>
<view class="analysis_title" v-if="qnsTyp !== 'ES'">
<view class="success_answer">正确答案{{ correctResultLabel.join('') }}</view>
<view class="your_answer">
你的答案
<text :class="right_or_wrong_class ? 'success' : 'error'">{{ myResultLabel.join('') }}</text>
</view>
</view>
<esAnalysis
v-if="qnsTyp === 'ES'"
:value="subject_data.itemVos[0]"
:judgeResult="judgeResultStatus"
:answerText="subject_data.anserResult"
></esAnalysis>
<!-- 问答题专属参考答案 -->
<view class="reference_answer" v-if="qnsTyp === 'ES'">
<view class="reference_answer_title">参考答案</view>
{{ subject_data.itemVos[0]?.itemAnswer ?? '' }}
</view>
<view class="analysis_note">试题解析{{ subject_data?.analyContent }}</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, reactive, onMounted, nextTick, computed, getCurrentInstance, watch } from 'vue';
import radioSubjectItem from './radio-subject-item.vue';
import esAnalysis from './es-analysis.vue';
import questionVue from './question.vue';
import { markIcon, feedbackIcon } from '@/common/imgSvg';
import { SUBJECT_TYPE } from '@/enum.js';
import common from '@/common/common';
// 多选的确定按钮的样式
const customStyle = {
borderRadius: '16rpx',
width: '292rpx',
backgroundColor: '#0066FF',
border: '1rpx solid rgb(12, 123, 245)'
};
const emit = defineEmits(['subject_click', 'update:modelValue']);
const props = defineProps({
modelValue: {
type: [Array, Object],
default: () => []
},
item: {
type: Object,
required: true
},
mode: {
type: String,
required: true,
validator: (value) => {
// 考试: examination
// 练习: practice
// 错题本: mistake
// 学习: study
// 考试结算页 settlement
// 练习记录页 practice_record
return ['examination', 'practice', 'mistake', 'study', 'settlement', 'practice_record'].includes(value);
}
},
// 是否预览模式
isPreview: {
type: Boolean,
default: false
},
// 是否清除正确答案
clearResult: {
type: Boolean,
default: true
}
});
// 状态管理
const isExpanded = ref(false);
const wrapperRef = ref(null);
const contentRef = ref(null);
const judgeResultStatus = computed(() => props.item.judgeResult === 'P');
// 不是错题本就默认不折叠,错题本默认折叠
const animationState = ref(props.mode !== 'mistake' ? true : false);
const animationHeight = ref(props.mode !== 'mistake' ? '' : '0px');
const modelValue = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
});
// result.anserResult
let answerText = props.item?.my_answer || props.item.anserResult || [];
// item_answer 是答题结果,可以是对象 数组 字符串类型
const item_answer = ref(typeof answerText === 'string' ? answerText.split(',') : answerText);
const feedback_click = () => {
emit('subject_click', 'feedback', props.item);
};
const instance = getCurrentInstance(); // 获取当前组件实例
// 切换展开/折叠状态(兼容H5和App)
const toggleExpand = async () => {
console.log('切换展开/折叠状态', animationHeight.value, animationState.value);
if (!animationState.value) {
animationState.value = true;
const query = uni.createSelectorQuery().in(instance);
query
.select('.collapse-content')
.boundingClientRect((rect) => {
animationHeight.value = `${rect.height}px`;
console.log('rect', rect);
})
.exec();
} else {
animationState.value = false;
animationHeight.value = '0px';
}
};
const queryRect = () => {
// 组件内部一般用this.$uvGetRect,对外的为getRect,二者功能一致,名称不同
return new Promise((resolve) => {
this.$uvGetRect(`#${this.elId}`).then((size) => {
resolve(size);
});
});
};
const mark_click = () => {
emit('subject_click', 'mark', props.item);
};
// 单选 SN, 多选 MU,判断题 JD,简答 ES
const qnsTyp = computed(() => props.item.qnsTyp);
// 正确答案选项
const correctResultLabel = computed(() =>
subject_data.value.itemVos.filter((res) => res.correctFlag === 'R').map((res) => res.itemNo)
);
// 我的答案选项
const myResultLabel = computed(() => {
if (!props.item.anserResult) return ['无'];
const anserResultArray = props.item.anserResult.split(',');
return subject_data.value.itemVos.filter((res) => anserResultArray.includes(res.itemId)).map((res) => res.itemNo);
});
// 判断选项是否完全正确
const right_or_wrong_class = computed(
() =>
correctResultLabel.value.length === myResultLabel.value.length &&
myResultLabel.value.every((res) => correctResultLabel.value.includes(res))
);
const subject_data = computed({
get: () => {
return {
qnsId: props.item.qnsId,
qnsContent: props.item.qnsContent,
qnsTyp: props.item.qnsTyp,
es_status: props.item.es_status,
analyContent: props.item.analyContent,
anserResult: props.item.anserResult,
itemVos: (props.item.itemVos ?? []).map((res) => ({
...res,
correctFlag: props.clearResult ? '' : res.correctFlag
}))
};
},
set: (val) => {}
});
// 语音播放组件小按钮
const questionButtonClick = (type, submitObj) => {
console.log('type', type, submitObj);
// 判断点击的是什么
if (type === 'complete') {
// 完成
} else if (type === 'again') {
// 重答
}
emit('subject_click', type, {
qnsTyp: props.item.qnsTyp,
qnsId: props.item.qnsId,
answer: submitObj,
answerItems: []
});
};
// 题类型
const subject_type_text = computed(() => SUBJECT_TYPE.find((res) => qnsTyp.value === res.value)?.label || '');
// 点击答题
const click_option = (analysisVo) => {
if (props.isPreview || !props.clearResult) return;
// 无论什么类型如果表里已经有了,再次点击就是弹出
if (item_answer.value.includes(analysisVo.itemId)) {
item_answer.value = item_answer.value.filter((id) => id !== analysisVo.itemId);
modelValue.value = item_answer.value;
if (!['study'].includes(props.mode)) {
muCheckbox('choose');
}
} else if (props.item.qnsTyp === 'SN') {
// 单选
// 单选插入,并且移除其他
item_answer.value[0] = analysisVo.itemId;
modelValue.value = item_answer.value;
muCheckbox('choose');
} else if (props.item.qnsTyp === 'MU') {
// 多选题
// 多选直接插入
item_answer.value.push(analysisVo.itemId);
modelValue.value = item_answer.value;
if (!['study'].includes(props.mode)) {
muCheckbox('choose');
}
} else if (props.item.qnsTyp === 'JD') {
// 判断题
// 判断插入,并且移除其他
item_answer.value = [analysisVo.itemId];
modelValue.value = item_answer.value;
muCheckbox('choose');
}
};
const muBtnClick = () => {
if(item_answer.value.length === 0){
common.msg('请至少选择一个选项!')
return
}
muCheckbox('answer')
}
const muCheckbox = (type = 'answer') => {
emit('subject_click', type, {
qnsTyp: props.item.qnsTyp,
qnsId: props.item.qnsId,
answer: item_answer.value,
answerItems: subject_data.value.itemVos.filter((t) => item_answer.value.includes(t.itemId))
});
};
// 初始化动画
onMounted(() => {});
</script>
<style scoped lang="scss">
.expand_button_div {
/* 相对定位,让两个文本重叠 */
// display: inline-block;
// cursor: pointer;
}
.toggle-container {
position: relative;
height: 42rpx;
width: 60rpx;
font-size: 30rpx;
line-height: 42rpx;
}
.toggle-text {
/* 绝对定位使文本重叠 */
position: absolute;
top: 0;
left: 0;
/* 初始状态:非激活文本透明且轻微下移 */
opacity: 0;
// transform: translateY(15rpx);
/* 添加过渡动画 */
transition: all 0.2s ease-out;
/* 确保文本占据空间一致 */
white-space: nowrap;
}
/* 激活状态:文本可见且位置正常 */
.toggle-text.active {
opacity: 1;
transform: translateY(0);
z-index: 1; /* 确保激活文本在上方 */
}
.collapse_wrapper {
overflow: hidden; /* 关键:隐藏超出部分 */
// height: 0; /* 初始高度为0 */
transition: height 0.3s ease-out;
}
.click_text_icon_action {
transform: rotate(-90deg);
}
.click_text_icon {
transition: transform 0.3s ease-out;
}
.mark_and_feedback_div {
display: flex;
height: 100%;
justify-content: end;
align-items: center;
.img {
width: 38rpx;
height: 38rpx;
margin-left: 4rpx;
padding: 4rpx;
}
}
.expand_button_div {
display: flex;
align-items: center;
width: 90rpx;
justify-content: space-between;
}
.option_div {
padding-top: 20rpx;
}
.option_div_button {
width: 100%;
height: 84rpx;
padding-top: 30rpx;
// background-color: red;
display: flex;
flex-direction: column;
align-items: center;
}
.analysis_div {
.analysis_title {
display: flex;
justify-content: space-between;
align-items: center;
margin: 32rpx 0 0 0;
.success_answer {
}
.your_answer {
.success {
color: #15b859;
}
.error {
color: #f5212d;
}
}
}
.analysis_note {
font-family: PingFangSC;
font-weight: 500;
font-size: 30rpx;
color: #666666;
line-height: 48rpx;
text-align: left;
font-style: normal;
margin-top: 28rpx;
}
.reference_answer {
.reference_answer_title {
font-weight: 700;
font-size: 30rpx;
color: #333333;
text-align: left;
line-height: 30rpx;
margin: 30rpx 0;
}
font-weight: 400;
font-size: 30rpx;
color: #666666;
line-height: 48rpx;
text-align: left;
}
}
.item {
// height: 170rpx;
overflow: hidden;
display: flex;
flex-direction: column;
padding-bottom: 40rpx;
.top {
display: flex;
align-items: center;
.type {
width: 108rpx;
height: 46rpx;
background: #267eff;
border-radius: 8rpx;
font-family: PingFangSC;
font-weight: 500;
font-size: 25rpx;
color: #ffffff;
text-align: center;
line-height: 46rpx;
}
.score {
margin-left: 24rpx;
font-family: ArialMT;
font-size: 30rpx;
color: #333333;
text-align: left;
}
.right {
display: flex;
align-items: center;
.right_text {
font-family: PingFangSC;
font-weight: 400;
font-size: 28rpx;
color: #333333;
line-height: 40px;
text-align: left;
font-style: normal;
}
}
}
.question_text {
font-family: PingFangSC;
font-weight: 500;
font-size: 30rpx;
color: #333333;
line-height: 48rpx;
text-align: left;
font-style: normal;
margin-top: 20rpx;
}
}
</style>