fix: 优化学生实训操作与成绩展示
parent
ec9e1822f5
commit
e0d64a76b7
@ -0,0 +1,93 @@
|
|||||||
|
<template>
|
||||||
|
<div class="training-task-toolbar">
|
||||||
|
<TrainingMaterialButton :material-name="resolvedMaterial.name" :material-url="resolvedMaterial.url" />
|
||||||
|
<el-button class="task-toolbar-button task-toolbar-button--submit" @click="triggerExistingAction('submit')">
|
||||||
|
<el-icon><CircleCheck /></el-icon>提交任务
|
||||||
|
</el-button>
|
||||||
|
<el-button class="task-toolbar-button" @click="triggerExistingAction('reset')">
|
||||||
|
<el-icon><RefreshLeft /></el-icon>重新实训
|
||||||
|
</el-button>
|
||||||
|
<el-button class="task-toolbar-button" @click="triggerExistingAction('export')">
|
||||||
|
<el-icon><Download /></el-icon>导出实训成果
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, ref, watch } from "vue";
|
||||||
|
import { CircleCheck, Download, RefreshLeft } from "@element-plus/icons-vue";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import { getTrainingTaskByKey } from "@/api/trainingTask";
|
||||||
|
import { resolveTrainingTaskKey } from "@/views/training/taskKeyMap";
|
||||||
|
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
materialName: { type: String, default: "" },
|
||||||
|
materialUrl: { type: String, default: "" },
|
||||||
|
taskKey: { type: String, default: "" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const loadedMaterial = ref({ name: "", url: "" });
|
||||||
|
const resolvedTaskKey = computed(() => resolveTrainingTaskKey(route, props.taskKey));
|
||||||
|
const resolvedMaterial = computed(() => ({
|
||||||
|
name: String(props.materialName || loadedMaterial.value.name || ""),
|
||||||
|
url: String(props.materialUrl || loadedMaterial.value.url || ""),
|
||||||
|
}));
|
||||||
|
|
||||||
|
onMounted(loadMaterial);
|
||||||
|
watch(resolvedTaskKey, loadMaterial);
|
||||||
|
|
||||||
|
async function loadMaterial() {
|
||||||
|
if (props.materialName || props.materialUrl || !resolvedTaskKey.value) return;
|
||||||
|
try {
|
||||||
|
const response = await getTrainingTaskByKey(resolvedTaskKey.value);
|
||||||
|
const task = response?.data || response || {};
|
||||||
|
loadedMaterial.value = {
|
||||||
|
name: String(task.materialName || ""),
|
||||||
|
url: String(task.materialUrl || ""),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
// The material button keeps its unavailable state when a task has no configured case file.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerExistingAction(action) {
|
||||||
|
const labelPriority = {
|
||||||
|
submit: [[/提交任务/], [/完成实训/, /保存并完成/, /校验并完成/]],
|
||||||
|
reset: [[/重新实训/], [/清空填写/]],
|
||||||
|
export: [[/导出实训成果/]],
|
||||||
|
};
|
||||||
|
const buttons = Array.from(document.querySelectorAll("button"))
|
||||||
|
.filter((button) => !button.closest(".training-task-toolbar") && isVisible(button) && !button.disabled);
|
||||||
|
const target = labelPriority[action]
|
||||||
|
.map((matchers) => buttons.find((button) => matchers.some((matcher) => matcher.test(button.innerText || ""))))
|
||||||
|
.find(Boolean);
|
||||||
|
|
||||||
|
if (target) {
|
||||||
|
target.click();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = {
|
||||||
|
submit: "请先完成全部实训步骤,再提交任务。",
|
||||||
|
reset: "当前步骤没有可重新实训的内容。",
|
||||||
|
export: "当前任务暂无可导出的实训成果。",
|
||||||
|
};
|
||||||
|
ElMessage.warning(messages[action]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVisible(element) {
|
||||||
|
const style = window.getComputedStyle(element);
|
||||||
|
return style.display !== "none" && style.visibility !== "hidden" && element.getClientRects().length > 0;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.training-task-toolbar { display:flex; flex-wrap:wrap; align-items:center; justify-content:flex-end; gap:8px; }
|
||||||
|
.task-toolbar-button { min-height:32px; padding:0 10px; border-color:rgba(99,217,255,.55); border-radius:7px; color:#e9fbff; background:rgba(17,126,178,.36); font-size:13px; font-weight:800; }
|
||||||
|
.task-toolbar-button:hover { border-color:rgba(116,235,255,.85); color:#e9fbff; background:rgba(18,146,204,.5); }
|
||||||
|
.task-toolbar-button--submit { border-color:rgba(66,224,187,.7); background:rgba(13,137,111,.34); }
|
||||||
|
.task-toolbar-button--submit:hover { border-color:rgba(100,245,208,.9); background:rgba(14,164,132,.45); }
|
||||||
|
@media (max-width:768px) { .training-task-toolbar { justify-content:stretch; } .training-task-toolbar > * { flex:1 1 auto; } }
|
||||||
|
</style>
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
const assert = require("node:assert/strict");
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const path = require("node:path");
|
||||||
|
const test = require("node:test");
|
||||||
|
|
||||||
|
const source = fs.readFileSync(
|
||||||
|
path.resolve(__dirname, "../src/views/demand/business-feasibility.vue"),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
const api = fs.readFileSync(
|
||||||
|
path.resolve(__dirname, "../src/api/studentTrainingAnswer.js"),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
const request = fs.readFileSync(
|
||||||
|
path.resolve(__dirname, "../src/utils/request.js"),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
|
||||||
|
test("商业可行性报告上传兼容公共上传接口的顶层文件地址", () => {
|
||||||
|
assert.match(
|
||||||
|
source,
|
||||||
|
/const url = response\?\.url \|\| response\?\.data\?\.url \|\| ""/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("商业可行性报告校验失败仅交由页面显示中文提示", () => {
|
||||||
|
assert.match(
|
||||||
|
api,
|
||||||
|
/business-feasibility\/step-3\/validate[\s\S]{0,240}skipErrorMessage:\s*true/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
request,
|
||||||
|
/code !== 200[\s\S]{0,260}skipErrorMessage[\s\S]{0,260}Promise\.reject\(new Error\(msg\)\)/,
|
||||||
|
);
|
||||||
|
});
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
const fs = require("node:fs");
|
||||||
|
const path = require("node:path");
|
||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert/strict");
|
||||||
|
|
||||||
|
const source = fs.readFileSync(path.resolve(__dirname, "../src/views/student/index.vue"), "utf8");
|
||||||
|
|
||||||
|
test("首页成绩环图不再同时展示会与图例重叠的外部标签", () => {
|
||||||
|
assert.match(source, /label:\s*\{\s*show:\s*false\s*\}/);
|
||||||
|
assert.match(source, /legend:\s*\{\s*orient:\s*"vertical"/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("首页成绩表的数值保持单行,并在窄空间内横向滚动", () => {
|
||||||
|
assert.match(source, /\.score-table-wrap[^}]*overflow-x:\s*auto/);
|
||||||
|
assert.match(source, /\.score-table[^}]*min-width:/);
|
||||||
|
assert.match(source, /\.score-table\s*:deep\([^)]*\)\s*\{[^}]*white-space:\s*nowrap/);
|
||||||
|
});
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
const fs = require("node:fs");
|
||||||
|
const path = require("node:path");
|
||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert/strict");
|
||||||
|
|
||||||
|
const frontendRoot = path.resolve(__dirname, "..");
|
||||||
|
const toolbarPath = path.join(frontendRoot, "src/views/components/TrainingTaskToolbar.vue");
|
||||||
|
|
||||||
|
const studentTrainingPages = [
|
||||||
|
"foundation/new-product-survey.vue",
|
||||||
|
"foundation/product-development-factors.vue",
|
||||||
|
"foundation/product-development-process.vue",
|
||||||
|
"demand/industry-demand-analysis.vue",
|
||||||
|
"demand/competitive-environment-analysis.vue",
|
||||||
|
"demand/business-feasibility.vue",
|
||||||
|
"demand/people.vue",
|
||||||
|
"demand/consumer-behavior.vue",
|
||||||
|
"demand/consumer-scenario.vue",
|
||||||
|
"demand/feature-conversion.vue",
|
||||||
|
"product/market-opportunity-selection.vue",
|
||||||
|
"product/differentiation.vue",
|
||||||
|
"product/structure.vue",
|
||||||
|
"product/after-sales-service.vue",
|
||||||
|
"product/requirements-doc.vue",
|
||||||
|
"product/hardware-cost.vue",
|
||||||
|
"product/price.vue",
|
||||||
|
"test/index.vue",
|
||||||
|
"test/promotion.vue",
|
||||||
|
"channel/index.vue",
|
||||||
|
"channel/supplier.vue",
|
||||||
|
"assessment/index.vue",
|
||||||
|
"assessment/diagnosis.vue",
|
||||||
|
"assessment/optimization.vue",
|
||||||
|
];
|
||||||
|
|
||||||
|
test("统一实训工具栏同时提供案例下载、提交、重新实训和导出入口", () => {
|
||||||
|
const source = fs.readFileSync(toolbarPath, "utf8");
|
||||||
|
|
||||||
|
assert.match(source, /TrainingMaterialButton/);
|
||||||
|
assert.match(source, /提交任务/);
|
||||||
|
assert.match(source, /重新实训/);
|
||||||
|
assert.match(source, /导出实训成果/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("每个学生端实训菜单均通过任务说明区接入统一顶部工具栏", () => {
|
||||||
|
for (const relativePath of studentTrainingPages) {
|
||||||
|
const source = fs.readFileSync(path.join(frontendRoot, "src/views", relativePath), "utf8");
|
||||||
|
if (relativePath === "assessment/index.vue") {
|
||||||
|
assert.match(source, /<TrainingTaskToolbar\b/, relativePath);
|
||||||
|
} else {
|
||||||
|
assert.match(source, /<TrainingTaskBrief\b/, relativePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const briefSource = fs.readFileSync(path.join(frontendRoot, "src/views/components/TrainingTaskBrief.vue"), "utf8");
|
||||||
|
assert.match(briefSource, /<TrainingTaskToolbar\b/);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue