zhxy-jsd/src/pages/view/homeSchool/ChengJiFenXi.vue
2025-04-22 10:22:33 +08:00

919 lines
28 KiB
Vue

<template>
<BasicLayout :show-nav-bar="true" :nav-bar-props="{ title: '成绩分析' }">
<view class="grade-analysis-page">
<!-- 1. Class Selector -->
<view class="selector-section class-selector">
<picker
mode="selector"
:range="classList"
range-key="name"
:value="selectedClassIndex"
@change="onClassChange"
>
<view class="picker-item">
<text>{{ selectedClassName || "选择班级" }}</text>
<uni-icons type="bottom" size="16" color="#666"></uni-icons>
</view>
</picker>
</view>
<!-- 2. Subject Tabs -->
<view class="selector-section subject-tabs">
<scroll-view
scroll-x="true"
class="scroll-view-h"
:show-scrollbar="false"
>
<view
class="tab-item"
v-for="subject in subjectList"
:key="subject.id"
:class="{ active: selectedSubjectId === subject.id }"
@click="selectSubject(subject.id)"
>
<text>{{ subject.name }}</text>
</view>
</scroll-view>
</view>
<!-- 3. Student Score Table -->
<uni-section title="学生成绩" type="line" titleFontSize="16px" padding>
<view class="score-table">
<view class="table-header">
<view class="th">姓名</view>
<view class="th">得分</view>
<view class="th">年级排名</view>
</view>
<view class="table-body">
<view
class="table-row"
v-for="student in studentScores"
:key="student.id"
>
<view class="td">{{ student.name }}</view>
<view class="td">{{ student.score }}</view>
<view class="td">{{ student.rank }}</view>
</view>
<view v-if="!studentScores.length" class="table-empty">
暂无数据
</view>
</view>
</view>
</uni-section>
<!-- 4. Summary Statistics -->
<uni-section title="统计概览" type="line" titleFontSize="16px" padding>
<view class="summary-stats-card">
<view class="stats-grid">
<view class="stat-item">
<text class="label">班级人数</text>
<text class="value">{{ summaryStats.classCount }}</text>
</view>
<view class="stat-item">
<text class="label">年级平均总分</text>
<text class="value primary">{{
summaryStats.gradeAvgTotal
}}</text>
</view>
<view class="stat-item">
<text class="label">班级平均分</text>
<text class="value primary">{{ summaryStats.classAvg }}</text>
</view>
<view class="stat-item">
<text class="label">年级最高分</text>
<text class="value primary">{{ summaryStats.gradeMax }}</text>
</view>
<view class="stat-item">
<text class="label">优生率</text>
<text class="value success"
>{{ summaryStats.excellentRate }}%</text
>
</view>
<view class="stat-item">
<text class="label">班级最高分</text>
<text class="value primary">{{ summaryStats.classMax }}</text>
</view>
<view class="stat-item">
<text class="label">优生人数</text>
<text class="value">{{ summaryStats.excellentCount }}</text>
</view>
<view class="stat-item">
<text class="label">及格率</text>
<text class="value success">{{ summaryStats.passRate }}%</text>
</view>
<view class="stat-item">
<text class="label">及格人数</text>
<text class="value">{{ summaryStats.passCount }}</text>
</view>
</view>
</view>
</uni-section>
<!-- 5. Charts -->
<uni-section
title="本班与年级平均分对比分析图表"
type="line"
titleFontSize="16px"
padding
>
<view class="chart-container" id="line-chart-container">
<!-- Use canvas for native rendering -->
<canvas
canvas-id="GradeAnalysisLineChart"
id="GradeAnalysisLineChart"
class="charts"
/>
<view v-if="isLoading" class="chart-placeholder"
>图表加载中或无数据...</view
>
</view>
</uni-section>
<uni-section title="总分分数段" type="line" titleFontSize="16px" padding>
<view class="chart-container" id="donut-chart-container">
<!-- Use canvas for native rendering -->
<canvas
canvas-id="GradeAnalysisDonutChart"
id="GradeAnalysisDonutChart"
class="charts"
/>
<view v-if="isLoading" class="chart-placeholder"
>图表加载中或无数据...</view
>
</view>
</uni-section>
<uni-section title="总分等级段" type="line" titleFontSize="16px" padding>
<view class="chart-container" id="area-chart-container">
<!-- Use canvas for native rendering -->
<canvas
canvas-id="GradeAnalysisAreaChart"
id="GradeAnalysisAreaChart"
class="charts"
/>
<view v-if="isLoading" class="chart-placeholder"
>图表加载中或无数据...</view
>
</view>
</uni-section>
</view>
</BasicLayout>
</template>
<script lang="ts" setup>
import {
ref,
computed,
onMounted,
nextTick,
getCurrentInstance,
ComponentInternalInstance,
} from "vue";
import uCharts from "@/components/charts/u-charts.js";
// --- Interfaces ---
interface ClassInfo { id: string | number; name: string; }
interface Subject { id: string | number; name: string; }
interface StudentScore { id: string | number; name: string; score: number; rank: number; }
interface SummaryStats { classCount: number; gradeAvgTotal: number; classAvg: number; gradeMax: number; excellentRate: number; classMax: number; excellentCount: number; passRate: number; passCount: number; }
// --- Static Mock Data ---
const classList = ref<ClassInfo[]>([
{ id: "c1", name: "五年级(1)班" },
{ id: "c2", name: "五年级(2)班" },
{ id: "c3", name: "五年级(3)班" },
{ id: "c4", name: "六年级(1)班" },
{ id: "c5", name: "六年级(2)班" },
]);
const staticSubjectList: Subject[] = [
{ id: "subj-yuwen", name: "语文" },
{ id: "subj-shuxue", name: "数学" },
{ id: "subj-yingyu", name: "英语" },
{ id: "subj-kexue", name: "科学" },
{ id: "subj-daofa", name: "道法" },
];
const staticStudentScores: StudentScore[] = [
{ id: "st-1", name: "张三", score: 95, rank: 1 },
{ id: "st-2", name: "李四", score: 88, rank: 5 },
{ id: "st-3", name: "王五", score: 92, rank: 3 },
{ id: "st-4", name: "赵六", score: 75, rank: 15 },
{ id: "st-5", name: "孙七", score: 85, rank: 8 },
];
const staticSummaryStats: SummaryStats = { classCount: 45, gradeAvgTotal: 82.5, classAvg: 85.1, gradeMax: 99, excellentRate: 75.6, classMax: 98, excellentCount: 34, passRate: 95.6, passCount: 43 };
// --- State ---
const selectedClassIndex = ref<number>(0);
const selectedClassId = ref<string | number | null>(classList.value[0]?.id || null);
const subjectList = ref<Subject[]>([...staticSubjectList]);
const selectedSubjectId = ref<string | number | null>(subjectList.value[0]?.id || null);
const studentScores = ref<StudentScore[]>([...staticStudentScores]);
const summaryStats = ref<SummaryStats>({ ...staticSummaryStats });
const isLoading = ref(false);
// --- Computed ---
const selectedClassName = computed(() => classList.value[selectedClassIndex.value]?.name);
// --- Event Handlers ---
const onClassChange = (event: any) => {
const index = Number(event.detail.value);
selectedClassIndex.value = index;
selectedClassId.value = classList.value[index]?.id || null;
selectedSubjectId.value = subjectList.value[0]?.id || null;
console.log(`切换到班级: ${selectedClassId.value}, 默认科目: ${selectedSubjectId.value}`);
};
const selectSubject = (subjectId: string | number) => {
selectedSubjectId.value = subjectId;
console.log(`选中科目: ${subjectId}`);
if (subjectId === 'subj-shuxue') {
summaryStats.value.classAvg = 88.2;
studentScores.value = [...staticStudentScores].map(s => s.id === 'st-1' ? {...s, score: 98} : s);
} else if (subjectId === 'subj-yingyu') {
summaryStats.value.classAvg = 80.5;
studentScores.value = [...staticStudentScores].map(s => s.id === 'st-2' ? {...s, score: 90} : s);
} else {
summaryStats.value = { ...staticSummaryStats };
studentScores.value = [...staticStudentScores];
}
};
// --- 图表绘制逻辑 (保持不变) ---
const drawChartBase = (
containerId: string,
canvasId: string,
type: string,
data: any
) => {
nextTick(() => {
// 使用 uni.$scope 获取当前页面/组件实例 (根据你的要求)
// @ts-ignore - Ignore $scope error as requested
uni
.createSelectorQuery()
.in(uni.$scope)
.select(`#${containerId}`)
.boundingClientRect((rect) => {
if (
rect &&
!Array.isArray(rect) &&
typeof rect.width === "number" &&
rect.width > 0 &&
typeof rect.height === "number" &&
rect.height > 0
) {
const containerWidth = rect.width;
const containerHeight = rect.height;
// @ts-ignore - Ignore $scope error as requested
const ctx = uni.createCanvasContext(canvasId, uni.$scope);
let specificOptions = {}; // 用于存放特定图表类型的配置
switch (type) {
case "line":
specificOptions = new uCharts({
type: "line",
context: ctx,
width: containerWidth,
height: containerHeight,
categories: data.categories,
series: data.series,
animation: true,
timing: "easeOut",
duration: 1000,
rotate: false,
rotateLock: false,
background: "#FFFFFF",
color: [
"#1890FF",
"#91CB74",
"#FAC858",
"#EE6666",
"#73C0DE",
"#3CA272",
"#FC8452",
"#9A60B4",
"#ea7ccc",
],
padding: [15, 10, 0, 15],
fontSize: 13,
fontColor: "#666666",
dataLabel: true,
dataPointShape: true,
dataPointShapeType: "solid",
touchMoveLimit: 60,
enableScroll: false,
enableMarkLine: false,
legend: {
show: false,
position: "bottom",
float: "center",
padding: 5,
margin: 5,
backgroundColor: "rgba(0,0,0,0)",
borderColor: "rgba(0,0,0,0)",
borderWidth: 0,
fontSize: 13,
fontColor: "#666666",
lineHeight: 11,
hiddenColor: "#CECECE",
itemGap: 10,
},
xAxis: {
disableGrid: true,
disabled: false,
axisLine: true,
axisLineColor: "#CCCCCC",
calibration: false,
fontColor: "#666666",
fontSize: 13,
lineHeight: 20,
marginTop: 0,
rotateLabel: false,
rotateAngle: 45,
itemCount: 5,
boundaryGap: "center",
splitNumber: 5,
gridColor: "#CCCCCC",
gridType: "solid",
dashLength: 4,
gridEval: 1,
scrollShow: false,
scrollAlign: "left",
scrollColor: "#A6A6A6",
scrollBackgroundColor: "#EFEBEF",
title: "",
titleFontSize: 13,
titleOffsetY: 0,
titleOffsetX: 0,
titleFontColor: "#666666",
formatter: "",
},
yAxis: {
gridType: "dash",
dashLength: 2,
disabled: false,
disableGrid: false,
splitNumber: 5,
gridColor: "#CCCCCC",
padding: 10,
showTitle: false,
data: [],
},
extra: {
line: {
type: "straight",
width: 2,
activeType: "hollow",
linearType: "none",
onShadow: false,
animation: "vertical",
},
tooltip: {
showBox: true,
showArrow: true,
showCategory: false,
borderWidth: 0,
borderRadius: 0,
borderColor: "#000000",
borderOpacity: 0.7,
bgColor: "#000000",
bgOpacity: 0.7,
gridType: "solid",
dashLength: 4,
gridColor: "#CCCCCC",
boxPadding: 3,
fontSize: 13,
lineHeight: 20,
fontColor: "#FFFFFF",
legendShow: true,
legendShape: "auto",
splitLine: true,
horizentalLine: false,
xAxisLabel: false,
yAxisLabel: false,
labelBgColor: "#FFFFFF",
labelBgOpacity: 0.7,
labelFontColor: "#666666",
},
markLine: {
type: "solid",
dashLength: 4,
data: [],
},
},
});
break;
case "area":
specificOptions = new uCharts({
type: "area",
context: ctx,
width: containerWidth,
height: containerHeight,
categories: data.categories,
series: data.series,
animation: true,
timing: "easeOut",
duration: 1000,
rotate: false,
rotateLock: false,
background: "#FFFFFF",
color: [
"#1890FF",
"#91CB74",
"#FAC858",
"#EE6666",
"#73C0DE",
"#3CA272",
"#FC8452",
"#9A60B4",
"#ea7ccc",
],
padding: [15, 15, 0, 15],
fontSize: 13,
fontColor: "#666666",
dataLabel: true,
dataPointShape: true,
dataPointShapeType: "solid",
touchMoveLimit: 60,
enableScroll: false,
enableMarkLine: false,
legend: {
show: false,
position: "bottom",
float: "center",
padding: 5,
margin: 5,
backgroundColor: "rgba(0,0,0,0)",
borderColor: "rgba(0,0,0,0)",
borderWidth: 0,
fontSize: 13,
fontColor: "#666666",
lineHeight: 11,
hiddenColor: "#CECECE",
itemGap: 10,
},
xAxis: {
disableGrid: true,
disabled: false,
axisLine: true,
axisLineColor: "#CCCCCC",
calibration: false,
fontColor: "#666666",
fontSize: 13,
lineHeight: 20,
marginTop: 0,
rotateLabel: false,
rotateAngle: 45,
itemCount: 5,
boundaryGap: "center",
splitNumber: 5,
gridColor: "#CCCCCC",
gridType: "solid",
dashLength: 4,
gridEval: 1,
scrollShow: false,
scrollAlign: "left",
scrollColor: "#A6A6A6",
scrollBackgroundColor: "#EFEBEF",
title: "",
titleFontSize: 13,
titleOffsetY: 0,
titleOffsetX: 0,
titleFontColor: "#666666",
formatter: "",
},
yAxis: {
gridType: "dash",
dashLength: 2,
disabled: false,
disableGrid: false,
splitNumber: 5,
gridColor: "#CCCCCC",
padding: 10,
showTitle: false,
data: [],
},
extra: {
area: {
type: "straight",
opacity: 0.2,
addLine: true,
width: 2,
gradient: false,
activeType: "hollow",
},
tooltip: {
showBox: true,
showArrow: true,
showCategory: false,
borderWidth: 0,
borderRadius: 0,
borderColor: "#000000",
borderOpacity: 0.7,
bgColor: "#000000",
bgOpacity: 0.7,
gridType: "solid",
dashLength: 4,
gridColor: "#CCCCCC",
boxPadding: 3,
fontSize: 13,
lineHeight: 20,
fontColor: "#FFFFFF",
legendShow: true,
legendShape: "auto",
splitLine: true,
horizentalLine: false,
xAxisLabel: false,
yAxisLabel: false,
labelBgColor: "#FFFFFF",
labelBgOpacity: 0.7,
labelFontColor: "#666666",
},
markLine: {
type: "solid",
dashLength: 4,
data: [],
},
},
});
break;
case "ring":
specificOptions = new uCharts({
type: "ring",
context: ctx,
width: containerWidth,
height: containerHeight,
series: data.series,
animation: true,
timing: "easeOut",
duration: 1000,
rotate: false,
rotateLock: false,
background: "#FFFFFF",
color: [
"#1890FF",
"#91CB74",
"#FAC858",
"#EE6666",
"#73C0DE",
"#3CA272",
"#FC8452",
"#9A60B4",
"#ea7ccc",
],
padding: [5, 5, 5, 5],
fontSize: 13,
fontColor: "#666666",
dataLabel: true,
dataPointShape: true,
dataPointShapeType: "solid",
touchMoveLimit: 60,
enableScroll: false,
enableMarkLine: false,
legend: {
show: true,
position: "right",
lineHeight: 25,
float: "center",
padding: 5,
margin: 5,
backgroundColor: "rgba(0,0,0,0)",
borderColor: "rgba(0,0,0,0)",
borderWidth: 0,
fontSize: 13,
fontColor: "#666666",
hiddenColor: "#CECECE",
itemGap: 10,
},
title: {
name: "",
fontSize: 15,
color: "#666666",
offsetX: 0,
offsetY: 0,
},
subtitle: {
name: "",
fontSize: 25,
color: "#7cb5ec",
offsetX: 0,
offsetY: 0,
},
extra: {
ring: {
ringWidth: 30,
activeOpacity: 0.5,
activeRadius: 10,
offsetAngle: 0,
labelWidth: 15,
border: true,
borderWidth: 3,
borderColor: "#FFFFFF",
centerColor: "#FFFFFF",
customRadius: 0,
linearType: "none",
},
tooltip: {
showBox: true,
showArrow: true,
showCategory: false,
borderWidth: 0,
borderRadius: 0,
borderColor: "#000000",
borderOpacity: 0.7,
bgColor: "#000000",
bgOpacity: 0.7,
gridType: "solid",
dashLength: 4,
gridColor: "#CCCCCC",
boxPadding: 3,
fontSize: 13,
lineHeight: 20,
fontColor: "#FFFFFF",
legendShow: true,
legendShape: "auto",
splitLine: true,
horizentalLine: false,
xAxisLabel: false,
yAxisLabel: false,
labelBgColor: "#FFFFFF",
labelBgOpacity: 0.7,
labelFontColor: "#666666",
},
},
});
break;
}
// 使用传入的 type 和合并后的配置创建图表
new uCharts(specificOptions);
} else {
console.error(`无法获取容器 #${containerId} 的有效尺寸:`, rect);
}
})
.exec();
});
};
const drawLineChart = (containerId: string, canvasId: string, data: any) => {
if (!data || !data.categories || !data.series) {
console.warn(`折线图 (${canvasId}) 数据无效`);
return;
}
console.log(`准备绘制折线图: ${canvasId}`);
drawChartBase(containerId, canvasId, "line", data);
};
const drawDonutChart = (containerId: string, canvasId: string, data: any) => {
if (!data || !data.series || !data.series[0]?.data) {
console.warn(`圆环图 (${canvasId}) 数据无效`);
return;
}
console.log(`准备绘制圆环图: ${canvasId}`);
drawChartBase(containerId, canvasId, "ring", { series: data.series });
};
const drawAreaChart = (containerId: string, canvasId: string, data: any) => {
if (!data || !data.categories || !data.series) {
console.warn(`区域图 (${canvasId}) 数据无效`);
return;
}
console.log(`准备绘制区域图: ${canvasId}`);
drawChartBase(containerId, canvasId, "area", data);
};
// --- 数据获取与触发 ---
const getServerData = () => {
setTimeout(() => {
let lineRes = { categories: ["2018", "2019", "2020", "2021", "2022", "2023"], series: [{ name: "本班平均分", data: [75, 78, 82, 80, 85, 83] }, { name: "年级平均分", data: [70, 72, 75, 74, 78, 77] }] };
let donutRes = { series: [{ name: "总分分数段", data: [{ name: "240-269", value: 5, color: "#1890ff" }, { name: "210-239", value: 12, color: "#2fc25b" }, { name: "180-210", value: 18, color: "#facc14" }, { name: "180以下", value: 10, color: "#f04864" }] }] };
let areaRes = { categories: ["A", "B", "C", "D", "E"], series: [{ name: "人数", data: [5, 15, 18, 10, 2], color: "#1890ff" }] };
drawLineChart("line-chart-container", "GradeAnalysisLineChart", lineRes);
drawDonutChart("donut-chart-container", "GradeAnalysisDonutChart", donutRes);
drawAreaChart("area-chart-container", "GradeAnalysisAreaChart", areaRes);
}, 500);
};
onMounted(() => {
getServerData();
});
</script>
<style scoped lang="scss">
.grade-analysis-page {
background-color: #f4f5f7;
min-height: calc(100vh - var(--window-top));
padding-bottom: 20rpx;
}
// --- Selectors ---
.selector-section {
background-color: #ffffff;
padding: 10rpx 30rpx;
}
.class-selector {
display: flex;
align-items: center;
height: 80rpx;
border-bottom: 1rpx solid #f0f0f0;
.picker-item {
display: flex;
align-items: center;
font-size: 30rpx;
color: #333;
font-weight: bold;
width: 100%; // Ensure picker takes full width
uni-icons {
margin-left: 10rpx;
}
}
}
.subject-tabs {
padding: 0; // Remove padding for full width scroll
.scroll-view-h {
white-space: nowrap;
width: 100%;
height: 80rpx;
line-height: 80rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.tab-item {
display: inline-block;
padding: 0 30rpx;
font-size: 28rpx;
color: #666;
position: relative;
text-align: center;
height: 100%;
&.active {
color: #447ade; // Theme color
font-weight: bold;
&::after {
content: "";
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 40%; // Adjust underline width
height: 6rpx;
background-color: #447ade;
border-radius: 3rpx;
}
}
}
}
// --- Section Styling ---
.uni-section {
background-color: #ffffff;
margin-top: 20rpx;
border-radius: 8px;
overflow: hidden;
}
// --- Score Table ---
.score-table {
width: 100%;
font-size: 26rpx;
border: 1rpx solid #e0e0e0;
border-radius: 4px;
overflow: hidden;
.table-header {
display: flex;
background-color: #eaf2ff;
color: #333;
font-weight: bold;
border-bottom: 1rpx solid #e0e0e0;
.th {
flex: 1;
padding: 16rpx 10rpx;
text-align: center;
border-right: 1rpx solid #e0e0e0;
&:last-child {
border-right: none;
}
}
}
.table-body {
.table-row {
display: flex;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.td {
flex: 1;
padding: 16rpx 10rpx;
text-align: center;
color: #666;
border-right: 1rpx solid #f0f0f0;
&:last-child {
border-right: none;
}
}
}
.table-empty {
text-align: center;
color: #999;
padding: 40rpx 0;
}
}
}
// --- Summary Stats ---
.summary-stats-card {
background-color: #f8f9fd;
border-radius: 6px;
padding: 20rpx;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 25rpx 15rpx;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
.label {
font-size: 24rpx;
color: #666;
margin-bottom: 6rpx;
}
.value {
font-size: 32rpx;
font-weight: bold;
color: #333;
&.primary {
color: #447ade;
}
&.success {
color: #67c23a;
}
}
}
// --- Charts ---
.chart-container {
width: 100%;
height: 350rpx; // 进一步减小高度
position: relative;
margin: 8rpx 0;
border-radius: 6rpx; // 减小圆角
overflow: hidden;
background-color: #ffffff;
box-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.05);
}
// Style the canvas element itself
.charts {
width: 100% !important; // 强制100%宽度
height: 100% !important; // 强制100%高度
position: absolute;
top: 0;
left: 0;
max-width: 100%;
max-height: 100%;
}
.chart-placeholder {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
color: #999;
font-size: 24rpx; // 减小占位文字大小
background-color: #ffffff; // Match section background
z-index: 1; // Ensure placeholder is above canvas initially
}
// Hide placeholder when loading is false (adjust logic if needed)
.chart-placeholder {
// ... existing styles
// Use v-if="isLoading" instead of complex CSS selector
}
</style>