|
|
<template>
|
|
|
<div class="grade-center">
|
|
|
<!-- 页面头部:极简标题 -->
|
|
|
<header class="page-head">
|
|
|
<div class="head-left">
|
|
|
<h1 class="page-title">成绩管理中心</h1>
|
|
|
<p class="page-desc">班级成绩走势 · 等级分布 · 关键指标概览</p>
|
|
|
</div>
|
|
|
<div class="head-meta">
|
|
|
<span class="meta-dot"></span>
|
|
|
<span class="meta-text">数据实时同步</span>
|
|
|
</div>
|
|
|
</header>
|
|
|
|
|
|
<!-- KPI 统计卡:4 个关键指标 -->
|
|
|
<section class="kpi-grid">
|
|
|
<div class="kpi-card">
|
|
|
<div class="kpi-icon">
|
|
|
<svg viewBox="0 0 24 24" fill="none">
|
|
|
<path d="M3 7L12 3L21 7L12 11L3 7Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
|
|
|
<path d="M6 9V15C6 15 7.5 17 12 17C16.5 17 18 15 18 15V9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
|
<path d="M21 7V13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
|
</svg>
|
|
|
</div>
|
|
|
<div class="kpi-info">
|
|
|
<span class="kpi-label">班级总数</span>
|
|
|
<div class="kpi-value-wrap">
|
|
|
<span class="kpi-value">{{ kpiData.classCount }}</span>
|
|
|
<span class="kpi-unit">个</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="kpi-card">
|
|
|
<div class="kpi-icon icon-blue">
|
|
|
<svg viewBox="0 0 24 24" fill="none">
|
|
|
<path d="M3 3V21H21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
|
<path d="M7 14L11 10L15 14L21 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
|
</svg>
|
|
|
</div>
|
|
|
<div class="kpi-info">
|
|
|
<span class="kpi-label">平均分</span>
|
|
|
<div class="kpi-value-wrap">
|
|
|
<span class="kpi-value">{{ kpiData.avg }}</span>
|
|
|
<span class="kpi-unit">分</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="kpi-card">
|
|
|
<div class="kpi-icon icon-mint">
|
|
|
<svg viewBox="0 0 24 24" fill="none">
|
|
|
<path d="M7 17L17 7M17 7H9M17 7V15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
|
</svg>
|
|
|
</div>
|
|
|
<div class="kpi-info">
|
|
|
<span class="kpi-label">最高分</span>
|
|
|
<div class="kpi-value-wrap">
|
|
|
<span class="kpi-value">{{ kpiData.max }}</span>
|
|
|
<span class="kpi-unit">分</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="kpi-card">
|
|
|
<div class="kpi-icon icon-amber">
|
|
|
<svg viewBox="0 0 24 24" fill="none">
|
|
|
<path d="M7 7L17 17M17 17H9M17 17V9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
|
</svg>
|
|
|
</div>
|
|
|
<div class="kpi-info">
|
|
|
<span class="kpi-label">最低分</span>
|
|
|
<div class="kpi-value-wrap">
|
|
|
<span class="kpi-value">{{ kpiData.min }}</span>
|
|
|
<span class="kpi-unit">分</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
<!-- 图表双栏 -->
|
|
|
<section class="chart-grid">
|
|
|
<!-- 走势图 -->
|
|
|
<div class="chart-card">
|
|
|
<div class="chart-head">
|
|
|
<div class="chart-title-wrap">
|
|
|
<h2 class="chart-title">班级平均成绩走势</h2>
|
|
|
<span class="chart-sub">多班级横向对比</span>
|
|
|
</div>
|
|
|
<div class="chart-legend">
|
|
|
<span class="legend-item" v-for="(name, idx) in className" :key="idx">
|
|
|
<span class="legend-dot" :style="{ background: getLineColor(idx) }"></span>
|
|
|
{{ name }}
|
|
|
</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="chart-body">
|
|
|
<div id="trend" class="chart-area"></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- 班级统计 -->
|
|
|
<div class="chart-card">
|
|
|
<div class="chart-head">
|
|
|
<div class="chart-title-wrap">
|
|
|
<h2 class="chart-title">班级成绩等级分布</h2>
|
|
|
<span class="chart-sub">按分数段统计人数</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- 筛选 -->
|
|
|
<div class="filter-bar">
|
|
|
<div class="filter-item">
|
|
|
<label class="filter-label">日期筛选</label>
|
|
|
<el-date-picker
|
|
|
v-model="classParams.time"
|
|
|
class="custom-date-picker"
|
|
|
type="datetime"
|
|
|
placeholder="选择日期时间"
|
|
|
format="YYYY/MM/DD HH:mm"
|
|
|
:clearable="true"
|
|
|
/>
|
|
|
</div>
|
|
|
<div class="filter-item">
|
|
|
<label class="filter-label">班级选择</label>
|
|
|
<el-select placeholder="全部班级" v-model="classParams.classId" class="custom-select" :clearable="true">
|
|
|
<el-option v-for="item in userStore.classList" :key="item" :label="item.class_name" :value="item.class_id" />
|
|
|
</el-select>
|
|
|
</div>
|
|
|
<el-button type="primary" class="search-btn" @click="getClassGradeStatistics">
|
|
|
<svg viewBox="0 0 24 24" fill="none" class="btn-svg">
|
|
|
<circle cx="11" cy="11" r="8" stroke="currentColor" stroke-width="2"/>
|
|
|
<path d="M21 21L16.65 16.65" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
|
</svg>
|
|
|
查询
|
|
|
</el-button>
|
|
|
</div>
|
|
|
|
|
|
<div class="stats-body">
|
|
|
<div class="pie-wrap">
|
|
|
<div id="statistics" class="pie-chart"></div>
|
|
|
</div>
|
|
|
<div class="score-list">
|
|
|
<div class="score-row" v-for="item in circularData2" :key="item.name" :class="getScoreCardClass(item.name)">
|
|
|
<span class="score-bar"></span>
|
|
|
<div class="score-row-info">
|
|
|
<span class="score-row-label">{{ item.name }}</span>
|
|
|
<span class="score-row-value">{{ item.value ?? 0 }}<small>分</small></span>
|
|
|
</div>
|
|
|
<span class="score-row-arrow">{{ getIndicatorArrow(item.name) }}</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</section>
|
|
|
</div>
|
|
|
</template>
|
|
|
<script setup>
|
|
|
import * as echarts from "echarts";
|
|
|
import * as teacherApi from "@/api/teacher.js";
|
|
|
const { proxy } = getCurrentInstance();
|
|
|
import { getAssetsFile } from "@/utils/index.js";
|
|
|
import useUserStore from "@/store/modules/user";
|
|
|
const userStore = useUserStore();
|
|
|
const classTime = ref([]);
|
|
|
const className = ref([]);
|
|
|
const classList = ref([]);
|
|
|
const classParams = ref({
|
|
|
classId: "",
|
|
|
time: "",
|
|
|
});
|
|
|
const initstatus = ref(false);
|
|
|
let trendChart = null;
|
|
|
let statisticsChart = null;
|
|
|
const circularData = ref([
|
|
|
{ value: 0, name: "一般", key: "generalCount" },
|
|
|
{ value: 0, name: "优秀", key: "excellentCount" },
|
|
|
{ value: 0, name: "良好", key: "goodCount" },
|
|
|
{ value: 0, name: "不及格", key: "failCount" },
|
|
|
]);
|
|
|
const circularData2 = ref([
|
|
|
{ score: 0, name: "最高分", key: "maxScore" },
|
|
|
{ score: 0, name: "最低分", key: "minScore" },
|
|
|
{ score: 0, name: "平均分", key: "averageScore" },
|
|
|
]);
|
|
|
|
|
|
// KPI 概览数据(纯展示组装,不调接口)
|
|
|
const kpiData = computed(() => {
|
|
|
const find = (n) => circularData2.value.find((i) => i.name === n)?.value ?? 0;
|
|
|
return {
|
|
|
classCount: classList.value.length,
|
|
|
avg: find("平均分"),
|
|
|
max: find("最高分"),
|
|
|
min: find("最低分"),
|
|
|
};
|
|
|
});
|
|
|
|
|
|
// 成绩卡片样式
|
|
|
const getScoreCardClass = (name) => {
|
|
|
const map = {
|
|
|
"最高分": "row-max",
|
|
|
"最低分": "row-min",
|
|
|
"平均分": "row-avg",
|
|
|
};
|
|
|
return map[name] || "";
|
|
|
};
|
|
|
|
|
|
const getIndicatorClass = (name) => {
|
|
|
const map = {
|
|
|
"最高分": "ind-max",
|
|
|
"最低分": "ind-min",
|
|
|
"平均分": "ind-avg",
|
|
|
};
|
|
|
return map[name] || "";
|
|
|
};
|
|
|
|
|
|
const getIndicatorArrow = (name) => {
|
|
|
const map = {
|
|
|
"最高分": "▲",
|
|
|
"最低分": "▼",
|
|
|
"平均分": "◆",
|
|
|
};
|
|
|
return map[name] || "●";
|
|
|
};
|
|
|
|
|
|
// 折线图配色方案(教育蓝绿系:蓝 / 薄荷 / 天青 / 靛 / 紫 / 青绿,克制收敛)
|
|
|
const getLineColor = (index) => {
|
|
|
const colors = [
|
|
|
{ main: "#3b82f6", light: "#bfdbfe" },
|
|
|
{ main: "#52c4a4", light: "#b8e6d5" },
|
|
|
{ main: "#5db8d5", light: "#b8e0eb" },
|
|
|
{ main: "#6366f1", light: "#c7d2fe" },
|
|
|
{ main: "#8b5cf6", light: "#ddd6fe" },
|
|
|
{ main: "#14b8a6", light: "#99f6e4" },
|
|
|
];
|
|
|
return colors[index % colors.length].main;
|
|
|
};
|
|
|
|
|
|
// 获取成绩走势图数据
|
|
|
const getTrendData = async () => {
|
|
|
trendChart = trendChart || echarts.init(document.getElementById("trend"));
|
|
|
const myinde = trendChart;
|
|
|
myinde.setOption({
|
|
|
backgroundColor: "transparent",
|
|
|
tooltip: {
|
|
|
trigger: "axis",
|
|
|
backgroundColor: "rgba(255, 255, 255, 0.98)",
|
|
|
borderColor: "rgba(59, 130, 246, 0.2)",
|
|
|
borderWidth: 1,
|
|
|
padding: [10, 14],
|
|
|
textStyle: {
|
|
|
color: "#1f2937",
|
|
|
fontSize: 13,
|
|
|
},
|
|
|
axisPointer: {
|
|
|
type: "cross",
|
|
|
lineStyle: {
|
|
|
color: "rgba(59, 130, 246, 0.3)",
|
|
|
type: "dashed",
|
|
|
},
|
|
|
crossStyle: {
|
|
|
color: "rgba(59, 130, 246, 0.45)",
|
|
|
},
|
|
|
},
|
|
|
formatter: function(params) {
|
|
|
let result = `<div style="font-weight:600;margin-bottom:8px;color:#1f2937;">${params[0].axisValue}</div>`;
|
|
|
params.forEach(param => {
|
|
|
result += `<div style="display:flex;align-items:center;gap:8px;margin:3px 0;">
|
|
|
<span style="width:8px;height:8px;border-radius:50%;background:${param.color};"></span>
|
|
|
<span style="flex:1;color:#6b7280;">${param.seriesName}</span>
|
|
|
<span style="font-weight:600;color:${param.color};">${param.value}分</span>
|
|
|
</div>`;
|
|
|
});
|
|
|
return result;
|
|
|
},
|
|
|
},
|
|
|
legend: {
|
|
|
show: false,
|
|
|
},
|
|
|
grid: {
|
|
|
left: "3%",
|
|
|
right: "4%",
|
|
|
bottom: "6%",
|
|
|
top: "6%",
|
|
|
containLabel: true,
|
|
|
},
|
|
|
xAxis: {
|
|
|
type: "category",
|
|
|
boundaryGap: false,
|
|
|
data: classTime.value,
|
|
|
axisLine: {
|
|
|
lineStyle: {
|
|
|
color: "rgba(59, 130, 246, 0.2)",
|
|
|
},
|
|
|
},
|
|
|
axisLabel: {
|
|
|
color: "#9ca3af",
|
|
|
fontSize: 11,
|
|
|
margin: 12,
|
|
|
},
|
|
|
axisTick: {
|
|
|
show: false,
|
|
|
},
|
|
|
// 悬停时 X 轴高亮日期标签:深蓝底白字,保证清晰可读
|
|
|
axisPointer: {
|
|
|
label: {
|
|
|
backgroundColor: "#2563eb",
|
|
|
color: "#ffffff",
|
|
|
fontSize: 11,
|
|
|
fontWeight: 600,
|
|
|
padding: [4, 8],
|
|
|
borderRadius: 4,
|
|
|
margin: 10,
|
|
|
shadowColor: "transparent",
|
|
|
shadowBlur: 0,
|
|
|
},
|
|
|
},
|
|
|
},
|
|
|
yAxis: {
|
|
|
type: "value",
|
|
|
min: 0,
|
|
|
max: 100,
|
|
|
splitLine: {
|
|
|
lineStyle: {
|
|
|
color: "rgba(59, 130, 246, 0.08)",
|
|
|
type: "dashed",
|
|
|
},
|
|
|
},
|
|
|
axisLine: {
|
|
|
show: false,
|
|
|
},
|
|
|
axisLabel: {
|
|
|
color: "#9ca3af",
|
|
|
fontSize: 11,
|
|
|
formatter: "{value}",
|
|
|
},
|
|
|
// 悬停时 Y 轴数值标签同样深蓝底白字
|
|
|
axisPointer: {
|
|
|
label: {
|
|
|
backgroundColor: "#2563eb",
|
|
|
color: "#ffffff",
|
|
|
fontSize: 11,
|
|
|
fontWeight: 600,
|
|
|
padding: [4, 8],
|
|
|
borderRadius: 4,
|
|
|
shadowColor: "transparent",
|
|
|
shadowBlur: 0,
|
|
|
},
|
|
|
},
|
|
|
},
|
|
|
series: classList.value.map((item, index) => {
|
|
|
const colorSet = [
|
|
|
{ main: "#3b82f6", light: "rgba(59, 130, 246, 0.18)" },
|
|
|
{ main: "#52c4a4", light: "rgba(82, 196, 164, 0.18)" },
|
|
|
{ main: "#5db8d5", light: "rgba(93, 184, 213, 0.18)" },
|
|
|
{ main: "#6366f1", light: "rgba(99, 102, 241, 0.18)" },
|
|
|
{ main: "#8b5cf6", light: "rgba(139, 92, 246, 0.18)" },
|
|
|
{ main: "#14b8a6", light: "rgba(20, 184, 166, 0.18)" },
|
|
|
][index % 6];
|
|
|
return {
|
|
|
...item,
|
|
|
smooth: 0.4,
|
|
|
lineStyle: {
|
|
|
width: 2.5,
|
|
|
color: colorSet.main,
|
|
|
shadowColor: colorSet.main,
|
|
|
shadowBlur: 6,
|
|
|
},
|
|
|
itemStyle: {
|
|
|
color: colorSet.main,
|
|
|
},
|
|
|
areaStyle: {
|
|
|
color: {
|
|
|
type: "linear",
|
|
|
x: 0,
|
|
|
y: 0,
|
|
|
x2: 0,
|
|
|
y2: 1,
|
|
|
colorStops: [
|
|
|
{ offset: 0, color: colorSet.light },
|
|
|
{ offset: 1, color: "rgba(0, 0, 0, 0)" },
|
|
|
],
|
|
|
},
|
|
|
},
|
|
|
symbol: "circle",
|
|
|
symbolSize: 5,
|
|
|
emphasis: {
|
|
|
scale: true,
|
|
|
scaleSize: 9,
|
|
|
},
|
|
|
};
|
|
|
}),
|
|
|
});
|
|
|
};
|
|
|
|
|
|
// 饼图配色方案(成绩等级语义色:优秀蓝 / 良好薄荷 / 一般暖黄 / 不及格珊瑚)
|
|
|
const getPieColor = (index) => {
|
|
|
const colors = [
|
|
|
{ main: "#3b82f6", light: "rgba(59, 130, 246, 0.2)" },
|
|
|
{ main: "#52c4a4", light: "rgba(82, 196, 164, 0.2)" },
|
|
|
{ main: "#f5b945", light: "rgba(245, 185, 69, 0.2)" },
|
|
|
{ main: "#ef8a76", light: "rgba(239, 138, 118, 0.2)" },
|
|
|
];
|
|
|
return colors[index % colors.length].main;
|
|
|
};
|
|
|
|
|
|
// 成绩统计分析
|
|
|
const getStatisticsData = async () => {
|
|
|
statisticsChart = statisticsChart || echarts.init(document.getElementById("statistics"));
|
|
|
const myChart = statisticsChart;
|
|
|
myChart.setOption({
|
|
|
backgroundColor: "transparent",
|
|
|
tooltip: {
|
|
|
trigger: "item",
|
|
|
backgroundColor: "rgba(255, 255, 255, 0.98)",
|
|
|
borderColor: "rgba(59, 130, 246, 0.2)",
|
|
|
borderWidth: 1,
|
|
|
padding: [10, 14],
|
|
|
textStyle: {
|
|
|
color: "#1f2937",
|
|
|
fontSize: 13,
|
|
|
},
|
|
|
formatter: "{a} <br/>{b} : {c}人 ({d}%)",
|
|
|
},
|
|
|
legend: {
|
|
|
orient: "horizontal",
|
|
|
bottom: 2,
|
|
|
left: "center",
|
|
|
itemWidth: 10,
|
|
|
itemHeight: 10,
|
|
|
itemGap: 12,
|
|
|
textStyle: {
|
|
|
color: "#6b7280",
|
|
|
fontSize: 12,
|
|
|
rich: {
|
|
|
name: { color: "#6b7280", fontSize: 12 },
|
|
|
num: { color: "#3b82f6", fontSize: 12, fontWeight: 600 },
|
|
|
},
|
|
|
},
|
|
|
formatter: (name) => {
|
|
|
const item = circularData.value.find((d) => d.name === name);
|
|
|
const num = item?.value || 0;
|
|
|
return `{name|${name}} {num|${num}人}`;
|
|
|
},
|
|
|
},
|
|
|
series: [
|
|
|
{
|
|
|
type: "pie",
|
|
|
radius: [56, 98],
|
|
|
center: ["50%", "44%"],
|
|
|
avoidLabelOverlap: true,
|
|
|
itemStyle: {
|
|
|
borderRadius: 6,
|
|
|
borderColor: "#ffffff",
|
|
|
borderWidth: 3,
|
|
|
},
|
|
|
label: {
|
|
|
show: false,
|
|
|
},
|
|
|
emphasis: {
|
|
|
scale: true,
|
|
|
scaleSize: 10,
|
|
|
itemStyle: {
|
|
|
shadowBlur: 14,
|
|
|
shadowColor: "rgba(59, 130, 246, 0.22)",
|
|
|
},
|
|
|
},
|
|
|
data: circularData.value.map((item, index) => ({
|
|
|
...item,
|
|
|
itemStyle: {
|
|
|
color: {
|
|
|
type: "linear",
|
|
|
x: 0, y: 0, x2: 1, y2: 1,
|
|
|
colorStops: [
|
|
|
{ offset: 0, color: getPieColor(index) },
|
|
|
{ offset: 1, color: adjustColor(getPieColor(index), -28) },
|
|
|
],
|
|
|
},
|
|
|
},
|
|
|
})),
|
|
|
},
|
|
|
],
|
|
|
});
|
|
|
};
|
|
|
|
|
|
// 颜色调整函数
|
|
|
const adjustColor = (hex, amount) => {
|
|
|
const num = parseInt(hex.replace("#", ""), 16);
|
|
|
const r = Math.max(0, Math.min(255, (num >> 16) + amount));
|
|
|
const g = Math.max(0, Math.min(255, ((num >> 8) & 0x00FF) + amount));
|
|
|
const b = Math.max(0, Math.min(255, (num & 0x0000FF) + amount));
|
|
|
return "#" + (0x1000000 + (r << 16) + (g << 8) + b).toString(16).slice(1);
|
|
|
};
|
|
|
|
|
|
// 获取班级成绩走势函数功能函数
|
|
|
const getClassGradeTrend = () => {
|
|
|
teacherApi.getAverageScoreTrend().then((res) => {
|
|
|
const trends = Array.isArray(res) ? res : [];
|
|
|
classTime.value = trends[0]?.startTime || [];
|
|
|
className.value = trends.map((item) => item.className);
|
|
|
classList.value = trends.map((item) => {
|
|
|
return {
|
|
|
name: item.className,
|
|
|
type: "line",
|
|
|
stack: "Total",
|
|
|
data: item.classAverageScore,
|
|
|
};
|
|
|
});
|
|
|
getTrendData();
|
|
|
});
|
|
|
};
|
|
|
// 获取班级成绩统计分析函数功能函数
|
|
|
const getClassGradeStatistics = () => {
|
|
|
if (!classParams.value.time) {
|
|
|
classParams.value.time = new Date().toString();
|
|
|
} else {
|
|
|
classParams.value.time = classParams.value.time.toString();
|
|
|
}
|
|
|
if (initstatus.value && !classParams.value.classId) {
|
|
|
proxy.$modal.msgWarning("请选择班级");
|
|
|
}
|
|
|
teacherApi.getScoreStatisticsTrend(classParams.value).then((res) => {
|
|
|
circularData.value.forEach((item, index) => {
|
|
|
item.value = res[item.key];
|
|
|
if (res.hasOwnProperty(item.key)) {
|
|
|
item.value = res[item.key];
|
|
|
}
|
|
|
});
|
|
|
circularData2.value.forEach((item, index) => {
|
|
|
item.value = res[item.key];
|
|
|
if (res.hasOwnProperty(item.key)) {
|
|
|
item.value = res[item.key];
|
|
|
}
|
|
|
});
|
|
|
getStatisticsData();
|
|
|
});
|
|
|
};
|
|
|
// 获取班级
|
|
|
const getClassList = () => {
|
|
|
teacherApi.getOwnedTeachingClasses().then((res) => {
|
|
|
userStore.classList = (res.data || []).map((item) => ({
|
|
|
class_name: item.className,
|
|
|
class_id: item.schoolClassId,
|
|
|
}));
|
|
|
});
|
|
|
};
|
|
|
onMounted(() => {
|
|
|
getClassGradeTrend();
|
|
|
getClassGradeStatistics();
|
|
|
getClassList();
|
|
|
initstatus.value = true;
|
|
|
requestAnimationFrame(() => {
|
|
|
trendChart?.resize();
|
|
|
statisticsChart?.resize();
|
|
|
});
|
|
|
});
|
|
|
|
|
|
const handleResize = () => {
|
|
|
trendChart?.resize();
|
|
|
statisticsChart?.resize();
|
|
|
};
|
|
|
window.addEventListener("resize", handleResize);
|
|
|
onBeforeUnmount(() => {
|
|
|
window.removeEventListener("resize", handleResize);
|
|
|
trendChart?.dispose();
|
|
|
statisticsChart?.dispose();
|
|
|
trendChart = null;
|
|
|
statisticsChart = null;
|
|
|
});
|
|
|
</script>
|
|
|
<style lang="scss" scoped>
|
|
|
// ============================================================
|
|
|
// 教师端 · 极简 SaaS 教育风 设计令牌(单一事实来源)
|
|
|
// 颜色:教育蓝单主色 + 中性灰,语义色仅用于状态/等级
|
|
|
// 字号:6 级紧凑字阶;间距:4px 基准刻度;圆角:2 档;按钮:36px 控件
|
|
|
// ============================================================
|
|
|
.grade-center {
|
|
|
/* —— 颜色 —— */
|
|
|
--edu-mint: #52c4a4; // 语义:在线/良好
|
|
|
--edu-amber: #f5b945; // 语义:一般/警示
|
|
|
--edu-coral: #ef8a76; // 语义:不及格/最高
|
|
|
--edu-text: #1f2937; // 主文字
|
|
|
--edu-text-2: #6b7280; // 次文字
|
|
|
--edu-text-3: #9ca3af; // 辅助/占位
|
|
|
--edu-border: #e5e7eb; // 卡片/控件描边
|
|
|
--edu-border-soft: #f0f1f3; // 分区线
|
|
|
--edu-bg: #ffffff; // 画布/卡片底
|
|
|
/* —— 字号(6 级紧凑字阶)—— */
|
|
|
--fs-display: 17px; // 页面标题 700
|
|
|
--fs-h2: 14px; // 卡片标题 700
|
|
|
--fs-caption: 11px; // 标签/副标题/徽章
|
|
|
--fs-data-lg: 26px; // KPI 大数字
|
|
|
--fs-data: 20px; // 行内数字
|
|
|
/* —— 字重 —— */
|
|
|
--fw-title: 700;
|
|
|
--fw-emph: 600;
|
|
|
--fw-body: 400;
|
|
|
/* —— 行高 —— */
|
|
|
--lh-title: 1.35;
|
|
|
--lh-body: 1.5;
|
|
|
--lh-data: 1;
|
|
|
/* —— 控件与按钮 —— */
|
|
|
--control-h: 36px; // 输入框、按钮统一高度
|
|
|
--btn-pad-x: 16px; // 按钮横向内边距
|
|
|
--field-w: 200px; // 输入框/下拉统一宽度
|
|
|
--pie-h: 300px; // 饼图固定高度
|
|
|
--score-list-w: 168px; // 分数列表固定宽度
|
|
|
--chart-min-h: 320px; // 走势图最小高度
|
|
|
/* —— 图标 —— */
|
|
|
--icon-box: 40px; // 图标底盒
|
|
|
--icon-glyph: 20px; // 图标笔画尺寸
|
|
|
--btn-icon: 14px; // 按钮内图标
|
|
|
/* —— 阴影 —— */
|
|
|
--edu-shadow: 0 1px 3px rgba(17, 24, 39, 0.04);
|
|
|
--edu-shadow-hover: 0 4px 14px rgba(17, 24, 39, 0.08);
|
|
|
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: var(--sp-4);
|
|
|
line-height: var(--lh-body);
|
|
|
}
|
|
|
|
|
|
// ===== 页面头部:极简标题 =====
|
|
|
.page-head {
|
|
|
display: flex;
|
|
|
align-items: flex-end;
|
|
|
justify-content: space-between;
|
|
|
|
|
|
.head-left {
|
|
|
.page-title {
|
|
|
font-size: var(--fs-display);
|
|
|
font-weight: var(--fw-title);
|
|
|
line-height: var(--lh-title);
|
|
|
color: var(--edu-text);
|
|
|
margin: 0;
|
|
|
}
|
|
|
|
|
|
.page-desc {
|
|
|
font-size: var(--fs-body);
|
|
|
color: var(--edu-text-3);
|
|
|
margin: var(--sp-1) 0 0;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.head-meta {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: var(--sp-2);
|
|
|
|
|
|
.meta-dot {
|
|
|
width: var(--sp-2);
|
|
|
height: var(--sp-2);
|
|
|
border-radius: 50%;
|
|
|
background: var(--edu-mint);
|
|
|
box-shadow: 0 0 0 3px rgba(82, 196, 164, 0.18);
|
|
|
}
|
|
|
|
|
|
.meta-text {
|
|
|
font-size: var(--fs-caption);
|
|
|
color: var(--edu-text-3);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// ===== KPI 统计卡:4 个关键指标 =====
|
|
|
.kpi-grid {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(4, 1fr);
|
|
|
gap: var(--sp-4);
|
|
|
}
|
|
|
|
|
|
.kpi-card {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: var(--sp-3);
|
|
|
padding: var(--sp-4);
|
|
|
background: var(--edu-bg);
|
|
|
border: 1px solid var(--edu-border);
|
|
|
border-radius: var(--r-card);
|
|
|
box-shadow: var(--edu-shadow);
|
|
|
transition: box-shadow 0.2s ease, border-color 0.2s ease;
|
|
|
|
|
|
&:hover {
|
|
|
border-color: rgba(59, 130, 246, 0.3);
|
|
|
box-shadow: var(--edu-shadow-hover);
|
|
|
}
|
|
|
|
|
|
.kpi-icon {
|
|
|
width: var(--icon-box);
|
|
|
height: var(--icon-box);
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
border-radius: var(--r-control);
|
|
|
background: var(--edu-primary-soft);
|
|
|
color: var(--edu-primary);
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
svg {
|
|
|
width: var(--icon-glyph);
|
|
|
height: var(--icon-glyph);
|
|
|
}
|
|
|
|
|
|
// 统一教育蓝浅底,不再用多色变体
|
|
|
&.icon-blue,
|
|
|
&.icon-mint,
|
|
|
&.icon-amber {
|
|
|
background: var(--edu-primary-soft);
|
|
|
color: var(--edu-primary);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.kpi-info {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: var(--sp-1);
|
|
|
min-width: 0;
|
|
|
|
|
|
.kpi-label {
|
|
|
font-size: var(--fs-caption);
|
|
|
color: var(--edu-text-3);
|
|
|
}
|
|
|
|
|
|
.kpi-value-wrap {
|
|
|
display: flex;
|
|
|
align-items: baseline;
|
|
|
gap: var(--sp-1);
|
|
|
|
|
|
.kpi-value {
|
|
|
font-size: var(--fs-data-lg);
|
|
|
font-weight: var(--fw-title);
|
|
|
line-height: var(--lh-data);
|
|
|
color: var(--edu-text);
|
|
|
font-feature-settings: "tnum";
|
|
|
}
|
|
|
|
|
|
.kpi-unit {
|
|
|
font-size: var(--fs-caption);
|
|
|
color: var(--edu-text-3);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// ===== 图表双栏 =====
|
|
|
.chart-grid {
|
|
|
display: grid;
|
|
|
grid-template-columns: 1.3fr 1fr;
|
|
|
gap: var(--sp-4);
|
|
|
flex: 1;
|
|
|
min-height: 0;
|
|
|
}
|
|
|
|
|
|
.chart-card {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
background: var(--edu-bg);
|
|
|
border: 1px solid var(--edu-border);
|
|
|
border-radius: var(--r-card);
|
|
|
box-shadow: var(--edu-shadow);
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
|
|
|
.chart-head {
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
justify-content: space-between;
|
|
|
gap: var(--sp-3);
|
|
|
padding: var(--sp-3) var(--sp-4);
|
|
|
border-bottom: 1px solid var(--edu-border-soft);
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
.chart-title-wrap {
|
|
|
.chart-title {
|
|
|
font-size: var(--fs-h2);
|
|
|
font-weight: var(--fw-title);
|
|
|
line-height: var(--lh-title);
|
|
|
color: var(--edu-text);
|
|
|
margin: 0;
|
|
|
}
|
|
|
|
|
|
.chart-sub {
|
|
|
display: block;
|
|
|
margin-top: var(--sp-1);
|
|
|
font-size: var(--fs-caption);
|
|
|
color: var(--edu-text-3);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.chart-legend {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
gap: var(--sp-2) var(--sp-3);
|
|
|
justify-content: flex-end;
|
|
|
max-width: 50%;
|
|
|
|
|
|
.legend-item {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: var(--sp-1);
|
|
|
font-size: var(--fs-caption);
|
|
|
color: var(--edu-text-2);
|
|
|
|
|
|
.legend-dot {
|
|
|
width: var(--sp-2);
|
|
|
height: var(--sp-2);
|
|
|
border-radius: 50%;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.chart-body {
|
|
|
flex: 1;
|
|
|
min-height: var(--chart-min-h);
|
|
|
padding: var(--sp-2) var(--sp-3) var(--sp-3);
|
|
|
|
|
|
.chart-area {
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
|
min-height: var(--chart-min-h);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// ===== 筛选条:紧凑横排(控件外观统一走 teacher-light.scss 全局规范)=====
|
|
|
.filter-bar {
|
|
|
display: flex;
|
|
|
align-items: flex-end;
|
|
|
gap: var(--sp-3);
|
|
|
padding: var(--sp-3) var(--sp-4);
|
|
|
border-bottom: 1px solid var(--edu-border-soft);
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
.filter-item {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: var(--sp-1);
|
|
|
|
|
|
.filter-label {
|
|
|
font-size: var(--fs-caption);
|
|
|
color: var(--edu-text-3);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* 查询按钮:外观(方案 A 静奢深蓝)由 teacher-light.scss 全局定义,
|
|
|
此处只保留布局 —— 页面 scoped 无法越过 #app id 特异性,勿在此写外观 */
|
|
|
.search-btn {
|
|
|
margin-left: auto;
|
|
|
gap: var(--sp-2); // 图标与文字 8px 间距
|
|
|
height: var(--control-h);
|
|
|
|
|
|
.btn-svg {
|
|
|
width: var(--btn-icon);
|
|
|
height: var(--btn-icon);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* 字段统一宽度(布局尺寸;外观由全局规范控制)*/
|
|
|
:deep(.el-date-editor),
|
|
|
:deep(.el-select) {
|
|
|
width: var(--field-w) !important;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// ===== 统计主体:饼图 + 分数行 =====
|
|
|
.stats-body {
|
|
|
flex: 1;
|
|
|
display: flex;
|
|
|
gap: var(--sp-3);
|
|
|
padding: var(--sp-3) var(--sp-4) var(--sp-4);
|
|
|
min-height: 0;
|
|
|
|
|
|
.pie-wrap {
|
|
|
flex: 1;
|
|
|
min-width: 0;
|
|
|
align-self: center;
|
|
|
|
|
|
.pie-chart {
|
|
|
width: 100%;
|
|
|
height: var(--pie-h);
|
|
|
min-height: var(--pie-h);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.score-list {
|
|
|
flex: 0 0 var(--score-list-w);
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: var(--sp-2);
|
|
|
justify-content: center;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.score-row {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: var(--sp-3);
|
|
|
padding: var(--sp-3);
|
|
|
background: var(--edu-bg);
|
|
|
border: 1px solid var(--edu-border);
|
|
|
border-radius: var(--r-control);
|
|
|
position: relative;
|
|
|
overflow: hidden;
|
|
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
|
|
|
|
&:hover {
|
|
|
box-shadow: var(--edu-shadow-hover);
|
|
|
}
|
|
|
|
|
|
.score-bar {
|
|
|
width: 3px;
|
|
|
height: var(--sp-5);
|
|
|
border-radius: 3px;
|
|
|
background: var(--edu-border);
|
|
|
flex-shrink: 0;
|
|
|
}
|
|
|
|
|
|
.score-row-info {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: var(--sp-1);
|
|
|
flex: 1;
|
|
|
min-width: 0;
|
|
|
|
|
|
.score-row-label {
|
|
|
font-size: var(--fs-caption);
|
|
|
color: var(--edu-text-3);
|
|
|
}
|
|
|
|
|
|
.score-row-value {
|
|
|
font-size: var(--fs-data);
|
|
|
font-weight: var(--fw-title);
|
|
|
line-height: var(--lh-data);
|
|
|
color: var(--edu-text);
|
|
|
font-feature-settings: "tnum";
|
|
|
|
|
|
small {
|
|
|
font-size: var(--fs-caption);
|
|
|
font-weight: var(--fw-body);
|
|
|
color: var(--edu-text-3);
|
|
|
margin-left: var(--sp-1);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.score-row-arrow {
|
|
|
font-size: var(--fs-caption);
|
|
|
color: var(--edu-text-3);
|
|
|
}
|
|
|
|
|
|
&.row-max {
|
|
|
.score-bar { background: var(--edu-coral); }
|
|
|
.score-row-arrow { color: var(--edu-coral); }
|
|
|
&:hover { border-color: rgba(239, 138, 118, 0.35); }
|
|
|
}
|
|
|
|
|
|
&.row-min {
|
|
|
.score-bar { background: var(--edu-primary); }
|
|
|
.score-row-arrow { color: var(--edu-primary); }
|
|
|
&:hover { border-color: rgba(59, 130, 246, 0.35); }
|
|
|
}
|
|
|
|
|
|
&.row-avg {
|
|
|
.score-bar { background: var(--edu-amber); }
|
|
|
.score-row-arrow { color: var(--edu-amber); }
|
|
|
&:hover { border-color: rgba(245, 185, 69, 0.35); }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// ===== 响应式 =====
|
|
|
@media (max-width: 1200px) {
|
|
|
.kpi-grid {
|
|
|
grid-template-columns: repeat(2, 1fr);
|
|
|
}
|
|
|
|
|
|
.chart-grid {
|
|
|
grid-template-columns: 1fr;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
.kpi-grid {
|
|
|
grid-template-columns: 1fr;
|
|
|
}
|
|
|
|
|
|
.page-head {
|
|
|
flex-direction: column;
|
|
|
align-items: flex-start;
|
|
|
gap: var(--sp-2);
|
|
|
}
|
|
|
|
|
|
.filter-bar {
|
|
|
flex-direction: column;
|
|
|
align-items: stretch;
|
|
|
|
|
|
.search-btn {
|
|
|
margin-left: 0;
|
|
|
width: 100%;
|
|
|
justify-content: center;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.stats-body {
|
|
|
flex-direction: column;
|
|
|
|
|
|
.score-list {
|
|
|
flex: none;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
</style>
|