fix: display synchronized teacher task grades

bugfix/ai-assessment-evidence-rescore
陈沅 1 month ago
parent 2513f591ef
commit 0acf99f780

@ -706,10 +706,10 @@ export function getScoreDetail(params) {
params, params,
}); });
} }
// 保存班级创建教师对学生的人工评分 // 保存教师对单个实训任务的评分
export function saveTeacherScore(params) { export function saveTaskTeacherScore(params) {
return request({ return request({
url: "/api/weight/saveTeacherScore", url: "/api/weight/saveTaskTeacherScore",
method: "POST", method: "POST",
params, params,
}); });

@ -50,34 +50,21 @@
<el-table-column prop="username" label="学号" align="center" /> <el-table-column prop="username" label="学号" align="center" />
<el-table-column prop="className" label="班级" align="center" /> <el-table-column prop="className" label="班级" align="center" />
<el-table-column prop="score" label="参考成绩" align="center" /> <el-table-column prop="score" label="参考成绩" align="center" />
<el-table-column label="老师评分" align="center" min-width="190"> <el-table-column label="老师评分" align="center" min-width="130">
<template #default="{ row }"> <template #default="{ row }">
<div v-if="canManageSelectedClass" class="teacher-score-editor"> <div class="teacher-score-readonly">
<el-input-number
v-model="row.teacherScoreDraft"
:min="0"
:max="100"
:precision="2"
:controls="false"
placeholder="0-100"
:aria-label="`给${row.name || row.username}评分`"
/>
<el-button
type="primary"
size="small"
:loading="savingScoreUserId === row.userId"
@click="saveTeacherScore(row)"
>保存</el-button>
</div>
<div v-else class="teacher-score-readonly">
<strong>{{ formatTeacherScore(row.teacherScore) }}</strong> <strong>{{ formatTeacherScore(row.teacherScore) }}</strong>
<small>只读</small> <small>自动汇总</small>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="实训进度" align="center"> <el-table-column label="实训进度" align="center">
<template #default="{ row }"> <template #default="{ row }">
<span :title="trainingProgressTitle(row)">{{ formatTrainingProgress(row) }}</span> <span :title="trainingProgressTitle(row)">
已完成 {{ row.completedTaskCount || 0 }}/{{ row.totalTaskCount || 0 }}
<small v-if="row.trainingProgress !== null && row.trainingProgress !== undefined">({{ formatTrainingProgress(row) }})</small>
<small v-else>(--)</small>
</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="成绩详情" align="center"> <el-table-column label="成绩详情" align="center">
@ -129,7 +116,7 @@
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
<el-dialog v-model="scoreDetailsStart" title="成绩详情" width="50%" class="F_dialog teacher-dialog"> <el-dialog v-model="scoreDetailsStart" title="任务评阅" width="60%" class="F_dialog teacher-dialog">
<div class="score-detail-summary"> <div class="score-detail-summary">
<div class="score-detail-summary__item"> <div class="score-detail-summary__item">
<span>学生姓名</span> <span>学生姓名</span>
@ -162,6 +149,23 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="aiAssessmentScore" label="AI评测成绩" align="center" /> <el-table-column prop="aiAssessmentScore" label="AI评测成绩" align="center" />
<el-table-column label="教师评分" align="center" min-width="190">
<template #default="{ row }">
<div v-if="canManageSelectedClass && canScoreTask(row)" class="task-score-editor">
<el-input-number
v-model="row.teacherScoreDraft"
:min="0"
:max="100"
:precision="2"
:controls="false"
placeholder="0-100"
:aria-label="`给${row.taskName}评分`"
/>
<el-button type="primary" size="small" :loading="savingTaskId === row.taskId" @click="saveTaskTeacherScore(row)">保存</el-button>
</div>
<span v-else>{{ formatTeacherScore(row.teacherScore) }}</span>
</template>
</el-table-column>
</el-table> </el-table>
</el-dialog> </el-dialog>
</div> </div>
@ -174,7 +178,7 @@ const userStore = useUserStore();
const currentUserId = computed(() => String(userStore.userInfo.userId || "")); const currentUserId = computed(() => String(userStore.userInfo.userId || ""));
const workingCapitalList = ref([]); const workingCapitalList = ref([]);
const loading = ref(false); const loading = ref(false);
const savingScoreUserId = ref(""); const savingTaskId = ref("");
const classList = ref([]); const classList = ref([]);
const teachingClassList = computed(() => classList.value.filter((item) => item.classType === "TEACHING")); const teachingClassList = computed(() => classList.value.filter((item) => item.classType === "TEACHING"));
const scoreParams = ref({ const scoreParams = ref({
@ -221,7 +225,7 @@ const formatScore = (score) => {
return Number.isFinite(value) ? value.toFixed(2).replace(/\.00$/, "") : "--"; return Number.isFinite(value) ? value.toFixed(2).replace(/\.00$/, "") : "--";
}; };
const formatTeacherScore = (score) => score === null || score === undefined || score === "" const formatTeacherScore = (score) => score === null || score === undefined || score === ""
? "未评分" ? "待评阅"
: formatScore(score); : formatScore(score);
const weightFields = ["foundationWeight", "marketInsightWeight", "planningDesignWeight", "developmentValidationWeight", "launchOperationWeight", "comprehensiveTrainingWeight"]; const weightFields = ["foundationWeight", "marketInsightWeight", "planningDesignWeight", "developmentValidationWeight", "launchOperationWeight", "comprehensiveTrainingWeight"];
const rules = Object.fromEntries(weightFields.map((field) => [field, [ const rules = Object.fromEntries(weightFields.map((field) => [field, [
@ -239,16 +243,11 @@ const getList = () => {
if (!scoreParams.value.classId) { if (!scoreParams.value.classId) {
workingCapitalList.value = []; workingCapitalList.value = [];
workingCapitalTotal.value = 0; workingCapitalTotal.value = 0;
return; return Promise.resolve();
} }
loading.value = true; loading.value = true;
indexApi.getScoreList(buildScoreQuery()).then((res) => { return indexApi.getScoreList(buildScoreQuery()).then((res) => {
workingCapitalList.value = (res.data.list || []).map((row) => ({ workingCapitalList.value = res.data.list || [];
...row,
teacherScoreDraft: row.teacherScore === null || row.teacherScore === undefined
? null
: Number(row.teacherScore),
}));
workingCapitalTotal.value = res.data.total; workingCapitalTotal.value = res.data.total;
}).finally(() => (loading.value = false)); }).finally(() => (loading.value = false));
}; };
@ -327,34 +326,48 @@ const getClassScoreDetail = (row) => {
return; return;
} }
scoreDetailStudent.value = { scoreDetailStudent.value = {
userId: row.userId,
name: row.name, name: row.name,
aiTotalScore: row.score, aiTotalScore: row.score,
teacherScore: row.teacherScore, teacherScore: row.teacherScore,
}; };
indexApi.getScoreDetail({ userId: row.userId, classId: scoreParams.value.classId }).then((res) => { indexApi.getScoreDetail({ userId: row.userId, classId: scoreParams.value.classId }).then((res) => {
tableData.value = res.data; tableData.value = (res.data || []).map((item) => ({
...item,
teacherScoreDraft: item.teacherScore === null || item.teacherScore === undefined
? null
: Number(item.teacherScore),
}));
scoreDetailsStart.value = true; scoreDetailsStart.value = true;
}); });
}; };
const saveTeacherScore = (row) => { const canScoreTask = (row) => Boolean(row.submitted) || row.progressStatus === "COMPLETED";
const saveTaskTeacherScore = (row) => {
if (row.teacherScoreDraft === null || row.teacherScoreDraft === undefined || row.teacherScoreDraft === "") { if (row.teacherScoreDraft === null || row.teacherScoreDraft === undefined || row.teacherScoreDraft === "") {
proxy.$message.error("请输入老师评分"); proxy.$message.error("请输入教师评分");
return; return;
} }
const score = Number(row.teacherScoreDraft); const score = Number(row.teacherScoreDraft);
if (!Number.isFinite(score) || score < 0 || score > 100) { if (!Number.isFinite(score) || score < 0 || score > 100) {
proxy.$message.error("老师评分必须为0至100之间的数值"); proxy.$message.error("教师评分必须为0至100之间的数值");
return; return;
} }
savingScoreUserId.value = row.userId; savingTaskId.value = row.taskId;
indexApi.saveTeacherScore({ indexApi.saveTaskTeacherScore({
classId: scoreParams.value.classId, classId: scoreParams.value.classId,
studentUserId: row.userId, studentUserId: scoreDetailStudent.value.userId,
taskId: row.taskId,
teacherScore: score, teacherScore: score,
}).then(() => { }).then(() => {
row.teacherScore = score; row.teacherScore = score;
proxy.$message.success("老师评分保存成功"); proxy.$message.success("任务评分保存成功");
}).finally(() => (savingScoreUserId.value = "")); return getList();
}).then(() => {
const latest = workingCapitalList.value.find((item) => item.userId === scoreDetailStudent.value.userId);
if (latest) {
scoreDetailStudent.value.teacherScore = latest.teacherScore;
}
}).finally(() => (savingTaskId.value = ""));
}; };
// 更新成绩 // 更新成绩
const updateScore = () => { const updateScore = () => {
@ -384,7 +397,7 @@ onMounted(() => {
background: #ffffff; background: #ffffff;
height: calc(100vh - 70px); height: calc(100vh - 70px);
} }
.teacher-score-editor { .task-score-editor {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;

@ -7,15 +7,15 @@ const page = fs.readFileSync(path.join(root, "src/views/teacherEnd/score/index.v
const api = fs.readFileSync(path.join(root, "src/api/teacher.js"), "utf8"); const api = fs.readFileSync(path.join(root, "src/api/teacher.js"), "utf8");
assert.match(page, /label="老师评分"/); assert.match(page, /label="老师评分"/);
assert.match(page, /teacherScoreDraft/); assert.match(page, /自动汇总/);
assert.match(page, /canManageSelectedClass/); assert.match(page, /canManageSelectedClass/);
assert.match(page, /createdBy/); assert.match(page, /createdBy/);
assert.match(page, /saveTeacherScore/); assert.match(page, /saveTaskTeacherScore/);
assert.match(page, /老师评分必须为0至100之间的数值/); assert.match(page, /教师评分必须为0至100之间的数值/);
assert.match(page, /ownClass \|\| teachingClassList\.value\[0\]/); assert.match(page, /ownClass \|\| teachingClassList\.value\[0\]/);
assert.match(page, /getAccessibleTeachingClasses/); assert.match(page, /getAccessibleTeachingClasses/);
assert.doesNotMatch(page, /onMounted\(\(\) => \{\s*getList\(\);\s*getClassList/); assert.doesNotMatch(page, /onMounted\(\(\) => \{\s*getList\(\);\s*getClassList/);
assert.match(api, /url: "\/api\/weight\/saveTeacherScore"/); assert.match(api, /url: "\/api\/weight\/saveTaskTeacherScore"/);
assert.match(api, /url: "\/api\/weight\/accessibleTeachingClasses"/); assert.match(api, /url: "\/api\/weight\/accessibleTeachingClasses"/);
console.log("score center class access and teacher score contract passed"); console.log("score center class access and teacher score contract passed");

@ -11,6 +11,6 @@ assert(page.includes("scoreDetailStudent.name"));
assert(page.includes("scoreDetailStudent.aiTotalScore")); assert(page.includes("scoreDetailStudent.aiTotalScore"));
assert(page.includes("scoreDetailStudent.teacherScore")); assert(page.includes("scoreDetailStudent.teacherScore"));
assert(page.includes("row.teacherScore = score")); assert(page.includes("row.teacherScore = score"));
assert(api.includes("/api/weight/saveTeacherScore")); assert(api.includes("/api/weight/saveTaskTeacherScore"));
console.log("score detail summary static checks passed"); console.log("score detail summary static checks passed");

@ -0,0 +1,17 @@
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const root = path.resolve(__dirname, "..");
const page = fs.readFileSync(path.join(root, "src", "views", "teacherEnd", "score", "index.vue"), "utf8");
const api = fs.readFileSync(path.join(root, "src", "api", "teacher.js"), "utf8");
assert.match(page, /已完成\s*\{\{\s*row\.completedTaskCount/);
assert.match(page, /teacherScoreDraft/);
assert.match(page, /saveTaskTeacherScore/);
assert.doesNotMatch(page, /saveTeacherScore\(row\)/);
assert.doesNotMatch(page, /teacher-score-editor/);
assert.match(api, /saveTaskTeacherScore/);
assert.doesNotMatch(api, /url: "\/api\/weight\/saveTeacherScore"/);
console.log("teacher score sync UI contract passed");
Loading…
Cancel
Save