2026-02-14 11:28:51 +08:00

772 lines
19 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="grades-page flex flex-col">
<!-- 数据加载遮罩 -->
<view v-if="pageLoading" class="page-loading-mask">
<view class="page-loading-box">
<view class="page-loading-spinner"></view>
<text class="page-loading-text">加载中...</text>
</view>
</view>
<!-- 顶部蓝色背景与教师端一致 -->
<view class="blue-header">
<view class="header-content">
<text class="exam-title">{{ kscc.ksmc || '考试场次' }}</text>
<view class="student-info-section">
<text class="student-name">{{ curXs.xm || curXs.name || '学生姓名' }}</text>
<text class="student-class" v-if="curXs.njmc || curXs.bjmc">
{{ [curXs.njmc, curXs.bjmc].filter(Boolean).join(' ') }}
</text>
</view>
<view class="score-info-section">
<view class="score-left">
<text class="total-score">{{ ksccKmList.length }}</text>
<text class="full-score">满分{{ totalKmFs }}</text>
</view>
<text class="grade">{{ curKsdj.dj || '-' }}</text>
</view>
</view>
</view>
<!-- 白色内容区 -->
<view style="background-color: #f6f6f6;">
<view class="content-area">
<!-- 选项卡去掉分数趋势 -->
<view class="tabs">
<view
v-if="ksccKmList && ksccKmList.length > 2"
class="tab-item"
:class="{ active: activeTab === 'diagnosis' }"
@click="switchTab('diagnosis')"
>
<text>学科诊断</text>
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'scores' }"
@click="switchTab('scores')"
>
<text>学科成绩</text>
</view>
</view>
<!-- 学科成绩视图与教师端布局一致分数按 sfXsFs 显示 -->
<view class="score-view" v-if="activeTab === 'scores'">
<view v-if="ksccKscjList.length > 0" class="score-content">
<view
class="subject-item"
v-for="(kscj, index) in ksccKscjList"
:key="index"
>
<view class="subject-header">
<text class="subject-name">{{ kscj.km?.kmmc || kscj.kmmc || '未知科目' }}</text>
<text class="detail-btn" :style="{ color: kscj.djclr || '#909399' }" v-if="kscj.dj">{{ kscj.djBx }}</text>
</view>
<view class="subject-body">
<text class="subject-grade" :style="{ color: kscj.djclr || '#303133' }">{{ kscj.dj || '-' }}</text>
<view class="grade-info-wrapper">
<text v-if="kscc.sfXsFs && kscj.ksfs" class="subject-score">{{ kscj.ksfs }}</text>
</view>
</view>
</view>
</view>
<view v-else class="empty-scores">
<text>暂无成绩数据</text>
</view>
<!-- 班主任寄语家长端只读展示 -->
<view class="comment-section">
<view class="comment-header">
<text class="comment-title">班主任寄语</text>
</view>
<view class="comment-text-wrap">
<text class="comment-text">{{ pjBzr || '暂无寄语' }}</text>
</view>
</view>
</view>
<!-- 学科诊断视图 -->
<view class="diagnosis-view flex-1 po-re" v-if="activeTab === 'diagnosis'">
<view class="po-ab inset-0 p-15" style="overflow: auto">
<view class="radar-placeholder" id="chart-container">
<canvas
style="width: 100%; height: 100%"
canvas-id="radarCanvas"
id="radarCanvas"
class="charts"
/>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted, watch, nextTick } from "vue";
import uCharts from "@/components/charts/u-charts.js";
import dayjs from "dayjs";
import { xsKscjApi, ksccPjFindByKsccAndXsApi } from "@/api/base/server";
import { useUserStore } from "@/store/modules/user";
import { useDataStore } from "@/store/modules/data";
const { getCurXs } = useUserStore();
const { getData } = useDataStore();
dayjs.locale("zh-cn");
const kmTabsRef = ref<any>(null)
const fsScale = 100;
const curXs = ref<any>({})
const kscc = ref<any>({})
const kscjList = ref<any>([])
const curKsdj = ref<any>({})
// 当前选中的选项卡
const activeTab = ref("scores");
// 页面数据加载中(显示遮罩)
const pageLoading = ref<boolean>(false);
// 班主任寄语(家长端只读)
const pjBzr = ref<string>("");
// 切换选项卡
const switchTab = (tab: string) => {
activeTab.value = tab;
}
type ColorMapType = {
[key: string]: string;
};
// 等级颜色与教师端一致
const colorMap: ColorMapType = {
"A": "#4bb604", // 优秀 - 绿色
"B": "#FF8C00", // 良好 - 橙色
"C": "#FFD700", // 中等 - 黄色
"D": "#FF0000", // 及格 - 红色
"E": "#666666", // 不及格 - 灰色
};
// 雷达图数据
const radarData = {
categories: ["语文", "数学", "英语", "科学", "音乐", "美术", "体育"],
series: [{ name: "分数", data: [85, 90, 88, 92, 86, 91, 89] }],
};
// 所有科目列表
const sysKmList = ref<any>([]);
// 当前考试科目列表
const ksccKmList = ref<any>([]);
// 科目卷面分总数
const totalKmFs = ref<number>(0);
// 科目等级列表
const kmDjList = ref<any>([]);
// 考试场次等级列表
const djList = ref<any>([]);
// 考试场次成绩列表
const ksccKscjList = ref<any>([]);
// 绘制雷达图
const drawRadarChart = () => {
nextTick(() => {
try {
// 获取容器信息
const query = uni.createSelectorQuery();
query
.select("#chart-container")
.boundingClientRect((data) => {
if (!data) {
console.error("未找到雷达图容器");
return;
}
// 安全类型判断
const rect = data as any;
const canvasWidth = rect.width || 300;
const canvasHeight = rect.height || 300;
const ctx = uni.createCanvasContext("radarCanvas");
// 绘制雷达图配置
const options = {
type: "radar",
context: ctx,
width: canvasWidth,
height: canvasHeight,
categories: radarData.categories,
series: radarData.series,
animation: true,
background: "#FFFFFF",
padding: [10, 10, 10, 10],
dataLabel: false,
legend: {
show: false,
},
extra: {
radar: {
gridType: "radar",
gridColor: "#CCCCCC",
gridCount: 3,
opacity: 0.2,
labelShow: true,
},
},
};
new uCharts(options);
})
.exec();
} catch (error) {
console.error("绘制雷达图出错:", error);
}
});
};
// 监听选项卡变化,重新渲染对应图表
watch(activeTab, (newValue) => {
if (newValue === "diagnosis") {
setTimeout(drawRadarChart, 50);
}
});
// Float乘以倍数然后四舍五入转int
const floatToInt = (floatNum: number, scale: number) => {
return Math.round(floatNum * scale);
}
let singleFlag = false;
// 初始化考试等级列表
const initKsdj = () => {
let maxDj = 0.0;
let maxKmDj = 0.0;
djList.value.forEach((ksdj: any) => {
ksdj.zdf = floatToInt(ksdj.zdf, fsScale); // 等级最低分
ksdj.zgf = floatToInt(ksdj.zgf, fsScale); // 等级最高分
ksdj.djclr = colorMap[ksdj.dj] || ''; // 等级颜色
maxDj = Math.max(maxDj, ksdj.zgf);
});
kmDjList.value.forEach((kmdj: any) => {
kmdj.zdf = floatToInt(kmdj.zdf, fsScale); // 等级最低分
kmdj.zgf = floatToInt(kmdj.zgf, fsScale); // 等级最高分
kmdj.djclr = colorMap[kmdj.dj] || ''; // 等级颜色
maxKmDj = Math.max(maxKmDj, kmdj.zgf);
});
singleFlag = maxDj < maxKmDj + 100;
};
const rebuildData = () => {
let ksfsList: any[] = [];
let totalFs = 0.00;
ksccKscjList.value = ksccKscjList.value.map((cj: any) => {
ksfsList.push(cj.ksfs);
totalFs += cj.ksfs;
let fs = floatToInt(cj.ksfs, fsScale);
cj.djclr = colorMap[cj.dj] || '';
// 查询考试等级
cj.km = ksccKmList.value.find((item: any) => item.kmId === cj.kmId) || {};
return cj;
});
// 雷达图数据
radarData.categories = ksccKmList.value.map((item: any) => {
totalKmFs.value += item.kmfs;
return item.kmmc;
});
radarData.series = [{ name: "分数", data: ksfsList }];
// 当前成绩列表
totalFs = floatToInt(totalFs, fsScale);
if (singleFlag) {
totalFs = totalFs / ksccKmList.value.length;
}
curKsdj.value = djList.value.find((item: any) => item.zdf <= totalFs && item.zgf >= totalFs) || {};
}
onMounted(async () => {
pageLoading.value = true;
try {
curXs.value = getCurXs;
kscc.value = getData;
const res = await xsKscjApi({
xsId: getCurXs.id,
ksccId: getData.id,
njmcId: getData.njmcId
});
if (res.resultCode == 1) {
sysKmList.value = res.result.kmList || [];
ksccKmList.value = res.result.ksccKmList || [];
kmDjList.value = res.result.kmDjList || [];
djList.value = res.result.djList || [];
ksccKscjList.value = res.result.ksccKscjList || [];
initKsdj();
rebuildData();
}
// 加载班主任寄语
try {
const pjRes = await ksccPjFindByKsccAndXsApi({
ksccId: getData.id,
xsId: getCurXs.id,
});
if (pjRes && pjRes.resultCode === 1 && pjRes.result && pjRes.result.pjBzr) {
pjBzr.value = pjRes.result.pjBzr;
}
} catch (_) {}
setTimeout(() => {
if (activeTab.value === "diagnosis") {
drawRadarChart();
}
}, 300);
} finally {
pageLoading.value = false;
}
});
</script>
<style lang="scss" scoped>
.grades-page {
background-color: #f8f8f8;
min-height: 100vh;
display: flex;
flex-direction: column;
padding-bottom: 30px;
}
/* 数据加载遮罩 */
.page-loading-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.45);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
}
.page-loading-box {
display: flex;
flex-direction: column;
align-items: center;
padding: 24px 32px;
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
}
.page-loading-spinner {
width: 40px;
height: 40px;
border: 3px solid #e8e8e8;
border-top-color: #2672ff;
border-radius: 50%;
animation: page-loading-spin 0.8s linear infinite;
}
.page-loading-text {
margin-top: 12px;
font-size: 14px;
color: #666;
}
@keyframes page-loading-spin {
to {
transform: rotate(360deg);
}
}
/* 顶部蓝色背景(与教师端一致) */
.blue-header {
background-color: #2672ff;
padding: 15px 15px 20px 15px;
position: relative;
color: white;
box-sizing: border-box;
.back-icon {
position: absolute;
top: 15px;
left: 15px;
}
.more-icon {
position: absolute;
top: 15px;
right: 15px;
}
.header-content {
padding-top: 15px;
padding-bottom: 15px;
display: flex;
flex-direction: column;
align-items: center;
.exam-title {
font-size: 18px;
font-weight: 500;
text-align: center;
margin-bottom: 10px;
opacity: 0.95;
}
.student-info-section {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-bottom: 10px;
width: 100%;
gap: 8px;
.student-name {
font-size: 22px;
font-weight: 600;
text-align: center;
}
.student-class {
font-size: 15px;
text-align: center;
opacity: 0.9;
padding: 4px 12px;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 12px;
}
}
.score-info-section {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 100%;
padding: 0 20px;
gap: 30px;
.score-left {
display: flex;
flex-direction: column;
align-items: center;
.total-score {
font-size: 14px;
margin-bottom: 5px;
opacity: 0.9;
}
.full-score {
font-size: 14px;
opacity: 0.9;
}
}
.grade {
font-size: 48px;
font-weight: bold;
line-height: 1;
}
}
}
}
/* 内容区域(与教师端一致) */
.content-area {
background-color: white;
padding: 20px 20px 40px 20px;
margin-top: -20px;
border-top-left-radius: 20px;
border-top-right-radius: 20px;
min-height: calc(100vh - 200px);
/* 选项卡 */
.tabs {
display: flex;
border-bottom: 1px solid #ebeef5;
.tab-item {
flex: 1;
text-align: center;
padding: 15px 0;
position: relative;
color: #606266;
font-size: 15px;
&.active {
color: #1976d2;
font-weight: 500;
&::after {
content: "";
position: absolute;
bottom: 0;
left: 25%;
width: 50%;
height: 2px;
background-color: #1976d2;
}
}
}
}
/* 学科成绩视图(与教师端一致) */
.score-view {
margin-top: 15px;
display: flex;
flex-direction: column;
.score-content {
padding: 0;
flex: 1;
}
.subject-item {
padding: 15px 0;
border-bottom: 1px solid #ebeef5;
&:last-child {
border-bottom: none;
}
.subject-header {
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: space-between;
.subject-name {
font-size: 15px;
color: #606266;
font-weight: 500;
}
.detail-btn {
font-size: 15px;
font-weight: 500;
color: #909399;
}
}
.subject-body {
display: flex;
align-items: center;
justify-content: space-between;
.subject-grade {
font-size: 24px;
font-weight: bold;
color: #303133;
}
.grade-info-wrapper {
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: center;
.subject-score {
font-size: 24px;
font-weight: bold;
color: #303133;
line-height: 1;
}
}
}
}
.empty-scores {
padding: 50px 15px;
text-align: center;
color: #999;
font-size: 14px;
}
/* 班主任寄语(家长端只读) */
.comment-section {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #ebeef5;
background-color: #fff;
.comment-header {
margin-bottom: 10px;
}
.comment-title {
font-size: 16px;
font-weight: 500;
color: #303133;
}
.comment-text-wrap {
padding: 12px 0;
}
.comment-text {
font-size: 14px;
color: #606266;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
}
}
}
.radar-placeholder {
height: 300px;
background-color: #f8f8f8;
border-radius: 8px;
margin-bottom: 20px;
display: flex;
align-items: center;
justify-content: center;
.placeholder-text {
color: #909399;
font-size: 16px;
}
}
/* 学科诊断视图 */
.diagnosis-view {
padding: 15px;
.diagnosis-comment {
padding: 15px;
border-radius: 8px;
background-color: #f8f8f8;
.comment-title {
font-size: 16px;
font-weight: 500;
color: #303133;
margin-bottom: 10px;
display: block;
}
.comment-text {
font-size: 14px;
color: #606266;
line-height: 1.6;
margin-bottom: 10px;
display: block;
}
}
}
}
/* 学生选择器弹窗样式 */
.student-selector {
background: linear-gradient(135deg, #ffffff 0%, #f8faff 100%);
border-top-left-radius: 20px;
border-top-right-radius: 20px;
padding-bottom: 30px;
.selector-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #f0f2f5;
.selector-title {
font-size: 18px;
font-weight: 600;
color: #303133;
line-height: 1;
}
.close-btn {
width: 32px;
height: 32px;
background-color: #f5f7fa;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
flex-shrink: 0;
&:active {
transform: scale(0.9);
background-color: #e6e8eb;
}
}
}
.student-list {
padding: 0 20px;
max-height: 50%;
overflow-y: auto;
.student-item {
display: flex;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid #f5f7fa;
transition: all 0.3s ease;
border-radius: 12px;
margin-bottom: 8px;
&:last-child {
border-bottom: none;
margin-bottom: 0;
}
&-active {
background: linear-gradient(135deg, rgba(74, 144, 226, 0.08) 0%, rgba(53, 122, 189, 0.05) 100%);
padding: 16px 12px;
}
.student-avatar {
width: 50px;
height: 50px;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(74, 144, 226, 0.15);
border: 2px solid #ffffff;
.avatar-img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.student-info {
flex: 1;
margin-left: 15px;
display: flex;
flex-direction: column;
justify-content: center;
min-width: 0;
.student-name {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
line-height: 1.2;
}
.student-class {
font-size: 13px;
color: #606266;
font-weight: 400;
line-height: 1;
}
}
.check-icon {
width: 24px;
height: 24px;
border-radius: 50%;
background: linear-gradient(135deg, #4A90E2 0%, #357ABD 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(74, 144, 226, 0.3);
flex-shrink: 0;
}
}
}
}
</style>