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/student/index.vue

351 lines
12 KiB
Vue

4 months ago
<template>
<div class="student-home app-container">
<el-alert
v-if="loadError"
class="load-error"
type="error"
:title="loadError"
:closable="false"
show-icon
/>
<el-card class="dashboard-card score-card" v-loading="loading">
4 months ago
<template #header>
<div class="card-header">
<span>我的实训成绩</span>
<small>按当前教学班发布的实训模块统计</small>
4 months ago
</div>
</template>
<div v-if="!loadError" class="score-layout">
<section class="score-visual" aria-label="模块成绩分布">
<div ref="scoreChartRef" class="score-chart"></div>
<div class="score-summary">
<span class="summary-label">综合成绩</span>
<strong>{{ formatScore(report.totalScore) }}</strong>
<span class="summary-unit">分</span>
<span class="rank-copy">当前排名:--</span>
4 months ago
</div>
</section>
<section class="score-table-wrap" aria-label="模块成绩明细">
<el-table :data="displayModules" stripe border class="score-table" empty-text="暂无实训模块">
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column prop="moduleName" label="实训模块" min-width="280" />
<el-table-column label="成绩占比" width="116" align="center">
<template #default="scope">{{ formatWeight(scope.row.weight) }}</template>
4 months ago
</el-table-column>
<el-table-column label="模块得分" width="110" align="center">
<template #default="scope">{{ formatScore(scope.row.score) }}</template>
</el-table-column>
<el-table-column label="完成进度" width="138" align="center">
<template #default="scope">{{ progressText(scope.row) }}</template>
4 months ago
</el-table-column>
</el-table>
</section>
4 months ago
</div>
</el-card>
<el-card class="dashboard-card progress-card" v-loading="loading">
4 months ago
<template #header>
<div class="card-header">
<span>实训进度概览</span>
<small>完成任务越多,模块进度越靠前</small>
4 months ago
</div>
</template>
<div v-if="!loadError" class="progress-layout">
<section class="learning-time" aria-label="有效学习时间">
<div class="time-orbit">
<div class="time-value">{{ studyHours }}</div>
<div class="time-unit">HRS</div>
4 months ago
</div>
<strong>有效学习时间</strong>
<span>累计投入 {{ totalStudyMinutes }} 分钟</span>
</section>
<section class="progress-chart-wrap" aria-label="模块任务完成情况">
<div ref="progressChartRef" class="progress-chart"></div>
</section>
<section class="module-progress-list" aria-label="模块实训进度列表">
<article v-for="(module, index) in displayModules" :key="module.moduleName" class="module-progress-item">
<div class="module-progress-head">
<span class="module-color" :style="{ backgroundColor: moduleColor(index) }"></span>
<strong>{{ module.moduleName }}</strong>
<span>{{ progressText(module) }}</span>
4 months ago
</div>
<el-progress
:percentage="progressPercent(module)"
:stroke-width="8"
:show-text="false"
:color="moduleColor(index)"
/>
</article>
</section>
4 months ago
</div>
</el-card>
</div>
</template>
4 months ago
<script setup>
import * as echarts from "echarts";
import useUserStore from "@/store/modules/user";
4 months ago
import * as indexApi from "@/api/index";
const userStore = useUserStore();
const scoreChartRef = ref(null);
const progressChartRef = ref(null);
const report = ref({ totalScore: 0, modules: [] });
const studyTime = ref({});
const loading = ref(true);
const loadError = ref("");
const MODULE_COLORS = ["#6958e7", "#3698ee", "#ffb83e", "#ff6f4a", "#45b982", "#39b8ce"];
let scoreChart;
let progressChart;
const displayModules = computed(() => report.value.modules || []);
const totalStudyMinutes = computed(() =>
Object.values(studyTime.value || {}).reduce((total, value) => total + (Number(value) || 0), 0)
);
const studyHours = computed(() => (totalStudyMinutes.value / 60).toFixed(1));
const formatScore = (value) => Number(value || 0).toFixed(2).replace(/\.00$/, "");
const formatWeight = (weight) => `${(Number(weight || 0) * 100).toFixed(0)}%`;
const moduleColor = (index) => MODULE_COLORS[index % MODULE_COLORS.length];
const progressPercent = (module) => {
const total = Number(module.totalTaskCount) || 0;
const completed = Number(module.completedTaskCount) || 0;
return total > 0 ? Math.min(100, Math.round((completed / total) * 100)) : 0;
4 months ago
};
const progressText = (module) => `${Number(module.completedTaskCount) || 0}/${Number(module.totalTaskCount) || 0}`;
4 months ago
const renderScoreChart = () => {
if (!scoreChartRef.value) return;
scoreChart = scoreChart || echarts.init(scoreChartRef.value);
scoreChart.setOption({
tooltip: { trigger: "item", valueFormatter: (value) => `${formatScore(value)} 分` },
legend: { orient: "vertical", right: 8, top: "middle", itemWidth: 12, itemHeight: 12 },
series: [{
name: "模块得分",
type: "pie",
radius: ["42%", "72%"],
center: ["34%", "50%"],
avoidLabelOverlap: true,
label: { show: false },
labelLine: { show: false },
data: displayModules.value.map((module, index) => ({
name: module.moduleName,
value: Number(module.score) || 0,
itemStyle: { color: moduleColor(index) }
}))
}]
}, true);
4 months ago
};
const renderProgressChart = () => {
if (!progressChartRef.value) return;
progressChart = progressChart || echarts.init(progressChartRef.value);
const modules = displayModules.value;
progressChart.setOption({
tooltip: { trigger: "axis", axisPointer: { type: "shadow" } },
legend: { top: 0, data: ["已完成", "待完成"] },
grid: { left: 24, right: 24, top: 40, bottom: 22, containLabel: true },
xAxis: { type: "value", minInterval: 1 },
yAxis: {
type: "category",
inverse: true,
axisLabel: { width: 132, overflow: "truncate" },
data: modules.map((module) => module.moduleName)
4 months ago
},
series: [
{
name: "已完成",
type: "bar",
stack: "tasks",
barMaxWidth: 18,
itemStyle: { color: "#3e9cf4", borderRadius: [9, 0, 0, 9] },
data: modules.map((module) => Number(module.completedTaskCount) || 0)
4 months ago
},
{
name: "待完成",
type: "bar",
stack: "tasks",
barMaxWidth: 18,
itemStyle: { color: "#e8eef8", borderRadius: [0, 9, 9, 0] },
data: modules.map((module) => Math.max(0, (Number(module.totalTaskCount) || 0) - (Number(module.completedTaskCount) || 0)))
}
]
}, true);
};
const renderCharts = () => {
renderScoreChart();
renderProgressChart();
4 months ago
};
const resizeCharts = () => {
scoreChart?.resize();
progressChart?.resize();
4 months ago
};
const loadDashboard = async () => {
loading.value = true;
loadError.value = "";
const [reportResult, timeResult] = await Promise.allSettled([
indexApi.getCurrentExperimentReport(),
indexApi.getStudyTime({ userId: userStore.userInfo.userId })
]);
if (reportResult.status !== "fulfilled") {
loadError.value = "新版实训报告加载失败,请刷新页面后重试。";
loading.value = false;
return;
}
report.value = reportResult.value?.data || { totalScore: 0, modules: [] };
studyTime.value = timeResult.status === "fulfilled" ? (timeResult.value?.data || {}) : {};
loading.value = false;
await nextTick();
renderCharts();
4 months ago
};
onMounted(() => {
window.addEventListener("resize", resizeCharts);
loadDashboard();
});
onBeforeUnmount(() => {
window.removeEventListener("resize", resizeCharts);
scoreChart?.dispose();
progressChart?.dispose();
4 months ago
});
</script>
4 months ago
<style lang="scss" scoped>
.student-home {
min-height: calc(100vh - 70px);
background: #f5f8fd;
padding: 20px;
4 months ago
}
.load-error { margin-bottom: 14px; }
.dashboard-card {
margin-bottom: 16px;
border: 0;
box-shadow: 0 10px 30px rgba(37, 77, 135, 0.08);
:deep(.el-card__header) { padding: 17px 20px; }
}
.card-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
span { color: #1d2b4a; font-size: 18px; font-weight: 700; }
small { color: #8a98af; font-size: 13px; }
}
.score-layout { display: grid; grid-template-columns: minmax(520px, 0.95fr) minmax(760px, 1.35fr); min-height: 330px; }
.score-visual {
position: relative;
display: flex;
min-width: 0;
border-right: 1px dashed #d8e2f1;
}
.score-chart { width: 100%; min-height: 330px; }
.score-summary {
position: absolute;
left: 28px;
top: 26px;
display: flex;
flex-direction: column;
color: #72809a;
strong { color: #5645d5; font-size: 34px; line-height: 1.1; }
.summary-unit { color: #5645d5; font-weight: 600; }
.rank-copy { margin-top: 18px; font-size: 13px; }
}
.summary-label { margin-bottom: 4px; font-size: 13px; }
.score-table-wrap { display: flex; align-items: center; min-width: 0; overflow-x: auto; padding: 8px 0 8px 24px; }
.score-table { width: 100%; min-width: 760px; }
.score-table :deep(.el-table__cell) { white-space: nowrap; }
.progress-layout { display: grid; grid-template-columns: 190px minmax(370px, 1fr) minmax(320px, 0.9fr); gap: 24px; align-items: stretch; }
.learning-time {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 270px;
color: #fff;
border-radius: 14px;
background: linear-gradient(150deg, #5fa9fa 0%, #387ee2 100%);
box-shadow: 0 14px 24px rgba(57, 126, 226, 0.24);
strong { margin-top: 18px; font-size: 17px; }
span { margin-top: 7px; font-size: 13px; opacity: 0.88; }
}
.time-orbit {
display: grid;
place-content: center;
width: 108px;
height: 108px;
text-align: center;
border: 4px solid rgba(255, 255, 255, 0.4);
border-radius: 50%;
box-shadow: inset 0 0 0 8px rgba(255, 255, 255, 0.16);
}
.time-value { font-size: 25px; font-weight: 800; line-height: 1; }
.time-unit { margin-top: 5px; font-size: 13px; letter-spacing: 0.08em; }
.progress-chart-wrap { min-width: 0; }
.progress-chart { min-height: 270px; width: 100%; }
.module-progress-list {
display: flex;
flex-direction: column;
justify-content: center;
gap: 14px;
min-width: 0;
padding: 8px 4px;
}
.module-progress-item { min-width: 0; }
.module-progress-head { display: flex; align-items: center; gap: 8px; margin-bottom: 7px; color: #6f7b91; font-size: 13px; }
.module-progress-head strong { flex: 1; overflow: hidden; color: #273858; font-size: 14px; text-overflow: ellipsis; white-space: nowrap; }
.module-color { flex: 0 0 auto; width: 8px; height: 8px; border-radius: 50%; }
@media (max-width: 1320px) {
.score-layout { grid-template-columns: 1fr; }
.score-visual { border-right: 0; border-bottom: 1px dashed #d8e2f1; }
.score-table-wrap { padding: 20px 0 0; }
.progress-layout { grid-template-columns: 180px 1fr; }
.module-progress-list { grid-column: 1 / -1; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 760px) {
.student-home { padding: 12px; }
.card-header { align-items: flex-start; flex-direction: column; }
.score-visual { min-height: 290px; }
.score-chart { min-height: 290px; }
.score-summary { left: 18px; top: 18px; }
.progress-layout { grid-template-columns: 1fr; }
.learning-time { min-height: 210px; }
.module-progress-list { grid-column: auto; grid-template-columns: 1fr; }
4 months ago
}
</style>