You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dianshang-qianduan/src/views/teacherEnd/index.vue

1012 lines
27 KiB
Vue

2 years ago
<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>
2 years ago
</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"
/>
4 months ago
</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>
4 months ago
</div>
<span class="score-row-arrow">{{ getIndicatorArrow(item.name) }}</span>
4 months ago
</div>
</div>
</div>
</div>
</section>
2 years ago
</div>
</template>
<script setup>
4 months ago
import * as echarts from "echarts";
import * as teacherApi from "@/api/teacher.js";
const { proxy } = getCurrentInstance();
import { getAssetsFile } from "@/utils/index.js";
4 months ago
import useUserStore from "@/store/modules/user";
const userStore = useUserStore();
const classTime = ref([]);
const className = ref([]);
const classList = ref([]);
const classParams = ref({
classId: "",
time: "",
2 years ago
});
const initstatus = ref(false);
let trendChart = null;
let statisticsChart = null;
4 months ago
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;
};
4 months ago
// 获取成绩走势图数据
const getTrendData = async () => {
trendChart = trendChart || echarts.init(document.getElementById("trend"));
const myinde = trendChart;
4 months ago
myinde.setOption({
backgroundColor: "transparent",
4 months ago
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;
},
4 months ago
},
legend: {
show: false,
4 months ago
},
grid: {
left: "3%",
4 months ago
right: "4%",
bottom: "6%",
top: "6%",
4 months ago
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,
},
},
4 months ago
},
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,
},
},
4 months ago
},
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,
},
};
}),
4 months ago
});
};
// 饼图配色方案(成绩等级语义色:优秀蓝 / 良好薄荷 / 一般暖黄 / 不及格珊瑚)
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;
};
4 months ago
// 成绩统计分析
const getStatisticsData = async () => {
statisticsChart = statisticsChart || echarts.init(document.getElementById("statistics"));
const myChart = statisticsChart;
4 months ago
myChart.setOption({
backgroundColor: "transparent",
4 months ago
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}%)",
4 months ago
},
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}人}`;
},
4 months ago
},
series: [
{
type: "pie",
radius: [56, 98],
center: ["50%", "44%"],
avoidLabelOverlap: true,
itemStyle: {
borderRadius: 6,
borderColor: "#ffffff",
borderWidth: 3,
},
label: {
show: false,
},
4 months ago
emphasis: {
scale: true,
scaleSize: 10,
4 months ago
itemStyle: {
shadowBlur: 14,
shadowColor: "rgba(59, 130, 246, 0.22)",
4 months ago
},
},
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) },
],
},
},
})),
4 months ago
},
],
});
};
// 颜色调整函数
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);
};
4 months ago
// 获取班级成绩走势函数功能函数
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) => {
4 months ago
return {
name: item.className,
type: "line",
stack: "Total",
data: item.classAverageScore,
4 months ago
};
});
getTrendData();
});
};
// 获取班级成绩统计分析函数功能函数
const getClassGradeStatistics = () => {
if (!classParams.value.time) {
classParams.value.time = new Date().toString();
} else {
classParams.value.time = classParams.value.time.toString();
2 years ago
}
if (initstatus.value && !classParams.value.classId) {
proxy.$modal.msgWarning("请选择班级");
}
4 months ago
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];
2 years ago
}
4 months ago
});
circularData2.value.forEach((item, index) => {
item.value = res[item.key];
if (res.hasOwnProperty(item.key)) {
item.value = res[item.key];
2 years ago
}
4 months ago
});
getStatisticsData();
});
};
// 获取班级
const getClassList = () => {
teacherApi.getOwnedTeachingClasses().then((res) => {
userStore.classList = (res.data || []).map((item) => ({
class_name: item.className,
class_id: item.schoolClassId,
4 months ago
}));
});
};
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;
4 months ago
});
</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);
2 years ago
}
}
}
}
// ===== 图表双栏 =====
.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);
2 years ago
}
}
.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); }
2 years ago
}
}
// ===== 响应式 =====
@media (max-width: 1200px) {
.kpi-grid {
grid-template-columns: repeat(2, 1fr);
}
.chart-grid {
grid-template-columns: 1fr;
}
2 years ago
}
@media (max-width: 768px) {
.kpi-grid {
grid-template-columns: 1fr;
2 years ago
}
.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;
}
2 years ago
}
}
</style>