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

656 lines
23 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="score" v-loading="loading">
<!-- 扁平筛选条(无卡片底,直接落在画布上) -->
<div class="query-bar">
<div class="query-item">
<span class="query-label">用户名:</span>
<el-input v-model="scoreParams.name" style="width: 14rem" placeholder="请输入" clearable />
</div>
<div class="query-item">
<span class="query-label">学号:</span>
<el-input v-model="scoreParams.studentId" style="width: 14rem" placeholder="请输入" clearable />
</div>
<div class="query-item">
<span class="query-label">班级:</span>
<el-select v-model="scoreParams.classId" style="width: 14rem" placeholder="请选择班级" @change="handleClassChange">
<el-option v-for="school in teachingClassList" :key="school.schoolClassId" :label="school.className" :value="school.schoolClassId" />
</el-select>
</div>
<el-button @click="getList">
<el-icon>
<Search />
</el-icon>
查询
</el-button>
</div>
<div class="btn-group">
<el-button :disabled="!scoreParams.classId" @click="exportExcel">
<el-icon>
<Download />
</el-icon>
导出
</el-button>
<span :title="classActionHint">
<el-button :disabled="!canManageSelectedClass" @click="scoreweight">
<el-icon>
<Setting />
</el-icon>
权重设置
</el-button>
</span>
<span :title="classActionHint">
<el-button :disabled="!canManageSelectedClass" @click="updateScore">
<el-icon>
<Refresh />
</el-icon>
成绩更新
</el-button>
</span>
</div>
<div class="table-info1">
<div class="table-hide">
<el-table :data="workingCapitalList" style="width: 100%">
<el-table-column prop="stuRank" label="排名" align="center" width="120" />
<el-table-column prop="name" label="用户名" align="center">
<template #default="{ row }">
<div class="name-cell">
<span class="name-badge">{{ (row.name || "?").charAt(0) }}</span>
<span class="name-text">{{ row.name }}</span>
</div>
</template>
</el-table-column>
<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 text type="success" @click="getClassScoreDetail(row)">
<el-icon>
<View />
</el-icon>
查看详情
</el-button>
</template>
</el-table-column>
<template #empty>
<div class="table-empty">
<el-icon class="empty-icon">
<FolderOpened />
</el-icon>
<p class="empty-title">暂无成绩数据</p>
<p class="empty-tip">请先在上方选择教学班后查询成绩</p>
</div>
</template>
</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 @click="WeightStart = false">
<el-icon>
<Close />
</el-icon>
取消
</el-button>
<el-button type="primary" @click="workingCapitalWeightHandle">
<el-icon>
<Check />
</el-icon>
确定
</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" style="width: 100%" height="400">
<el-table-column prop="taskName" label="实训任务" align="center">
<template #default="{ row }">
<div class="name-cell">
<span class="name-badge">{{ (row.taskName || "?").charAt(0) }}</span>
<span class="name-text">{{ row.taskName }}</span>
</div>
</template>
</el-table-column>
<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-icon>
<Check />
</el-icon>
保存
</el-button>
</div>
<span v-else>{{ formatTeacherScore(row.teacherScore) }}</span>
</template>
</el-table-column>
<template #empty>
<div class="table-empty">
<el-icon class="empty-icon">
<FolderOpened />
</el-icon>
<p class="empty-title">暂无任务评阅数据</p>
<p class="empty-tip">该学生暂无可评阅的实训任务</p>
</div>
</template>
</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 { Search, Download, Setting, Refresh, View, Check, Close, FolderOpened } from "@element-plus/icons-vue";
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 {
/* ===== 筛选条(扁平,直接落在画布上;外观归 teacher-light.scss 全局接管)===== */
.query-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--sp-3);
margin-bottom: var(--sp-2);
.query-item {
display: flex;
align-items: center;
gap: var(--sp-2);
.query-label {
color: var(--text-body);
font-size: var(--fs-body);
white-space: nowrap;
}
}
}
/* ===== 操作按钮行 ===== */
.btn-group {
display: flex;
align-items: center;
gap: var(--sp-2);
margin-bottom: var(--sp-4);
}
/* ===== 名称列首字徽标(用户名 / 实训任务)===== */
.name-cell {
display: inline-flex;
align-items: center;
gap: var(--sp-2);
vertical-align: middle;
.name-badge {
flex: 0 0 auto;
width: 28px;
height: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--edu-primary-soft);
color: var(--edu-primary);
border-radius: var(--r-control);
font-size: var(--fs-body);
font-weight: 600;
}
.name-text {
font-weight: 600;
color: var(--text-strong);
}
}
/* ===== 自定义空状态 ===== */
.table-empty {
padding: var(--sp-5) 0;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--sp-1);
.empty-icon {
font-size: 28px;
color: var(--text-muted);
}
.empty-title {
margin: var(--sp-1) 0 0;
font-size: var(--fs-body);
font-weight: 600;
color: var(--text-body);
}
.empty-tip {
margin: 0;
font-size: var(--fs-tag);
color: var(--text-muted);
}
}
/* ===== 数据表格(纯白 + 灰线;stripe/border 已删,基准由全局接管)===== */
.table-info1 .el-table {
background: #ffffff;
border: 1px solid var(--border-color);
border-radius: var(--r-card);
overflow: hidden;
:deep(.el-table__header-wrapper th) {
background: #ffffff;
color: var(--text-strong);
font-weight: 600;
font-size: var(--fs-body);
}
:deep(.el-table__body tr:hover > td) {
background: #f9fafb !important;
}
:deep(.el-table__cell) {
font-size: var(--fs-body);
color: var(--text-strong);
border-color: var(--border-color);
}
}
}
/* ===== 弹窗内布局(外观由 teacher-light.scss 全局接管)===== */
.task-score-editor {
display: flex;
align-items: center;
justify-content: center;
gap: var(--sp-2);
:deep(.el-input-number) {
width: 104px;
}
}
.score-detail-summary {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--sp-3);
margin-bottom: var(--sp-4);
}
.score-detail-summary__item {
display: flex;
flex-direction: column;
gap: var(--sp-2);
padding: var(--sp-3) var(--sp-4);
background: #ffffff;
border: 1px solid var(--border-color);
border-radius: var(--r-control);
span {
color: var(--text-body);
font-size: var(--fs-body);
}
strong {
color: var(--text-strong);
font-size: var(--fs-num);
font-variant-numeric: tabular-nums;
}
}
.teacher-score-readonly {
display: inline-flex;
align-items: center;
gap: var(--sp-2);
strong {
color: var(--text-strong);
font-size: var(--fs-card);
font-variant-numeric: tabular-nums;
}
small {
padding: var(--sp-1) var(--sp-2);
color: var(--text-muted);
border: 1px solid var(--border-color);
border-radius: var(--r-control);
font-size: var(--fs-tag);
}
}
</style>