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

490 lines
18 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!--
* @Author: qinzhenpen qzp1807@126.com
* @Date: 2024-10-31 14:59:14
* @LastEditors: qinzhenpen qzp1807@126.com
* @LastEditTime: 2025-05-16 18:25:56
* @FilePath: \corporate-finance\src\views\teacher\working-capital.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<div class="app-container score" v-loading="loading">
<el-form :inline="true" class="demo-form-inline">
<el-form-item label="用户名:">
<el-input v-model="scoreParams.name" placeholder="" clearable>
<template #suffix>
<el-button type="primary" link color="#51bcb5" @click="getList">
<!-- <img src="@/assets/images/tea搜索.png" alt="" /> -->
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item label="学号:">
<el-input v-model="scoreParams.studentId" placeholder="" clearable>
<template #suffix>
<el-button type="primary" link color="#51bcb5" @click="getList">
<!-- <img src="@/assets/images/tea搜索.png" alt="" /> -->
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item label="班级:">
<el-select v-model="scoreParams.classId" placeholder="请选择班级" @change="handleClassChange">
<el-option v-for="school in teachingClassList" :key="school.schoolClassId" :label="school.className" :value="school.schoolClassId" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :disabled="!scoreParams.classId" @click="exportExcel">导出</el-button>
<span :title="classActionHint">
<el-button type="success" :disabled="!canManageSelectedClass" @click="scoreweight">权重设置</el-button>
</span>
<span :title="classActionHint">
<el-button type="success" :disabled="!canManageSelectedClass" @click="updateScore">成绩更新</el-button>
</span>
</el-form-item>
</el-form>
<div class="table-info1">
<div class="table-hide">
<el-table :data="workingCapitalList" stripe border style="width: 100%">
<el-table-column prop="stuRank" label="排名" align="center" width="120" />
<el-table-column prop="name" label="用户名" align="center" />
<el-table-column prop="username" label="学号" align="center" />
<el-table-column prop="className" label="班级" align="center" />
<el-table-column prop="score" label="参考成绩" align="center" />
<el-table-column label="老师评分" align="center" min-width="130">
<template #default="{ row }">
<div class="teacher-score-readonly">
<strong>{{ formatTeacherScore(row.teacherScore) }}</strong>
<small>自动汇总</small>
</div>
</template>
</el-table-column>
<el-table-column label="实训进度" align="center">
<template #default="{ row }">
<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>
</el-table-column>
<el-table-column label="成绩详情" align="center">
<template #default="{ row }">
<el-button link type="success" @click="getClassScoreDetail(row)">查看详情</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<pagination v-show="workingCapitalTotal > 0" :total="workingCapitalTotal" v-model:page="scoreParams.index" v-model:limit="scoreParams.size" @pagination="getList" />
<el-dialog v-model="WeightStart" title="权重设置" width="500" class="F_dialog teacher-dialog">
<el-form ref="ruleFormRef" :model="scoreWeightObj" class="demo-form" :rules="rules">
<el-form-item label="互联网产品开发基础认知:" prop="foundationWeight" label-width="200">
<el-input v-model.number="scoreWeightObj.foundationWeight" placeholder="" clearable>
<template #suffix>%</template>
</el-input>
</el-form-item>
<el-form-item label="市场洞察与需求分析:" prop="marketInsightWeight" label-width="200">
<el-input v-model.number="scoreWeightObj.marketInsightWeight" placeholder="" clearable>
<template #suffix>%</template>
</el-input>
</el-form-item>
<el-form-item label="产品规划与设计:" prop="planningDesignWeight" label-width="200">
<el-input v-model.number="scoreWeightObj.planningDesignWeight" placeholder="" clearable>
<template #suffix>%</template>
</el-input>
</el-form-item>
<el-form-item label="产品开发与测试验证:" prop="developmentValidationWeight" label-width="200">
<el-input v-model.number="scoreWeightObj.developmentValidationWeight" placeholder="" clearable>
<template #suffix>%</template>
</el-input>
</el-form-item>
<el-form-item label="产品上线与运营推广:" prop="launchOperationWeight" label-width="200">
<el-input v-model.number="scoreWeightObj.launchOperationWeight" placeholder="" clearable>
<template #suffix>%</template>
</el-input>
</el-form-item>
<el-form-item label="综合实训:" prop="comprehensiveTrainingWeight" label-width="200">
<el-input v-model.number="scoreWeightObj.comprehensiveTrainingWeight" placeholder="" clearable>
<template #suffix>%</template>
</el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer" style="text-align: center">
<el-button type="primary" @click="workingCapitalWeightHandle">确定</el-button>
<el-button @click="WeightStart = false">取消</el-button>
</div>
</template>
</el-dialog>
<el-dialog v-model="scoreDetailsStart" title="任务评阅" width="60%" class="F_dialog teacher-dialog">
<div class="score-detail-summary">
<div class="score-detail-summary__item">
<span>学生姓名</span>
<strong>{{ scoreDetailStudent.name || "--" }}</strong>
</div>
<div class="score-detail-summary__item">
<span>AI评测总分</span>
<strong>{{ formatScore(scoreDetailStudent.aiTotalScore) }}</strong>
</div>
<div class="score-detail-summary__item">
<span>老师评分</span>
<strong>{{ formatTeacherScore(scoreDetailStudent.teacherScore) }}</strong>
</div>
</div>
<el-table
:data="tableData"
border
style="width: 100%"
height="400"
:header-cell-style="{
color: '#fff',
fontSize: '14px',
backgroundColor: '#02a7f0!important',
}"
>
<el-table-column prop="taskName" label="实训任务" align="center" />
<el-table-column prop="progressStatus" label="任务状态" align="center">
<template #default="{ row }">
<span>{{ row.progressStatus === "COMPLETED" ? "已完成" : row.progressStatus === "IN_PROGRESS" ? "进行中" : "未开始" }}</span>
</template>
</el-table-column>
<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="0"
:step="1"
: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>
<pagination
v-show="taskDetailTotal > 0"
:total="taskDetailTotal"
v-model:page="taskDetailPagination.index"
v-model:limit="taskDetailPagination.size"
@pagination="getClassScoreDetail"
/>
</el-dialog>
</div>
</template>
<script setup>
import * as indexApi from "@/api/teacher";
const { proxy } = getCurrentInstance();
import useUserStore from "@/store/modules/user";
const userStore = useUserStore();
const currentUserId = computed(() => String(userStore.userInfo.userId || ""));
const workingCapitalList = ref([]);
const loading = ref(false);
const savingTaskId = ref("");
const classList = ref([]);
const teachingClassList = computed(() => classList.value.filter((item) => item.classType === "TEACHING"));
const scoreParams = ref({
classId: "",
index: 1,
size: 10,
schoolId: userStore.userInfo.schoolId,
});
const scoreDetailsStart = ref(false);
const tableData = ref([]);
const taskDetailRows = ref([]);
const taskDetailTotal = ref(0);
const taskDetailPagination = ref({ index: 1, size: 10 });
const scoreDetailStudent = ref({
name: "",
aiTotalScore: null,
teacherScore: null,
});
const WeightStart = ref(false);
// 权重
const workingCapitalTotal = ref(0);
const scoreWeightObj = ref({});
const selectedTeachingClass = computed(() => teachingClassList.value.find(
(item) => item.schoolClassId === scoreParams.value.classId
));
const canManageSelectedClass = computed(() =>
Boolean(selectedTeachingClass.value)
&& String(selectedTeachingClass.value.createdBy || "") === currentUserId.value
);
const classActionHint = computed(() => canManageSelectedClass.value
? ""
: "权重设置和成绩更新仅限班级创建老师操作"
);
const formatTrainingProgress = (row) => {
if (row.trainingProgress === null || row.trainingProgress === undefined) return "--";
const percentage = Number(row.trainingProgress);
if (!Number.isFinite(percentage)) return "--";
return `${percentage.toFixed(2).replace(/\.00$/, "")}%`;
};
const trainingProgressTitle = (row) => {
if (row.trainingProgress === null || row.trainingProgress === undefined) return "";
return `已完成 ${row.completedTaskCount || 0}/${row.totalTaskCount || 0} 项`;
};
const formatScore = (score) => {
if (score === null || score === undefined || score === "") return "--";
const value = Number(score);
return Number.isFinite(value) ? value.toFixed(2).replace(/\.00$/, "") : "--";
};
const formatTeacherScore = (score) => score === null || score === undefined || score === ""
? "待评阅"
: formatScore(score);
const normalizeTaskScoreRow = (row) => ({
...row,
teacherScoreDraft: row.teacherScore === null || row.teacherScore === undefined
? null
: Number(row.teacherScore),
});
const weightFields = ["foundationWeight", "marketInsightWeight", "planningDesignWeight", "developmentValidationWeight", "launchOperationWeight", "comprehensiveTrainingWeight"];
const rules = Object.fromEntries(weightFields.map((field) => [field, [
{ required: true, message: "请输入权重", trigger: "blur" },
{ type: "number", message: "权重必须为数字值" },
]]));
const buildScoreQuery = () => {
const { studentId, ...query } = scoreParams.value;
return {
...query,
userName: studentId,
};
};
const getList = () => {
if (!scoreParams.value.classId) {
workingCapitalList.value = [];
workingCapitalTotal.value = 0;
return Promise.resolve();
}
loading.value = true;
return indexApi.getScoreList(buildScoreQuery()).then((res) => {
workingCapitalList.value = res.data.list || [];
workingCapitalTotal.value = res.data.total;
}).finally(() => (loading.value = false));
};
const scoreweight = () => {
if (!scoreParams.value.classId) {
proxy.$modal.msgWarning("请选择自己的教学班后再设置权重");
return;
}
WeightStart.value = true;
indexApi.getTeachingClassWeight(scoreParams.value.classId).then((res) => {
scoreWeightObj.value = res.data;
weightFields.forEach((key) => (scoreWeightObj.value[key] = Number(scoreWeightObj.value[key]) * 100));
});
};
const workingCapitalWeightHandle = () => {
proxy.$refs.ruleFormRef.validate((valid) => {
if (valid) {
let params = {};
let sum = 0;
weightFields.forEach((key) => {
params[key] = Number(scoreWeightObj.value[key]) / 100;
sum += params[key];
});
if (Math.abs(sum - 1) > 0.000001) {
proxy.$message.error("权重合计为100%");
return;
}
indexApi.updateTeachingClassWeight(scoreParams.value.classId, params).then((res) => {
proxy.$message.success("权重设置成功");
WeightStart.value = false;
});
}
});
};
const getClassList = async () => {
try {
const res = await indexApi.getAccessibleTeachingClasses();
classList.value = res.data || [];
if (scoreParams.value.classId && !teachingClassList.value.some((item) => item.schoolClassId === scoreParams.value.classId)) {
scoreParams.value.classId = "";
}
if (!scoreParams.value.classId) {
const ownClass = teachingClassList.value.find((item) => String(item.createdBy || "") === currentUserId.value);
scoreParams.value.classId = (ownClass || teachingClassList.value[0])?.schoolClassId || "";
}
getList();
} catch (error) {
workingCapitalList.value = [];
workingCapitalTotal.value = 0;
}
};
const handleClassChange = () => {
scoreParams.value.index = 1;
getList();
};
const exportExcel = () => {
if (!scoreParams.value.classId) {
proxy.$modal.msgWarning("请选择教学班后再导出成绩");
return;
}
const { index, size, ...query } = buildScoreQuery();
indexApi.exportScoreList(query).then((res) => {
// 打开怎么是空的
const blob = new Blob([res], { type: "application/vnd.ms-excel" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "成绩表.xlsx";
link.click();
URL.revokeObjectURL(url);
});
};
const refreshScoreDetail = () => {
if (!scoreDetailStudent.value.userId || !scoreParams.value.classId) {
return Promise.resolve();
}
return indexApi.getScoreDetail({
userId: scoreDetailStudent.value.userId,
classId: scoreParams.value.classId,
...taskDetailPagination.value,
}).then((res) => {
const responseData = res.data || [];
if (Array.isArray(responseData)) {
taskDetailRows.value = responseData.map(normalizeTaskScoreRow);
taskDetailTotal.value = taskDetailRows.value.length;
const start = (taskDetailPagination.value.index - 1) * taskDetailPagination.value.size;
tableData.value = taskDetailRows.value.slice(start, start + taskDetailPagination.value.size);
return;
}
tableData.value = (responseData.list || []).map(normalizeTaskScoreRow);
taskDetailTotal.value = Number(responseData.total) || tableData.value.length;
});
};
const getClassScoreDetail = (row) => {
if (row?.userId) {
taskDetailPagination.value.index = 1;
scoreDetailStudent.value = {
userId: row.userId,
name: row.name,
aiTotalScore: row.score,
teacherScore: row.teacherScore,
};
}
if (!scoreParams.value.classId || !scoreDetailStudent.value.userId) {
proxy.$modal.msgWarning("请选择教学班后再查看成绩详情");
return;
}
refreshScoreDetail().then(() => (scoreDetailsStart.value = true));
};
const canScoreTask = (row) => Boolean(row.submitted) || row.progressStatus === "COMPLETED";
const saveTaskTeacherScore = (row) => {
if (row.teacherScoreDraft === null || row.teacherScoreDraft === undefined || row.teacherScoreDraft === "") {
proxy.$message.error("请输入教师评分");
return;
}
const score = Number(row.teacherScoreDraft);
if (!Number.isInteger(score) || score < 0 || score > 100) {
proxy.$message.error("教师评分必须为0至100之间的整数");
return;
}
savingTaskId.value = row.taskId;
indexApi.saveTaskTeacherScore({
classId: scoreParams.value.classId,
studentUserId: scoreDetailStudent.value.userId,
taskId: row.taskId,
teacherScore: score,
}).then(() => {
row.teacherScore = score;
row.teacherScoreDraft = score;
proxy.$message.success("任务评分保存成功");
return Promise.all([refreshScoreDetail(), 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 = () => {
if (!scoreParams.value.classId) {
proxy.$modal.msgWarning("请选择自己的教学班后再更新成绩");
return;
}
loading.value = true;
const params = { schoolId: userStore.userInfo.schoolId };
if (scoreParams.value.classId) {
params.teachingClassId = scoreParams.value.classId;
}
indexApi.doRankOne(params).then((res) => {
proxy.$message.success("成绩更新成功");
loading.value = false;
getList();
});
};
// 拿到当前路由信息
onMounted(() => {
getClassList();
});
</script>
<style lang="scss" scoped>
.score {
background: #ffffff;
height: calc(100vh - 70px);
}
.task-score-editor {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
:deep(.el-input-number) {
width: 104px;
}
}
.score-detail-summary {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
margin-bottom: 16px;
}
.score-detail-summary__item {
display: flex;
flex-direction: column;
gap: 8px;
padding: 14px 18px;
background: rgba(2, 167, 240, 0.08);
border: 1px solid rgba(2, 167, 240, 0.3);
border-radius: 6px;
span {
color: #7f9cab;
font-size: 13px;
}
strong {
color: #02a7f0;
font-size: 20px;
font-variant-numeric: tabular-nums;
}
}
.teacher-score-readonly {
display: inline-flex;
align-items: center;
gap: 8px;
strong {
color: #63dcff;
font-size: 16px;
font-variant-numeric: tabular-nums;
}
small {
padding: 2px 7px;
color: rgba(190, 218, 231, 0.68);
border: 1px solid rgba(96, 172, 207, 0.28);
border-radius: 999px;
}
}
</style>