|
|
|
|
@ -1,424 +1,564 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div v-loading="loading" 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">
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
<span>我的实训成绩</span>
|
|
|
|
|
<small>按当前教学班发布的实训模块统计</small>
|
|
|
|
|
</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">
|
|
|
|
|
<div class="score-metric score-metric--ai">
|
|
|
|
|
<span class="summary-label">AI评分</span>
|
|
|
|
|
<strong>{{ formatScore(aiScore) }}</strong>
|
|
|
|
|
<span class="summary-unit">分</span>
|
|
|
|
|
<div v-loading="loading" 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">
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
<span>我的实训成绩</span>
|
|
|
|
|
<small>按当前教学班发布的实训模块统计</small>
|
|
|
|
|
</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">
|
|
|
|
|
<div class="score-metric score-metric--ai">
|
|
|
|
|
<span class="summary-label">AI评分</span>
|
|
|
|
|
<strong>{{ formatScore(aiScore) }}</strong>
|
|
|
|
|
<span class="summary-unit">分</span>
|
|
|
|
|
</div>
|
|
|
|
|
</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>
|
|
|
|
|
</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>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
</section>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="score-metric score-metric--teacher">
|
|
|
|
|
<span class="summary-label">老师评分</span>
|
|
|
|
|
<strong>{{ formatTeacherScore(teacherScore) }}</strong>
|
|
|
|
|
<span v-if="hasTeacherScore(teacherScore)" class="summary-unit">分</span>
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
<el-card class="dashboard-card progress-card">
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
<span>实训进度概览</span>
|
|
|
|
|
<small>项目学习时长与任务完成进度对比</small>
|
|
|
|
|
</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>
|
|
|
|
|
</div>
|
|
|
|
|
<strong>有效学习时间</strong>
|
|
|
|
|
<span>累计投入 {{ totalStudyMinutes }} 分钟</span>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section class="study-time-chart-wrap" aria-label="项目学习时长">
|
|
|
|
|
<div ref="studyTimeChartRef" class="study-time-chart"></div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section class="progress-chart-wrap" aria-label="模块任务完成情况">
|
|
|
|
|
<div ref="progressChartRef" class="progress-chart"></div>
|
|
|
|
|
</section>
|
|
|
|
|
</div>
|
|
|
|
|
</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>
|
|
|
|
|
</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>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
</section>
|
|
|
|
|
</div>
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
<el-card class="dashboard-card progress-card">
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
<span>实训进度概览</span>
|
|
|
|
|
<small>项目学习时长与任务完成进度对比</small>
|
|
|
|
|
</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>
|
|
|
|
|
</div>
|
|
|
|
|
<strong>有效学习时间</strong>
|
|
|
|
|
<span>累计投入 {{ totalStudyMinutes }} 分钟</span>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section class="study-time-chart-wrap" aria-label="项目学习时长">
|
|
|
|
|
<div ref="studyTimeChartRef" class="study-time-chart"></div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section class="progress-chart-wrap" aria-label="模块任务完成情况">
|
|
|
|
|
<div ref="progressChartRef" class="progress-chart"></div>
|
|
|
|
|
</section>
|
|
|
|
|
</div>
|
|
|
|
|
</el-card>
|
|
|
|
|
</div>
|
|
|
|
|
</el-card>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
import * as echarts from "echarts";
|
|
|
|
|
import useUserStore from "@/store/modules/user";
|
|
|
|
|
import * as indexApi from "@/api/index";
|
|
|
|
|
import { getCurrentProjectStudyTime } from "@/api/projectStudyTime";
|
|
|
|
|
|
|
|
|
|
const userStore = useUserStore();
|
|
|
|
|
const scoreChartRef = ref(null);
|
|
|
|
|
const studyTimeChartRef = ref(null);
|
|
|
|
|
const progressChartRef = ref(null);
|
|
|
|
|
const report = ref({ totalScore: 0, modules: [] });
|
|
|
|
|
const projectStudyTimes = ref([]);
|
|
|
|
|
const loading = ref(true);
|
|
|
|
|
const loadError = ref("");
|
|
|
|
|
|
|
|
|
|
const MODULE_COLORS = ["#1d4ed8", "#2563eb", "#3b82f6", "#60a5fa", "#93c5fd", "#bfdbfe"];
|
|
|
|
|
|
|
|
|
|
let scoreChart;
|
|
|
|
|
let studyTimeChart;
|
|
|
|
|
let progressChart;
|
|
|
|
|
|
|
|
|
|
const displayModules = computed(() => report.value.modules || []);
|
|
|
|
|
const aiScore = computed(() => report.value.aiAssessmentScore ?? report.value.aiScore ?? report.value.totalScore);
|
|
|
|
|
const teacherScore = computed(() => report.value.teacherScore);
|
|
|
|
|
import * as echarts from "echarts"
|
|
|
|
|
import useUserStore from "@/store/modules/user"
|
|
|
|
|
import * as indexApi from "@/api/index"
|
|
|
|
|
import { getCurrentProjectStudyTime } from "@/api/projectStudyTime"
|
|
|
|
|
|
|
|
|
|
const userStore = useUserStore()
|
|
|
|
|
const scoreChartRef = ref(null)
|
|
|
|
|
const studyTimeChartRef = ref(null)
|
|
|
|
|
const progressChartRef = ref(null)
|
|
|
|
|
const report = ref({ totalScore: 0, modules: [] })
|
|
|
|
|
const projectStudyTimes = ref([])
|
|
|
|
|
const loading = ref(true)
|
|
|
|
|
const loadError = ref("")
|
|
|
|
|
|
|
|
|
|
const MODULE_COLORS = ["#1d4ed8", "#2563eb", "#3b82f6", "#60a5fa", "#93c5fd", "#bfdbfe"]
|
|
|
|
|
|
|
|
|
|
let scoreChart
|
|
|
|
|
let studyTimeChart
|
|
|
|
|
let progressChart
|
|
|
|
|
|
|
|
|
|
const displayModules = computed(() => report.value.modules || [])
|
|
|
|
|
const aiScore = computed(() => report.value.aiAssessmentScore ?? report.value.aiScore ?? report.value.totalScore)
|
|
|
|
|
const teacherScore = computed(() => report.value.teacherScore)
|
|
|
|
|
|
|
|
|
|
const studySecondsByProject = computed(() => Object.fromEntries(
|
|
|
|
|
projectStudyTimes.value.map((item) => [item.projectName, Number(item.studySeconds) || 0])
|
|
|
|
|
));
|
|
|
|
|
projectStudyTimes.value.map((item) => [item.projectName, Number(item.studySeconds) || 0])
|
|
|
|
|
))
|
|
|
|
|
const displayStudyProjects = computed(() => {
|
|
|
|
|
const knownProjects = displayModules.value.map((module) => module.moduleName);
|
|
|
|
|
const extraProjects = projectStudyTimes.value.map((item) => item.projectName).filter((name) => !knownProjects.includes(name));
|
|
|
|
|
return [...knownProjects, ...extraProjects].map((projectName) => ({
|
|
|
|
|
projectName,
|
|
|
|
|
studySeconds: studySecondsByProject.value[projectName] || 0,
|
|
|
|
|
}));
|
|
|
|
|
});
|
|
|
|
|
const knownProjects = displayModules.value.map((module) => module.moduleName)
|
|
|
|
|
const extraProjects = projectStudyTimes.value.map((item) => item.projectName).filter((name) => !knownProjects.includes(name))
|
|
|
|
|
return [...knownProjects, ...extraProjects].map((projectName) => ({
|
|
|
|
|
projectName,
|
|
|
|
|
studySeconds: studySecondsByProject.value[projectName] || 0,
|
|
|
|
|
}))
|
|
|
|
|
})
|
|
|
|
|
const totalStudyMinutes = computed(() => Math.round(
|
|
|
|
|
displayStudyProjects.value.reduce((total, item) => total + item.studySeconds, 0) / 60
|
|
|
|
|
));
|
|
|
|
|
displayStudyProjects.value.reduce((total, item) => total + item.studySeconds, 0) / 60
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
const studyHours = computed(() => (totalStudyMinutes.value / 60).toFixed(1));
|
|
|
|
|
const studyHours = computed(() => (totalStudyMinutes.value / 60).toFixed(1))
|
|
|
|
|
|
|
|
|
|
const formatScore = (value) => Number(value || 0).toFixed(2).replace(/\.00$/, "");
|
|
|
|
|
const hasTeacherScore = (value) => value !== null && value !== undefined && value !== "";
|
|
|
|
|
const formatTeacherScore = (value) => hasTeacherScore(value) ? formatScore(value) : "待评阅";
|
|
|
|
|
const formatWeight = (weight) => `${(Number(weight || 0) * 100).toFixed(0)}%`;
|
|
|
|
|
const moduleColor = (index) => MODULE_COLORS[index % MODULE_COLORS.length];
|
|
|
|
|
const formatScore = (value) => Number(value || 0).toFixed(2).replace(/\.00$/, "")
|
|
|
|
|
const hasTeacherScore = (value) => value !== null && value !== undefined && value !== ""
|
|
|
|
|
const formatTeacherScore = (value) => hasTeacherScore(value) ? formatScore(value) : "待评阅"
|
|
|
|
|
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;
|
|
|
|
|
};
|
|
|
|
|
const progressText = (module) => `${Number(module.completedTaskCount) || 0}/${Number(module.totalTaskCount) || 0}`;
|
|
|
|
|
const total = Number(module.totalTaskCount) || 0
|
|
|
|
|
const completed = Number(module.completedTaskCount) || 0
|
|
|
|
|
return total > 0 ? Math.min(100, Math.round((completed / total) * 100)) : 0
|
|
|
|
|
}
|
|
|
|
|
const progressText = (module) => `${Number(module.completedTaskCount) || 0}/${Number(module.totalTaskCount) || 0}`
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
};
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
},
|
|
|
|
|
series: [
|
|
|
|
|
{
|
|
|
|
|
name: "已完成",
|
|
|
|
|
type: "bar",
|
|
|
|
|
stack: "tasks",
|
|
|
|
|
barMaxWidth: 18,
|
|
|
|
|
itemStyle: { color: "#2563eb", borderRadius: [9, 0, 0, 9] },
|
|
|
|
|
data: modules.map((module) => Number(module.completedTaskCount) || 0)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "待完成",
|
|
|
|
|
type: "bar",
|
|
|
|
|
stack: "tasks",
|
|
|
|
|
barMaxWidth: 18,
|
|
|
|
|
itemStyle: { color: "#dbeafe", borderRadius: [0, 9, 9, 0] },
|
|
|
|
|
data: modules.map((module) => Math.max(0, (Number(module.totalTaskCount) || 0) - (Number(module.completedTaskCount) || 0)))
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}, true);
|
|
|
|
|
};
|
|
|
|
|
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)
|
|
|
|
|
},
|
|
|
|
|
series: [
|
|
|
|
|
{
|
|
|
|
|
name: "已完成",
|
|
|
|
|
type: "bar",
|
|
|
|
|
stack: "tasks",
|
|
|
|
|
barMaxWidth: 18,
|
|
|
|
|
itemStyle: { color: "#2563eb", borderRadius: [9, 0, 0, 9] },
|
|
|
|
|
data: modules.map((module) => Number(module.completedTaskCount) || 0)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "待完成",
|
|
|
|
|
type: "bar",
|
|
|
|
|
stack: "tasks",
|
|
|
|
|
barMaxWidth: 18,
|
|
|
|
|
itemStyle: { color: "#dbeafe", borderRadius: [0, 9, 9, 0] },
|
|
|
|
|
data: modules.map((module) => Math.max(0, (Number(module.totalTaskCount) || 0) - (Number(module.completedTaskCount) || 0)))
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}, true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const renderStudyTimeChart = () => {
|
|
|
|
|
if (!studyTimeChartRef.value) return;
|
|
|
|
|
studyTimeChart = studyTimeChart || echarts.init(studyTimeChartRef.value);
|
|
|
|
|
const projects = displayStudyProjects.value;
|
|
|
|
|
studyTimeChart.setOption({
|
|
|
|
|
tooltip: {
|
|
|
|
|
trigger: "axis",
|
|
|
|
|
axisPointer: { type: "shadow" },
|
|
|
|
|
valueFormatter: (value) => `${(Number(value) || 0).toFixed(1)} 分钟`,
|
|
|
|
|
},
|
|
|
|
|
grid: { left: 24, right: 24, top: 36, bottom: 22, containLabel: true },
|
|
|
|
|
xAxis: { type: "value", name: "分钟", minInterval: 1 },
|
|
|
|
|
yAxis: {
|
|
|
|
|
type: "category",
|
|
|
|
|
inverse: true,
|
|
|
|
|
axisLabel: { width: 136, overflow: "truncate" },
|
|
|
|
|
data: projects.map((item) => item.projectName),
|
|
|
|
|
},
|
|
|
|
|
series: [{
|
|
|
|
|
name: "学习时长",
|
|
|
|
|
type: "bar",
|
|
|
|
|
barMaxWidth: 18,
|
|
|
|
|
itemStyle: { color: "#3b82f6", borderRadius: [0, 9, 9, 0] },
|
|
|
|
|
data: projects.map((item) => Number((item.studySeconds / 60).toFixed(1))),
|
|
|
|
|
}],
|
|
|
|
|
}, true);
|
|
|
|
|
};
|
|
|
|
|
if (!studyTimeChartRef.value) return
|
|
|
|
|
studyTimeChart = studyTimeChart || echarts.init(studyTimeChartRef.value)
|
|
|
|
|
const projects = displayStudyProjects.value
|
|
|
|
|
studyTimeChart.setOption({
|
|
|
|
|
tooltip: {
|
|
|
|
|
trigger: "axis",
|
|
|
|
|
axisPointer: { type: "shadow" },
|
|
|
|
|
valueFormatter: (value) => `${(Number(value) || 0).toFixed(1)} 分钟`,
|
|
|
|
|
},
|
|
|
|
|
grid: { left: 24, right: 24, top: 36, bottom: 22, containLabel: true },
|
|
|
|
|
xAxis: { type: "value", name: "分钟", minInterval: 1 },
|
|
|
|
|
yAxis: {
|
|
|
|
|
type: "category",
|
|
|
|
|
inverse: true,
|
|
|
|
|
axisLabel: { width: 136, overflow: "truncate" },
|
|
|
|
|
data: projects.map((item) => item.projectName),
|
|
|
|
|
},
|
|
|
|
|
series: [{
|
|
|
|
|
name: "学习时长",
|
|
|
|
|
type: "bar",
|
|
|
|
|
barMaxWidth: 18,
|
|
|
|
|
itemStyle: { color: "#3b82f6", borderRadius: [0, 9, 9, 0] },
|
|
|
|
|
data: projects.map((item) => Number((item.studySeconds / 60).toFixed(1))),
|
|
|
|
|
}],
|
|
|
|
|
}, true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const renderCharts = () => {
|
|
|
|
|
renderScoreChart();
|
|
|
|
|
renderStudyTimeChart();
|
|
|
|
|
renderProgressChart();
|
|
|
|
|
};
|
|
|
|
|
renderScoreChart()
|
|
|
|
|
renderStudyTimeChart()
|
|
|
|
|
renderProgressChart()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const resizeCharts = () => {
|
|
|
|
|
scoreChart?.resize();
|
|
|
|
|
studyTimeChart?.resize();
|
|
|
|
|
progressChart?.resize();
|
|
|
|
|
};
|
|
|
|
|
scoreChart?.resize()
|
|
|
|
|
studyTimeChart?.resize()
|
|
|
|
|
progressChart?.resize()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const loadDashboard = async () => {
|
|
|
|
|
loading.value = true;
|
|
|
|
|
loadError.value = "";
|
|
|
|
|
const [reportResult, projectStudyTimeResult] = await Promise.allSettled([
|
|
|
|
|
indexApi.getCurrentExperimentReport(),
|
|
|
|
|
getCurrentProjectStudyTime(),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
if (reportResult.status !== "fulfilled") {
|
|
|
|
|
loadError.value = "新版实训报告加载失败,请刷新页面后重试。";
|
|
|
|
|
loading.value = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
report.value = reportResult.value?.data || { totalScore: 0, modules: [] };
|
|
|
|
|
projectStudyTimes.value = projectStudyTimeResult.status === "fulfilled" && Array.isArray(projectStudyTimeResult.value?.data)
|
|
|
|
|
? projectStudyTimeResult.value.data
|
|
|
|
|
: [];
|
|
|
|
|
loading.value = false;
|
|
|
|
|
await nextTick();
|
|
|
|
|
renderCharts();
|
|
|
|
|
};
|
|
|
|
|
loading.value = true
|
|
|
|
|
loadError.value = ""
|
|
|
|
|
const [reportResult, projectStudyTimeResult] = await Promise.allSettled([
|
|
|
|
|
indexApi.getCurrentExperimentReport(),
|
|
|
|
|
getCurrentProjectStudyTime(),
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
if (reportResult.status !== "fulfilled") {
|
|
|
|
|
loadError.value = "新版实训报告加载失败,请刷新页面后重试。"
|
|
|
|
|
loading.value = false
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
report.value = reportResult.value?.data || { totalScore: 0, modules: [] }
|
|
|
|
|
projectStudyTimes.value = projectStudyTimeResult.status === "fulfilled" && Array.isArray(projectStudyTimeResult.value?.data)
|
|
|
|
|
? projectStudyTimeResult.value.data
|
|
|
|
|
: []
|
|
|
|
|
loading.value = false
|
|
|
|
|
await nextTick()
|
|
|
|
|
renderCharts()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
window.addEventListener("resize", resizeCharts);
|
|
|
|
|
loadDashboard();
|
|
|
|
|
});
|
|
|
|
|
window.addEventListener("resize", resizeCharts)
|
|
|
|
|
loadDashboard()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
window.removeEventListener("resize", resizeCharts);
|
|
|
|
|
scoreChart?.dispose();
|
|
|
|
|
studyTimeChart?.dispose();
|
|
|
|
|
progressChart?.dispose();
|
|
|
|
|
});
|
|
|
|
|
window.removeEventListener("resize", resizeCharts)
|
|
|
|
|
scoreChart?.dispose()
|
|
|
|
|
studyTimeChart?.dispose()
|
|
|
|
|
progressChart?.dispose()
|
|
|
|
|
})
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
.student-home {
|
|
|
|
|
min-height: calc(100vh - 70px);
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
padding: 0;
|
|
|
|
|
min-height: calc(100vh - 70px);
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
padding: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.load-error { margin-bottom: 12px; }
|
|
|
|
|
.load-error {
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dashboard-card {
|
|
|
|
|
margin-bottom: 14px;
|
|
|
|
|
border: 1px solid #e5e7eb;
|
|
|
|
|
box-shadow: 0 2px 10px rgba(17, 24, 39, 0.05);
|
|
|
|
|
|
|
|
|
|
:deep(.el-card__header) { padding: 13px 16px; border-bottom: 1px solid #e5e7eb; }
|
|
|
|
|
margin-bottom: 14px;
|
|
|
|
|
border: 1px solid #e5e7eb;
|
|
|
|
|
box-shadow: 0 2px 10px rgba(17, 24, 39, 0.05);
|
|
|
|
|
|
|
|
|
|
:deep(.el-card__header) {
|
|
|
|
|
padding: 13px 16px;
|
|
|
|
|
border-bottom: 1px solid #e5e7eb;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.card-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: baseline;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
|
|
|
|
span { color: #1f2937; font-size: 15px; font-weight: 700; }
|
|
|
|
|
small { color: #9ca3af; font-size: 12px; }
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: baseline;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
|
|
|
|
span {
|
|
|
|
|
color: #1f2937;
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
small {
|
|
|
|
|
color: #9ca3af;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.score-layout { display: grid; grid-template-columns: minmax(520px, 0.95fr) minmax(760px, 1.35fr); min-height: 330px; }
|
|
|
|
|
.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 #e5e7eb;
|
|
|
|
|
position: relative;
|
|
|
|
|
display: flex;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
border-right: 1px dashed #e5e7eb;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.score-chart { width: 100%; min-height: 330px; }
|
|
|
|
|
.score-chart {
|
|
|
|
|
width: 100%;
|
|
|
|
|
min-height: 330px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.score-summary {
|
|
|
|
|
position: absolute;
|
|
|
|
|
left: 28px;
|
|
|
|
|
top: 26px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 18px;
|
|
|
|
|
flex-wrap: nowrap;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
color: #72809a;
|
|
|
|
|
|
|
|
|
|
strong { font-size: 30px; line-height: 1; }
|
|
|
|
|
.summary-unit { font-weight: 600; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.score-metric { display: inline-flex; align-items: baseline; gap: 4px; }
|
|
|
|
|
.score-metric--ai { color: #1d4ed8; }
|
|
|
|
|
.score-metric--teacher { color: #6b7280; }
|
|
|
|
|
.summary-label { font-size: 12px; font-weight: 600; }
|
|
|
|
|
.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; }
|
|
|
|
|
position: absolute;
|
|
|
|
|
left: 34%;
|
|
|
|
|
top: 50%;
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 18px;
|
|
|
|
|
flex-wrap: nowrap;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
color: #72809a;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
|
|
|
|
strong {
|
|
|
|
|
font-size: 26px;
|
|
|
|
|
line-height: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.summary-unit {
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.score-metric {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: baseline;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.score-metric--ai {
|
|
|
|
|
color: #1d4ed8;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.score-metric--teacher {
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.summary-label {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 成绩表:纯白底 + 中性灰边框,无蓝色/斑马重色 */
|
|
|
|
|
.score-table :deep(th.el-table__cell) {
|
|
|
|
|
color: #1f2937;
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
border-color: #e5e7eb;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
color: #1f2937;
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
border-color: #e5e7eb;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.score-table :deep(td.el-table__cell) {
|
|
|
|
|
color: #374151;
|
|
|
|
|
border-color: #e5e7eb;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #374151;
|
|
|
|
|
border-color: #e5e7eb;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.score-table :deep(.el-table__row--striped td.el-table__cell) {
|
|
|
|
|
background: #fafbfc;
|
|
|
|
|
background: #fafbfc;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.score-table :deep(.el-table__body tr:hover > td.el-table__cell) {
|
|
|
|
|
background: #f3f4f6;
|
|
|
|
|
background: #f3f4f6;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.progress-layout { display: grid; grid-template-columns: 190px minmax(370px, 1fr) minmax(370px, 1fr); gap: 24px; align-items: stretch; }
|
|
|
|
|
.progress-layout {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 190px minmax(370px, 1fr) minmax(370px, 1fr);
|
|
|
|
|
gap: 24px;
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.learning-time {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
min-height: 270px;
|
|
|
|
|
color: #1f2937;
|
|
|
|
|
border: 1px solid #e5e7eb;
|
|
|
|
|
border-radius: 14px;
|
|
|
|
|
background: #fafbfc;
|
|
|
|
|
|
|
|
|
|
strong { margin-top: 16px; font-size: 14px; font-weight: 800; }
|
|
|
|
|
span { margin-top: 6px; font-size: 12px; color: #6b7280; }
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
min-height: 270px;
|
|
|
|
|
color: #1f2937;
|
|
|
|
|
border: 1px solid #e5e7eb;
|
|
|
|
|
border-radius: 14px;
|
|
|
|
|
background: #fafbfc;
|
|
|
|
|
|
|
|
|
|
strong {
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
span {
|
|
|
|
|
margin-top: 6px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.time-orbit {
|
|
|
|
|
display: grid;
|
|
|
|
|
place-content: center;
|
|
|
|
|
width: 108px;
|
|
|
|
|
height: 108px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
border: 3px solid #2563eb;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
box-shadow: inset 0 0 0 7px #eff6ff;
|
|
|
|
|
display: grid;
|
|
|
|
|
place-content: center;
|
|
|
|
|
width: 108px;
|
|
|
|
|
height: 108px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
border: 3px solid #2563eb;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
box-shadow: inset 0 0 0 7px #eff6ff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.time-value {
|
|
|
|
|
font-size: 25px;
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
line-height: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.time-unit {
|
|
|
|
|
margin-top: 5px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
letter-spacing: 0.08em;
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.time-value { font-size: 25px; font-weight: 800; line-height: 1; }
|
|
|
|
|
.time-unit { margin-top: 5px; font-size: 12px; letter-spacing: 0.08em; color: #6b7280; }
|
|
|
|
|
.study-time-chart-wrap, .progress-chart-wrap { min-width: 0; }
|
|
|
|
|
.study-time-chart, .progress-chart { min-height: 270px; width: 100%; }
|
|
|
|
|
.study-time-chart-wrap,
|
|
|
|
|
.progress-chart-wrap {
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.study-time-chart,
|
|
|
|
|
.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;
|
|
|
|
|
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%; }
|
|
|
|
|
.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-bottom: 1px dashed #e5e7eb; }
|
|
|
|
|
.score-table-wrap { padding: 20px 0 0; }
|
|
|
|
|
.progress-layout { grid-template-columns: 180px 1fr; }
|
|
|
|
|
.progress-chart-wrap { grid-column: 1 / -1; }
|
|
|
|
|
.score-layout {
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.score-visual {
|
|
|
|
|
border-bottom: 1px dashed #e5e7eb;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.score-table-wrap {
|
|
|
|
|
padding: 20px 0 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.progress-layout {
|
|
|
|
|
grid-template-columns: 180px 1fr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.progress-chart-wrap {
|
|
|
|
|
grid-column: 1 / -1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@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; }
|
|
|
|
|
.progress-chart-wrap { grid-column: auto; }
|
|
|
|
|
.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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.progress-chart-wrap {
|
|
|
|
|
grid-column: auto;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
@ -429,10 +569,10 @@ onBeforeUnmount(() => {
|
|
|
|
|
* 此处仅保留 padding 清零与外层 .index 的 margin 清零,使首页内容贴边。
|
|
|
|
|
*/
|
|
|
|
|
#app .studentLayout section.app-main:has(.student-home) {
|
|
|
|
|
padding: 0 !important;
|
|
|
|
|
padding: 0 !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#app .studentLayout .app-main:has(.student-home) .index {
|
|
|
|
|
margin: 0 !important;
|
|
|
|
|
margin: 0 !important;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|