2026-05-28 16:40:52 +08:00

2105 lines
68 KiB
Vue

<template>
<div class="workOrder">
<!-- 报告类型 Tab 栏 -->
<div class="reportHeader">
<div class="tabBar">
<div class="tabItem" :class="{ active: activeTab === 'grade' }" @click="switchTab('grade')">年级报告</div>
<div class="tabItem" :class="{ active: activeTab === 'class' }" @click="switchTab('class')">班级报告</div>
<div class="tabItem" :class="{ active: activeTab === 'teacher' }" @click="switchTab('teacher')">教师报告</div>
<div class="tabItem" :class="{ active: activeTab === 'student' }" @click="switchTab('student')">学生报告</div>
</div>
<div class="tabFilter">
<template v-if="activeTab === 'grade'">
<span class="filterLabel">年级:</span>
<el-select v-model="gradeTabId" placeholder="请选择年级" style="width:200px"
@change="onGradeTabChange" clearable>
<el-option v-for="item in data.gradeList" :key="item.id" :label="item.gradeName"
:value="item.id" />
</el-select>
</template>
<template v-else-if="activeTab === 'class'">
<span class="filterLabel">年级-班级:</span>
<el-cascader v-model="classTabId" :options="data.classes" clearable placeholder="请选择班级"
style="width:260px" @change="onClassTabChange" />
</template>
<template v-else-if="activeTab === 'teacher'">
<span class="filterLabel">教师:</span>
<el-select v-model="teacherTabId" placeholder="请选择教师" style="width:200px" filterable
@change="onTeacherTabChange">
<el-option v-for="item in data.teacherList" :key="item.id" :label="item.teacherName"
:value="item.id" />
</el-select>
</template>
<template v-else-if="activeTab === 'student'">
<span class="filterLabel">年级-班级:</span>
<el-cascader v-model="studentClassTabId" :options="data.classes" clearable placeholder="请先选择班级"
style="width:260px" @change="onStudentClassChange" />
<template v-if="studentList.length > 0">
<span class="filterLabel" style="margin-left:16px">学生:</span>
<el-select v-model="studentTabId" placeholder="请选择学生" style="width:200px" filterable
@change="onStudentTabChange">
<el-option v-for="item in studentList" :key="item.studentNo || item.StudentNo"
:label="item.studentName" :value="item" />
</el-select>
</template>
<el-icon v-if="studentListLoading" class="is-loading" style="margin-left:8px"><loading /></el-icon>
</template>
</div>
</div>
<!-- 统计卡片 -->
<template v-if="activeTab === 'grade'">
<div class="list" v-if="loading.stats">
<div class="listOne skeleton-card" v-for="i in 6" :key="i">
<el-skeleton animated style="width:100%;display:flex;align-items:center;padding:0 16px 0 20px">
<template #template>
<el-skeleton-item variant="circle" style="width:60px;height:60px;flex-shrink:0" />
<div style="margin-left:16px;flex:1">
<el-skeleton-item variant="h3" style="width:55%;margin-bottom:10px" />
<el-skeleton-item variant="text" style="width:35%" />
</div>
</template>
</el-skeleton>
</div>
</div>
<div class="list" v-else>
<div class="listOne" v-for="(item, index) in data.listData" :key="index">
<div class="left">
<img :src="item.url" alt="">
</div>
<div class="right">
<p>{{ item.num }}</p>
<p>{{ item.name }}</p>
</div>
</div>
</div>
<!-- 年级均分 & 等级占比 -->
<div class="cont1">
<div class="cont1Left">
<div class="title">
<p>年级体测平均成绩</p>
</div>
<div class="chart-skeleton" v-if="loading.stats">
<el-skeleton animated>
<template #template>
<div class="skeleton-chart-wrap">
<div class="skeleton-bars">
<div class="skeleton-bar-group" v-for="i in 7" :key="i">
<el-skeleton-item variant="rect"
:style="`width:24px;height:${40 + Math.random() * 120}px`" />
<el-skeleton-item variant="text" style="width:32px;margin-top:8px" />
</div>
</div>
</div>
</template>
</el-skeleton>
</div>
<div id="barChart" v-show="!loading.stats"></div>
</div>
<div class="cont1Right">
<div class="title">
<div class="head">
<div class="left">
<p>各体测项目等级占比</p>
</div>
<div class="right">
<span>项目:</span>
<el-select @change="changeProject" v-model="data.project" placeholder="请选择"
style="width: 200px">
<el-option v-for="item in data.projectData" :key="item.categoryId"
:label="item.categoryName" :value="item.categoryId" />
</el-select>
</div>
</div>
</div>
<div class="chart-skeleton" v-if="loading.level">
<el-skeleton animated>
<template #template>
<div class="skeleton-chart-wrap">
<div class="skeleton-pie-wrap">
<el-skeleton-item variant="circle" style="width:130px;height:130px" />
<div class="skeleton-pie-legend">
<div class="skeleton-legend-item" v-for="i in 4" :key="i">
<el-skeleton-item variant="circle" style="width:10px;height:10px;flex-shrink:0" />
<el-skeleton-item variant="text" style="width:80px" />
</div>
</div>
</div>
</div>
</template>
</el-skeleton>
</div>
<div id="levelbarChart" v-show="!loading.level"></div>
</div>
</div>
<!-- 班级达标率 -->
<div class="cont3">
<div class="title">
<p>班级体测达标率对比</p>
</div>
<div class="chart-skeleton" v-if="loading.stats">
<el-skeleton animated>
<template #template>
<div class="skeleton-chart-wrap">
<div class="skeleton-bars">
<div class="skeleton-bar-group" v-for="i in 8" :key="i">
<div style="display:flex;gap:4px;align-items:flex-end">
<el-skeleton-item variant="rect"
:style="`width:14px;height:${50 + Math.random() * 100}px`" />
<el-skeleton-item variant="rect"
:style="`width:14px;height:${50 + Math.random() * 100}px`" />
<el-skeleton-item variant="rect"
:style="`width:14px;height:${50 + Math.random() * 100}px`" />
</div>
<el-skeleton-item variant="text" style="width:42px;margin-top:8px" />
</div>
</div>
</div>
</template>
</el-skeleton>
</div>
<div class="cont3Chart" id="classBarChart" v-show="!loading.stats"></div>
</div>
<!-- 成绩趋势 -->
<div class="cont4">
<div class="title">
<div class="left">
<p>成绩趋势</p>
</div>
<div class="right">
<div class="one">
<p>项目:</p>
<el-select @change="changeTrendProject" v-model="data.trendProject" placeholder="请选择"
style="width: 240px">
<el-option v-for="item in data.projectData" :key="item.categoryId"
:label="item.categoryName" :value="item.categoryId" />
</el-select>
</div>
<div class="one">
<p>日期:</p>
<el-select @change="changeTrendDate" v-model="data.date" placeholder="请选择" style="width: 240px">
<el-option v-for="item in data.dateData" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
</div>
</div>
</div>
<div class="chart-skeleton" v-if="loading.trend">
<el-skeleton animated>
<template #template>
<div class="skeleton-chart-wrap skeleton-line-wrap">
<el-skeleton-item variant="rect" style="width:100%;height:3px;margin-bottom:24px" />
<div class="skeleton-line-dots">
<div class="skeleton-line-dot" v-for="i in 12" :key="i">
<el-skeleton-item variant="circle" style="width:8px;height:8px" />
<el-skeleton-item variant="text" style="width:28px;margin-top:8px" />
</div>
</div>
</div>
</template>
</el-skeleton>
</div>
<div class="cont4Chart" id="trendChart" v-show="!loading.trend"></div>
</div>
<!-- 详细列表 -->
<div class="tableCont">
<p>详细列表</p>
<div v-if="loading.stats" class="skeleton-table">
<el-skeleton animated>
<template #template>
<div class="skeleton-table-header">
<el-skeleton-item variant="rect" v-for="i in 6" :key="i" style="height:16px;flex:1;margin:0 8px" />
</div>
<div class="skeleton-table-row" v-for="j in 8" :key="j">
<el-skeleton-item variant="text" v-for="i in 6" :key="i" style="flex:1;margin:0 8px" />
</div>
</template>
</el-skeleton>
</div>
<commonTable :tableNeed="data.tableNeed" @toView="toView" v-else-if="data.tableNeed.refresh"></commonTable>
</div>
</template><!-- end grade tab -->
<!-- 班级报告 Tab -->
<template v-else-if="activeTab === 'class'">
<div v-if="!classTabReady" class="emptyHint">
<el-icon><office-building /></el-icon>
<p>请在上方选择年级-班级,查看对应班级体测报告</p>
</div>
<ClassView1 v-else :key="classKey" class="embeddedReport" />
</template>
<!-- 教师报告 Tab -->
<template v-else-if="activeTab === 'teacher'">
<div v-if="!teacherTabReady" class="emptyHint">
<el-icon><user /></el-icon>
<p>请在上方选择教师,查看对应教师体测报告</p>
</div>
<TeacherView v-else :key="teacherKey" class="embeddedReport" />
</template>
<!-- 学生报告 Tab -->
<template v-else-if="activeTab === 'student'">
<div v-if="!studentTabReady" class="emptyHint">
<el-icon><avatar /></el-icon>
<p>请在上方依次选择班级和学生,查看对应学生体测报告</p>
</div>
<StudentDataView v-else :key="studentKey" class="embeddedReport" />
</template>
</div>
<commonDialog :list="data.dialogData" @cancle="confirm" @confirm="confirm">
<template #cont>
<div class="dialogManage">
<el-scrollbar height="400px">
<div class="cont">
<div class="one" @click="toViewStudentDetail(item)"
v-for="(item, index) in data.filterStudentList" :key="index">
<div class="left">
<img style="width: 100px;height: 100px;" :src="item.photo" alt="">
</div>
<div class="right">
<p>{{ item.studentName }}</p>
</div>
</div>
</div>
</el-scrollbar>
</div>
</template>
</commonDialog>
</template>
<script lang="ts" setup>
import { ref, onMounted, onBeforeMount, onBeforeUnmount, markRaw, nextTick } from 'vue';
import { ArrowDown, OfficeBuilding, User, Avatar, Loading } from '@element-plus/icons-vue'
import * as echarts from 'echarts';
import { useRoute } from 'vue-router';
import * as _api from '@/api/grade.js';
import commonTable from '@/views/motionData/trainingData/components/commonTbale/index.vue';
import eventBus from '@/eventBus.js';
import { ElMessage } from 'element-plus';
import commonDialog from '@/components/commonDialog/index.vue';
import ClassView1 from '@/views/classView1/index.vue';
import TeacherView from '@/views/teacherView/index.vue';
import StudentDataView from '@/views/StudentDataStatistics/index.vue';
const route = useRoute();
// ── Tab 切换状态 ──
const activeTab = ref('grade')
const gradeTabId = ref(null)
const classTabId = ref(null)
const classTabReady = ref(false)
const classKey = ref(0)
const teacherTabId = ref(null)
const teacherTabReady = ref(false)
const teacherKey = ref(0)
const studentClassTabId = ref(null)
const studentTabId = ref(null)
const studentTabReady = ref(false)
const studentKey = ref(0)
const studentList = ref([])
const studentListLoading = ref(false)
async function switchTab(tab: string) {
activeTab.value = tab
if (tab === 'grade') {
await nextTick()
if (!loading.value.stats) {
averageBar(data.value.averageBarData)
classBar(data.value.classBarData)
}
if (!loading.value.level) {
levelChart(data.value.levelChartData)
}
if (!loading.value.trend) {
trendEcharts(data.value.chartsData3)
}
}
}
async function onGradeTabChange(val: any) {
if (!val) return
data.value.newGradeId = val
data.value.gradeId = val
loading.value.stats = true
loading.value.level = true
loading.value.trend = true
data.value.tableNeed.refresh = false
await initCategory(val)
if (data.value.projectData.length > 0) {
data.value.project = data.value.projectData[0].categoryId
data.value.trendProject = data.value.projectData[0].categoryId
}
initGradeWholeDataStats(val)
initCategoryRankRatio(data.value.project, val)
initResultTrends(data.value.date, data.value.trendProject, val)
}
function onClassTabChange(val: any) {
if (!val || val.length < 2) return
const classId = val[1]
const gradeId = val[0]
localStorage.setItem('classId', JSON.stringify({ id: classId, gradeId: gradeId }))
classTabReady.value = true
classKey.value++
}
function onTeacherTabChange(val: any) {
if (!val) return
localStorage.setItem('teacherView', JSON.stringify({ id: val }))
teacherTabReady.value = true
teacherKey.value++
}
async function onStudentClassChange(val: any) {
studentTabReady.value = false
studentTabId.value = null
studentList.value = []
if (!val || val.length < 2) return
const classId = val[1]
studentListLoading.value = true
try {
const res = await _api.StudentGetClassAll(classId)
if (res.code == 200) {
studentList.value = res.data || []
}
} catch (e) {
console.error(e)
} finally {
studentListLoading.value = false
}
}
function onStudentTabChange(val: any) {
if (!val) return
const gradeId = studentClassTabId.value[0]
const classId = studentClassTabId.value[1]
const studentObj = {
...val,
studentNo: val.studentNo || val.StudentNo,
gradeId: gradeId,
classId: classId,
}
localStorage.setItem('studentNo', JSON.stringify(studentObj))
studentTabReady.value = true
studentKey.value++
}
const loading = ref({
stats: true, // 统计卡片 + 年级平均成绩图 + 班级达标率图 + 表格
level: true, // 各体测项目等级占比图
trend: true, // 成绩趋势图
})
const data = ref({
dialogData: {
dialogVisible: false,
width: 800,
title: '学生列表',
},
classes: [
],
ClassId: '',
gradeList: [],
gradeId: null,
teacherList: [],
teacherId: '',
studentId: '',
selectData: 1,
selectOptions: [{
label: '全部',
value: 1
}, {
label: '课堂模式',
value: 2
}, {
label: '自由模式',
value: 3
},],
listData: [{
url: require('@/assets/imgs/icon.png'),
num: 12,
name: '班级数量'
}, {
url: require('@/assets/imgs/icon.png'),
num: 12,
name: '学生数量'
}, {
url: require('@/assets/imgs/icon.png'),
num: '20%',
name: '优秀'
}, {
url: require('@/assets/imgs/icon.png'),
num: '20%',
name: '良好'
}, {
url: require('@/assets/imgs/icon.png'),
num: '20%',
name: '及格'
}, {
url: require('@/assets/imgs/icon.png'),
num: '20%',
name: '不及格'
}],
averageBarData: {
xData: [],
yData: [],
color: ['#4188FE']
},
project: 1,
projectData: [],
levelChartData: {
color: ['#4188FE', '#00DAAA', '#FFD11A', '#FF6C1A', '#7F70C9'],
seriesData: []
},
classBarData: {
xData: ['1班', '2班', '3班', '4班', '5班', '6班'],
yData: [],
yData1: [],
yData2: [],
color: ['#4188FE', '#00DAAA', '#FAC553'],
},
trendProject: 1,
date: 4,
dateData: [{
value: 1,
label: '近一周'
}, {
value: 2,
label: '近两周'
}, {
value: 3,
label: '近一月'
}, {
value: 4,
label: '近一年'
},],
chartsData3: {
ChartCont: {
xData: [],
yName: '分',
axisLabelColor: '#868A95',
legendColor: '#3F404A',
seriesName: "",
spitAreaColor: ['#fff', '#F6F7F9'],
labelColor: '#666566',
lineStyleColor: '#DDDEE2',
seriesData: [],
splitNumber: null,
color: '#0979FA',
areaStyle: {
color: {
type: "linear",
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: "#0979FA" // 0% 处的颜色
},
{
offset: 1,
color: "rgba(67,173,255,0.08)" // 100% 处的颜色
}
],
global: false // 缺省为 false
}
}
}
},
tableNeed: {
refresh: false,
showoperate: true,
showEdit: false,
showPagtion: 2,
pageIndex: 1,
pageSize: 10,
total: 0,
height: '66.5vh',
tdHeight: '56px',
tableLabel: [
{
label: "序号",
prop: "ind",
},
{
label: "年级-班级",
prop: "gradeAndClassName",
},
{
label: "人数",
prop: "count",
},
{
label: "教师姓名",
prop: "teacherName",
},
{
label: "授课次数",
prop: "accumulatedTeachingTimes",
},
{
label: "达标率",
prop: "passRate",
},
],
tableData: [],
},
// gradeId: null
newGradeId: null,
allStudentList: [],
filterStudentList: []
})
function confirm() {
data.value.dialogData.dialogVisible = false
}
onMounted(async () => {
data.value.newGradeId = route.params.id || JSON.parse(localStorage.getItem('gradeId'))?.id || 0;
data.value.gradeId = data.value.newGradeId
gradeTabId.value = data.value.newGradeId || null
// 先加载项目分类,确保 project 初始值是有效的 categoryId
await initCategory(data.value.newGradeId)
if (data.value.projectData.length > 0) {
data.value.project = data.value.projectData[0].categoryId
data.value.trendProject = data.value.projectData[0].categoryId
}
await nextTick()
initGradeWholeDataStats(data.value.newGradeId)
initCategoryRankRatio(data.value.project, data.value.newGradeId)
initResultTrends(data.value.date, data.value.trendProject, data.value.newGradeId)
GetGradeNames()
getAllGrade()
GetTeacherNames()
})
onBeforeUnmount(() => {
// localStorage.removeItem('gradeId')
})
function changeProject(e) {
console.log(e, 'changeProject')
data.value.project = e
data.value.levelChartData.seriesData = []
initCategoryRankRatio(data.value.project, data.value.gradeId)
}
function changeTrendProject(e) {
data.value.trendProject = e
data.value.chartsData3.ChartCont.xData = []
data.value.chartsData3.ChartCont.seriesData = []
initResultTrends(data.value.date, data.value.trendProject, data.value.gradeId)
}
function changeTrendDate(e) {
data.value.date = e
data.value.chartsData3.ChartCont.xData = []
data.value.chartsData3.ChartCont.seriesData = []
initResultTrends(data.value.date, data.value.trendProject, data.value.gradeId)
}
// 获取年级对应项目名称
async function initCategory(val) {
try {
const res = await _api.CategoryList(val != 0 ? val : 1);
if (res.code == 200) {
data.value.projectData = res.data
}
} catch (error) {
console.error('Error occurred while calling API:', error);
}
}
// 各体测项目等级占比
async function initCategoryRankRatio(val, val1) {
try {
const res = await _api.CategoryRankRatio(val, val1);
if (res.code == 200) {
data.value.levelChartData.seriesData = []
for (let i in res.data) {
data.value.levelChartData.seriesData.push({ value: (res.data[i] * 100).toFixed(0), name: i })
}
loading.value.level = false
await nextTick()
levelChart(data.value.levelChartData)
}
} catch (error) {
loading.value.level = false
console.error('Error occurred while calling API:', error);
}
}
// 成绩趋势
async function initResultTrends(val, val1, val2) {
try {
const res = await _api.ResultTrends(val, val1, val2);
if (res.code == 200) {
data.value.chartsData3.ChartCont.xData = res.data.axisX
data.value.chartsData3.ChartCont.seriesData = res.data.axisY
loading.value.trend = false
await nextTick()
trendEcharts(data.value.chartsData3)
}
} catch (error) {
loading.value.trend = false
console.error('Error occurred while calling API:', error);
}
}
// 年级数据统计
async function initGradeWholeDataStats(val) {
try {
const res = await _api.GradeWholeDataStats(val);
if (res.code == 200) {
data.value.listData[0].num = res.data.classCount
data.value.listData[1].num = res.data.studentCount
data.value.listData[2].num = res.data.excellentRate + '%'
data.value.listData[3].num = res.data.fineRate + '%'
data.value.listData[4].num = res.data.passRate + '%'
data.value.listData[5].num = res.data.failRate + '%'
data.value.averageBarData.xData = []
data.value.averageBarData.yData = []
for (let i in res.data.testResultAvg) {
data.value.averageBarData.xData.push(i)
data.value.averageBarData.yData.push(res.data.testResultAvg[i].toFixed(0))
}
data.value.classBarData.xData = res.data.testResultPassRate.axisX
data.value.classBarData.yData = []
data.value.classBarData.yData1 = []
data.value.classBarData.yData2 = []
res.data.testResultPassRate.axisY.map((i) => {
data.value.classBarData.yData.push((i.maleValue * 100).toFixed(0))
data.value.classBarData.yData1.push((i.femaleValue * 100).toFixed(0))
data.value.classBarData.yData2.push((i.overallValue * 100).toFixed(0))
})
data.value.tableNeed.tableData = res.data.gradeDetailsList
data.value.tableNeed.tableData.map((i, ind) => {
i.ind = ind + 1
i.passRate = (i.passRate * 100).toFixed(0) + '%'
})
data.value.tableNeed.refresh = true
loading.value.stats = false
await nextTick()
averageBar(data.value.averageBarData)
classBar(data.value.classBarData)
}
} catch (error) {
loading.value.stats = false
console.error('Error occurred while calling API:', error);
}
}
// 查看详情
function toView(val) {
console.log(val, 'valllllltoView==============>')
const gradeName = val.gradeAndClassName.split("-")[0];
console.log(gradeName,'gradeName')
console.log(data.value.classes,'data.value.classes')
const grade = data.value.classes.find(item => item.label.startsWith(gradeName));
console.log(grade,'gradeeeeee')
let params = {
id: val.classId,
gradeId: grade.value
}
// let params=[val.classId,data.value.newGradeId!=0?data.value.newGradeId:1]
eventBus.emit('toDetail', params)
}
// 年级体测平均成绩
function averageBar(val) {
echarts.dispose(document.getElementById('barChart'));
let myChart = markRaw(echarts.init(document.getElementById('barChart')));
myChart.setOption(
{
color: val.color,
legend: {
icon: "rect",
x: "right",
padding: [5, 40, 0, 0],
itemWidth: 12,
itemHeight: 6,
itemGap: 41,
textStyle: {
color: "#000",
fontSize: "12px"
}
},
grid: {
left: "2%",
right: "2%",
// top:'10%',
bottom: "5%",
containLabel: true
},
xAxis: {
type: 'category',
data: val.xData,
axisTick: {
show: false
},
axisLabel: {
//x轴文字的配置
show: true,
color: '#868A95',
fontSize: 12
},
axisLine: {
show: true,
lineStyle: {
color: '#DDDEE2'
}
},
},
yAxis: {
type: 'value',
name: '分',
nameTextStyle: {
color: "#868A95",
fontSize: 12,
// padding: val.Ypadding
},
// interval: 20,
axisLabel: {
color: '#868A95',
fontStyle: "normal",
fontWeight: "400",
fontSize: "12"
},
axisLine: {
show: true,
lineStyle: {
color: '#DDDEE2'
}
},
splitLine: {
//分割线配置
show: false,
},
splitArea: {
show: true,
areaStyle: {
// color: val.splitAreaColor,
opacity: 1
}
}
},
series: [
{
name: '平均成绩',
data: val.yData,
type: 'bar',
barWidth: 20,
itemStyle: {
normal: {
label: {
formatter: "{c}",
show: true,
position: "top",
textStyle: {
fontWeight: "400",
fontSize: "14",
color: "#000"
}
},
}
}
},
]
}
)
window.onresize = function () { // 自适应大小
myChart.resize();
};
}
// 各体测项目等级占比
function levelChart(val) {
echarts.dispose(document.getElementById('levelbarChart'));
let myChart = markRaw(echarts.init(document.getElementById('levelbarChart')));
// 绘制图表
myChart.setOption({
color: val.color,
series: [
{
type: 'pie',
radius: ['50%', '70%'],
center: ['50%', '45%'],
startAngle: 0,
hoverAnimation: true,
avoidLabelOverlap: true,
animationDurationUpdate: 2000, // 更新动画时长为 1s
animationEasingUpdate: 'cubicInOut', // 更新动画缓动效果为 'quinticInOut'
silent: true,
label: {
color: '#000',
show: true,
position: 'outside',
formatter: function (value) {
console.log(value)
let str = '{a|' + value.name + ':}' + '{a|' + value.value + '%}' + '\n\n'
return str
},
rich: {
a: {
padding: [10, 0, 0, 0],
},
},
emphasis: {
show: true,
textStyle: {
fontSize: 12,
fontWeight: 'bold'
}
}
},
data: val.seriesData,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
});
window.onresize = function () { // 自适应大小
myChart.resize();
};
}
// 班级体测达标率对比
function classBar(val) {
echarts.dispose(document.getElementById('classBarChart'));
let myChart = markRaw(echarts.init(document.getElementById('classBarChart')));
myChart.setOption(
{
color: val.color,
tooltip: {},
legend: {
icon: "rect",
x: "right",
padding: [5, 40, 0, 0],
itemWidth: 12,
itemHeight: 6,
itemGap: 41,
textStyle: {
color: "#000",
fontSize: "12px"
}
},
grid: {
left: "2%",
right: "2%",
// top:'10%',
bottom: "5%",
containLabel: true
},
xAxis: {
type: 'category',
data: val.xData,
axisTick: {
show: false
},
axisLabel: {
//x轴文字的配置
show: true,
color: '#868A95',
fontSize: 12
},
axisLine: {
show: true,
lineStyle: {
color: '#DDDEE2'
}
},
},
yAxis: {
type: 'value',
name: '%',
nameTextStyle: {
color: "#868A95",
fontSize: 12,
// padding: val.Ypadding
},
// interval: 20,
axisLabel: {
color: '#868A95',
fontStyle: "normal",
fontWeight: "400",
fontSize: "12"
},
axisLine: {
show: true,
lineStyle: {
color: '#DDDEE2'
}
},
splitLine: {
//分割线配置
show: false,
},
splitArea: {
show: true,
areaStyle: {
// color: val.splitAreaColor,
opacity: 1
}
}
},
series: [
{
name: '男生',
data: val.yData,
type: 'bar',
barWidth: null, // 宽度自适应
barMaxWidth: 20, // 限制最大宽度为 20
// itemStyle: {
// normal: {
// label: {
// formatter: "{c}%",
// show: true,
// position: "top",
// textStyle: {
// fontWeight: "400",
// fontSize: "10",
// color: "#666566"
// }
// },
// // color: function (params) {
// // let colorList = val.color;
// // return colorList[params.dataIndex];
// // }
// }
// }
},
{
data: val.yData1,
name: '女生',
type: 'bar',
barWidth: null, // 宽度自适应
barMaxWidth: 20, // 限制最大宽度为 20
// itemStyle: {
// normal: {
// label: {
// formatter: "{c}%",
// show: true,
// position: "top",
// textStyle: {
// fontWeight: "400",
// fontSize: "10",
// color: "#666566"
// }
// },
// // color: function (params) {
// // let colorList = val.color;
// // return colorList[params.dataIndex];
// // }
// }
// }
},
{
data: val.yData2,
name: '总体',
type: 'bar',
barWidth: null, // 宽度自适应
barMaxWidth: 20, // 限制最大宽度为 20
// itemStyle: {
// normal: {
// label: {
// formatter: "{c}%",
// show: true,
// position: "insideTopLeft",
// textStyle: {
// fontWeight: "400",
// fontSize: "10",
// color: "#666566"
// }
// },
// // color: function (params) {
// // let colorList = val.color;
// // return colorList[params.dataIndex];
// // }
// }
// }
}
]
}
)
window.onresize = function () { // 自适应大小
myChart.resize();
};
}
// 成绩趋势
function trendEcharts(val) {
echarts.dispose(document.getElementById('trendChart'));
let myChart = markRaw(echarts.init(document.getElementById('trendChart')));
myChart.setOption(
{
grid: {
left: "2%",
right: "2%",
bottom: "10%",
containLabel: true
},
color: [val.ChartCont.color],
tooltip: {
trigger: "axis",
formatter: function (params) {
// 自定义tooltip内容
return `${params[0].name}<br/>平均成绩:${params[0].data}`;
}
},
legend: {
show: val.showLegend ? false : true,
icon: "rect",
x: "right",
padding: [val.padTop ? val.padTop : 35, val.padRight ? val.padRight : 20, 0, 0],
itemWidth: val.ChartCont.legendWidth ? val.ChartCont.legendWidth : 19,
itemHeight: val.ChartCont.legendHeight ? val.ChartCont.legendHeight : 9,
itemGap: 41,
textStyle: {
color: val.ChartCont.legendColor ? val.ChartCont.legendColor : '#F2F2F2',
fontSize: "12px"
}
},
toolbox: {
show: false
},
xAxis: {
type: val.ChartCont.xType ? val.ChartCont.xType : "category",
// position: "bottom",
boundaryGap: false,
min: val.ChartCont.min ? val.ChartCont.min : null,
max: val.ChartCont.max ? val.ChartCont.max : null,
splitNumber: val.ChartCont.splitNumber ? val.ChartCont.splitNumber : null,
data: val.ChartCont.xData,
axisTick: {
show: false
},
axisLabel: {
//x轴文字的配置
show: true,
// textStyle: {
color: val.ChartCont.axisLabelColor ? val.ChartCont.axisLabelColor : '#fff',
fontSize: 12
// }
},
axisLine: {
show: true,
lineStyle: {
color: val.ChartCont.lineStyleColor ? val.ChartCont.lineStyleColor : "#1C3B36"
}
}
},
yAxis: {
type: "value",
name: val.ChartCont.yName,
nameTextStyle: {
color: val.ChartCont.axisLabelColor ? val.ChartCont.axisLabelColor : '#fff',
fontSize: 12,
padding: [0, val.chartsPadding ? val.chartsPadding : 20, 0, 0]
},
axisLabel: {
color: val.ChartCont.axisLabelColor ? val.ChartCont.axisLabelColor : '#fff',
fontStyle: "normal",
fontWeight: "400",
fontSize: "12",
align: val.ChartCont.yAxisAlign ? val.ChartCont.yAxisAlign : null,
margin: val.ChartCont.yAxisMargin ? val.ChartCont.yAxisMargin : 10, // 可以调整偏移量
},
axisLine: {
show: true,
lineStyle: {
color: val.ChartCont.lineStyleColor ? val.ChartCont.lineStyleColor : "#1C3B36"
}
},
splitLine: {
//分割线配置
show: false,
lineStyle: {
color: "#012624"
}
},
splitArea: {
show: true,
areaStyle: {
color: val.ChartCont.spitAreaColor ? val.ChartCont.spitAreaColor : ["rgba(200,200,200,0.3)", "#8AFFDD"],
opacity: val.ChartCont.spitAreaColor ? 1 : 0.04
}
}
},
series: [
{
name: val.ChartCont.seriesName1,
// stack: 'Total',
symbol: 'circle', //将小圆点改成实心 不写symbol默认空心
symbolSize: 5,
type: "line",
// smooth: true,
itemStyle: {
normal: {
label: {
show: true,
fontSize: 14,
fontFamily: 'DIN-Black',
fontWeight: 900,
color: val.ChartCont.labelColor ? val.ChartCont.labelColor : '#fff',
position: 'top',
}
}
},
// symbol: 'circle',
data: val.ChartCont.seriesData,
// lineStyle: {
// width: val.ChartCont.seriesData.length === 1 ? 0 : 2, // 如果只有一条数据,隐藏线条
// },
areaStyle: val.ChartCont.areaStyle,
},
],
visualMap: val.ChartCont.visualMap ? val.ChartCont.visualMap : null
}
)
}
function transformData(data) {
return data.map(grade => ({
value: grade.id,
label: grade.gradeName,
children: grade.class.map(cls => ({
value: cls.id,
label: cls.className
}))
}));
}
//年级接口
async function GetGradeNames() {
try {
const res = await _api.GetGradeName();
if (res.code == 200) {
if (res.data) {
data.value.classes = transformData(res.data)
}
} else {
ElMessage.error(res.message)
}
} catch (error) {
console.error('Error occurred while calling API:', error);
}
}
//班级接口
async function GetClassNames(val, num) {
try {
const res = await _api.GetClassNames(val);
if (res.code == 200) {
if (res.data) {
data.value.classes = res.data
}
} else {
ElMessage.error(res.message)
}
} catch (error) {
console.error('Error occurred while calling API:', error);
}
}
//获取所有年级名称
async function getAllGrade() {
try {
const res = await _api.GetGradeName();
if (res.code == 200) {
data.value.gradeList = res.data;
console.log(data.value.gradeList, 'data.value.gradeList')
}
} catch (error) {
console.error('Error occurred while calling API:', error);
}
}
//获取所有老师名称
async function GetTeacherNames() {
try {
const res = await _api.GetTeacherNames();
if (res.code == 200) {
data.value.teacherList = res.data;
}
} catch (error) {
console.error('Error occurred while calling API:', error);
}
}
//获取所有学生
async function initStudent() {
try {
const res = await _api.StudentGetClassPageList('', '', '', '', '', 1, 1000000);
if (res.code == 200) {
console.log(res, 'initStudent')
data.value.allStudentList = res.data.datas
}
} catch (error) {
console.error('Error occurred while calling API:', error);
}
}
function toViewGrade(val) {
console.log(val, 'valllll')
// return
let params = {
id: val
}
data.value.newGradeId = val
eventBus.emit('toViewGrade', params);
}
function toStudentView(val) {
console.log(val, 'vallllltoStudentView')
let params = {
id: val[1],
gradeId: val[0]
}
console.log(params, 'paramstoStudentView')
eventBus.emit('toDetail', params);
}
function toView1(val) {
console.log(val, 'valllll')
let params = {
id: val
}
eventBus.emit('toView1', params);
}
async function toViewStudent(val) {
console.log(val, 'valllll')
await initStudent()
data.value.filterStudentList = data.value.allStudentList.filter(item => item.classId == val[1])
console.log(data.value.filterStudentList, 'kkkkkkk')
data.value.dialogData.dialogVisible = true
}
function toViewStudentDetail(val) {
console.log(val, 'toViewStudentDetail')
let k = data.value.gradeList.filter(item => item.gradeName == val.gradeName)
val.gradeId = k[0].id
eventBus.emit('toViewStudent', val);
}
</script>
<style lang="less" scoped>
.dialogManage {
.cont {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
.one {
width: 150px;
height: 150px;
img {
display: block;
margin: 0 auto;
}
p {
font-family: PingFangSC;
font-weight: 400;
font-size: 14px;
color: rgba(0, 0, 0, 0.63);
text-align: center;
}
}
}
}
.workOrder {
width: calc(100% - 40px);
height: auto;
margin: 20px;
.ipt {
width: 100%;
min-height: 72px;
height: auto;
padding: 16px 0;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12px;
background: #FFFFFF;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
.one {
display: flex;
align-items: center;
margin-right: 0;
margin-left: 20px;
p {
font-family: PingFangSC;
font-weight: 400;
font-size: 14px;
color: rgba(0, 0, 0, 0.85);
margin-right: 10px;
}
}
}
.head {
width: 100%;
height: 40px;
display: flex;
align-items: center;
margin-left: 5px;
.one {
width: 330px;
height: 40px;
display: flex;
align-items: center;
background: #FFFFFF;
border-radius: 8px;
// margin-left: 20px;
margin-right: 20px;
p {
width: 84px;
height: 40px;
border: 1px solid rgba(0, 0, 0, 0.15);
text-align: center;
line-height: 40px;
font-family: PingFangSC;
font-weight: 400;
font-size: 14px;
color: rgba(0, 0, 0, 0.85);
border-right: 0px;
border-radius: 4px 0px 0px 4px;
}
.m-2 {
width: 246px;
// height: 60px;
:deep(.el-input__wrapper) {
height: 40px;
border-radius: 0 4px 4px 0;
}
}
}
}
.list {
width: 100%;
height: auto;
min-height: 100px;
display: flex;
align-items: stretch;
flex-wrap: wrap;
gap: 16px;
margin-top: 16px;
.listOne {
flex: 1;
min-width: 180px;
height: 100px;
background: #FFFFFF;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
display: flex;
align-items: center;
.left {
width: 60px;
height: 60px;
background: linear-gradient(135deg, rgba(65, 136, 254, 0.2), rgba(65, 136, 254, 0.08));
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
margin: 20px 20px;
flex-shrink: 0;
img {
width: 36px;
height: 36px;
}
}
.right {
p:nth-of-type(1) {
height: auto;
font-family: DINAlternate;
font-weight: 900;
font-size: 22px;
color: #4188FE;
}
p:nth-of-type(2) {
font-family: PingFangSC;
font-weight: 400;
font-size: 14px;
color: rgba(0, 0, 0, 0.85);
}
}
}
}
.cont1 {
width: 100%;
height: auto;
min-height: 330px;
display: flex;
align-items: stretch;
gap: 16px;
margin-top: 16px;
.cont1Left,
.cont1Right {
flex: 1;
min-width: 0;
height: auto;
background: #FFFFFF;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
padding-bottom: 16px;
.title {
width: 100%;
height: auto;
min-height: 48px;
display: flex;
align-items: center;
padding: 0 20px;
border-bottom: 1px solid #F5F5F5;
margin-bottom: 4px;
p {
font-family: PingFangSC;
font-weight: 600;
font-size: 16px;
color: rgba(0, 0, 0, 0.85);
}
}
#barChart {
width: 100%;
height: 260px;
}
#levelbarChart {
width: 100%;
height: 260px;
}
// .contList {
// width: 546px;
// height: 100%;
// display: flex;
// flex-wrap: wrap;
// .descript {
// width: 235px;
// height: 75px;
// background: rgba(0, 86, 221, 0.25);
// border-radius: 8px;
// margin-top: 20px;
// display: flex;
// flex-direction: column;
// align-items: center;
// justify-content: center;
// margin-right: 25px;
// p:nth-of-type(1) {
// height: 0px;
// font-family: DINAlternate;
// font-weight: 900;
// font-size: 24px;
// color: #4188FE;
// margin-top: 8px;
// }
// p:nth-of-type(2) {
// font-family: PingFangSC;
// font-weight: 400;
// font-size: 14px;
// color: #666566;
// }
// }
// .descript:nth-of-type(3),
// .descript:nth-of-type(4) {
// margin-top: -5px;
// }
// }
}
.cont1Right {
.title {
.head {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
.left {
p {
font-family: PingFangSC;
font-weight: 600;
font-size: 16px;
color: rgba(0, 0, 0, 0.85);
}
}
.right {
display: flex;
align-items: center;
span {
display: inline-block;
font-family: PingFangSC;
font-weight: 400;
font-size: 14px;
color: rgba(0, 0, 0, 0.65);
margin-right: 8px;
white-space: nowrap;
}
}
}
}
}
}
.cont2 {
width: 100%;
height: 350px;
display: flex;
margin-top: 20px;
margin-left: 5px;
.cont2Left {
width: 820px;
height: 350px;
background: #FFFFFF;
border-radius: 8px;
margin-right: 20px;
.title {
width: 100%;
height: 60px;
display: flex;
align-items: center;
p {
width: calc(100% - 240px);
font-family: PingFangSC;
font-weight: 600;
font-size: 16px;
color: rgba(0, 0, 0, 0.85);
margin-left: 20px;
}
.tabList {
width: 240px;
height: 30px;
display: flex;
align-items: center;
p {
width: 83px;
height: 24px;
background: #FAFAFA;
// background: #4188FE;
border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.15);
text-align: center;
line-height: 24px;
font-size: 12px;
font-family: PingFangSC;
font-weight: 400;
color: rgba(120, 118, 118, 0.85);
margin-left: 20px;
margin-top: 20px;
cursor: pointer;
}
.active {
background: #4188FE;
color: rgba(255, 255, 255, 0.85);
}
}
}
.centerOneCont {
width: 100%;
height: 25px;
display: flex;
align-items: center;
margin-left: 20px;
margin-top: 30px;
.left {
.average {
width: 300px;
height: 300px;
margin-top: 180px;
}
}
.right {
width: 410px;
margin-top: 180px;
.one {
width: 100%;
height: 50px;
margin-bottom: 20px;
margin-left: 20px;
position: relative;
.head {
width: 100%;
height: 30px;
display: flex;
align-items: center;
// justify-content: space-between;
p,
h5 {
font-family: PingFangSC;
font-weight: 500;
font-size: 14px;
color: #2C2C2C;
}
h5 {
margin-left: 10px;
}
p::before {
content: ' ';
width: 8px;
height: 8px;
border-radius: 50%;
position: absolute;
top: 21%;
left: 0px;
}
p {
margin-left: 13px;
}
}
.bodyCont {
width: calc(100% - 60px);
height: 25px;
display: flex;
margin-top: -10px;
p {
font-family: PingFangSC;
font-weight: 500;
font-size: 12px;
color: #868A95;
margin-right: 27px;
}
}
.chart {
width: 410px;
height: 100%;
}
}
.one:nth-of-type(1) {
.head {
p::before {
background-color: #00DAAA;
}
}
}
.one:nth-of-type(2) {
.head {
p::before {
background-color: #7BADFF;
}
}
}
.one:nth-of-type(3) {
.head {
p::before {
background-color: #FFD271;
}
}
}
.one:nth-of-type(4) {
.head {
p::before {
background-color: #FE7B9B;
}
}
}
}
}
}
.cont2Right {
width: 820px;
height: 350px;
background: #FFFFFF;
border-radius: 8px;
.title {
width: 100%;
height: 60px;
display: flex;
align-items: center;
p {
width: calc(100% - 240px);
font-family: PingFangSC;
font-weight: 600;
font-size: 16px;
color: rgba(0, 0, 0, 0.85);
margin-left: 20px;
}
.tabList {
width: 240px;
height: 30px;
display: flex;
align-items: center;
p {
width: 83px;
height: 24px;
background: #FAFAFA;
// background: #4188FE;
border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.15);
text-align: center;
line-height: 24px;
font-size: 12px;
font-family: PingFangSC;
font-weight: 400;
color: rgba(120, 118, 118, 0.85);
margin-left: 20px;
margin-top: 20px;
cursor: pointer;
}
.active {
background: #4188FE;
color: rgba(255, 255, 255, 0.85);
}
}
}
.centerThreeChart {
width: 100%;
height: 300px;
}
}
}
.cont3,
.cont4 {
width: 100%;
height: auto;
min-height: 350px;
background: #FFFFFF;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
margin-top: 16px;
.title {
width: 100%;
height: 56px;
display: flex;
align-items: center;
border-bottom: 1px solid #F5F5F5;
p {
width: auto;
font-family: PingFangSC;
font-weight: 600;
font-size: 16px;
color: rgba(0, 0, 0, 0.85);
margin-left: 20px;
}
}
.cont3Chart,
.cont4Chart {
width: 100%;
height: 280px;
}
}
.cont4 {
.title {
height: auto;
min-height: 56px;
padding: 8px 20px;
display: flex;
align-items: center;
justify-content: space-between;
.right {
display: flex;
align-items: center;
gap: 16px;
.one {
display: flex;
align-items: center;
gap: 8px;
p {
margin-left: 0;
white-space: nowrap;
font-weight: 400;
font-size: 14px;
color: rgba(0, 0, 0, 0.65);
}
}
}
}
}
.tableCont {
width: 100%;
margin-top: 16px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
p {
width: auto;
height: 56px;
line-height: 56px;
font-family: PingFangSC;
font-weight: 600;
font-size: 16px;
color: rgba(0, 0, 0, 0.85);
margin-left: 20px;
border-bottom: 1px solid #F5F5F5;
}
}
// ──── 报告 Tab 头部 ────
.reportHeader {
width: 100%;
background: #FFFFFF;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
display: flex;
align-items: center;
min-height: 60px;
padding: 0 20px;
gap: 0;
flex-wrap: wrap;
.tabBar {
display: flex;
align-items: center;
height: 60px;
border-right: 1px solid #F0F0F0;
padding-right: 20px;
margin-right: 20px;
flex-shrink: 0;
.tabItem {
height: 32px;
line-height: 32px;
padding: 0 18px;
font-family: PingFangSC;
font-weight: 400;
font-size: 14px;
color: rgba(0, 0, 0, 0.65);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
margin-right: 4px;
white-space: nowrap;
&:hover {
color: #4188FE;
background: rgba(65, 136, 254, 0.06);
}
&.active {
color: #4188FE;
font-weight: 600;
background: rgba(65, 136, 254, 0.1);
}
}
}
.tabFilter {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
flex: 1;
padding: 8px 0;
.filterLabel {
font-family: PingFangSC;
font-weight: 400;
font-size: 14px;
color: rgba(0, 0, 0, 0.65);
white-space: nowrap;
}
}
}
// ──── 嵌入子报告 ────
.embeddedReport {
width: 100%;
// margin-top: 0 !important;
// 重置子组件外边距,让它与当前页风格一致
:deep(.workOrder),
:deep(.StudentDataStatistics) {
width: 100%;
margin: 0;
padding: 0;
}
}
// ──── 空状态提示 ────
.emptyHint {
width: 100%;
height: 320px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
background: #FFFFFF;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
margin-top: 16px;
.el-icon {
font-size: 56px;
color: #C8C8C8;
}
p {
font-family: PingFangSC;
font-weight: 400;
font-size: 14px;
color: rgba(0, 0, 0, 0.35);
}
}
// ──── 骨架屏公共样式 ────
.skeleton-card {
display: flex;
align-items: center;
}
.chart-skeleton {
width: 100%;
height: 260px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 24px;
box-sizing: border-box;
}
.skeleton-chart-wrap {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding-bottom: 12px;
}
.skeleton-bars {
display: flex;
align-items: flex-end;
justify-content: space-around;
height: 200px;
padding: 0 12px;
.skeleton-bar-group {
display: flex;
flex-direction: column;
align-items: center;
gap: 0;
}
}
.skeleton-pie-wrap {
display: flex;
align-items: center;
justify-content: center;
gap: 32px;
height: 100%;
padding-top: 20px;
.skeleton-pie-legend {
display: flex;
flex-direction: column;
gap: 14px;
.skeleton-legend-item {
display: flex;
align-items: center;
gap: 8px;
}
}
}
.skeleton-line-wrap {
justify-content: center;
padding: 24px 12px 12px;
.skeleton-line-dots {
display: flex;
align-items: flex-end;
justify-content: space-around;
padding: 0 12px;
.skeleton-line-dot {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
}
}
.skeleton-table {
padding: 0 20px 20px;
.skeleton-table-header {
display: flex;
align-items: center;
padding: 14px 0;
border-bottom: 1px solid #F5F5F5;
margin-bottom: 4px;
}
.skeleton-table-row {
display: flex;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid #FAFAFA;
}
}
}
</style>