refactor(student-view): enhance score display logic and improve UI consistency

- Updated score display logic to show teacher scores when available, otherwise defaulting to AI scores.
- Introduced computed properties for better handling of score visibility and labeling.
- Improved the layout and styling of score metrics for better visual consistency across the application.
- Added new dialog components for batch AI scoring and AI-assisted evaluation in the teacher's score review panel.
dev-QQq
qinzhenpen 5 hours ago
parent d77b307457
commit cb1d33da5a

@ -20,17 +20,12 @@
<div v-if="!loadError" class="score-layout"> <div v-if="!loadError" class="score-layout">
<section class="score-visual" aria-label="模块成绩分布"> <section class="score-visual" aria-label="模块成绩分布">
<div ref="scoreChartRef" class="score-chart"></div> <div ref="scoreChartRef" class="score-chart"></div>
<div class="score-summary"> <div v-if="hasScore" class="score-summary">
<div class="score-metric score-metric--ai"> <div class="score-metric" :class="scoreType === 'teacher' ? 'score-metric--teacher' : 'score-metric--ai'">
<span class="summary-label">AI评分</span> <span class="summary-label">{{ scoreLabel }}</span>
<strong>{{ formatScore(aiScore) }}</strong> <strong>{{ displayScore }}</strong>
<span class="summary-unit">分</span> <span class="summary-unit">分</span>
</div> </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>
</div>
</div> </div>
</section> </section>
@ -107,6 +102,20 @@ const displayModules = computed(() => report.value.modules || []);
const aiScore = computed(() => report.value.aiAssessmentScore ?? report.value.aiScore ?? report.value.totalScore); const aiScore = computed(() => report.value.aiAssessmentScore ?? report.value.aiScore ?? report.value.totalScore);
const teacherScore = computed(() => report.value.teacherScore); const teacherScore = computed(() => report.value.teacherScore);
// 新的评分显示逻辑:有老师评分显示老师评分,否则显示AI评分,都没有就不显示
const hasTeacherScore = computed(() =>
teacherScore.value !== null && teacherScore.value !== undefined && teacherScore.value !== ""
);
const hasAiScore = computed(() =>
aiScore.value !== null && aiScore.value !== undefined && aiScore.value !== ""
);
const hasScore = computed(() => hasTeacherScore.value || hasAiScore.value);
const scoreType = computed(() => hasTeacherScore.value ? 'teacher' : 'ai');
const displayScore = computed(() =>
hasTeacherScore.value ? formatScore(teacherScore.value) : formatScore(aiScore.value)
);
const scoreLabel = computed(() => hasTeacherScore.value ? '老师评分' : 'AI评分');
const studySecondsByProject = computed(() => Object.fromEntries( 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])
)); ));
@ -125,8 +134,6 @@ const totalStudyMinutes = computed(() => Math.round(
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 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 formatWeight = (weight) => `${(Number(weight || 0) * 100).toFixed(0)}%`;
const moduleColor = (index) => MODULE_COLORS[index % MODULE_COLORS.length]; const moduleColor = (index) => MODULE_COLORS[index % MODULE_COLORS.length];
const progressPercent = (module) => { const progressPercent = (module) => {
@ -336,8 +343,9 @@ onBeforeUnmount(() => {
.score-metric { display: inline-flex; align-items: baseline; gap: 4px; } .score-metric { display: inline-flex; align-items: baseline; gap: 4px; }
.score-metric--ai { color: #1d4ed8; } .score-metric--ai { color: #1d4ed8; }
.score-metric--ai strong { font-size: 26px; }
.score-metric--teacher { color: #6b7280; } .score-metric--teacher { color: #6b7280; }
.score-metric--teacher strong { font-size: 20px; } .score-metric--teacher strong { font-size: 26px; }
.summary-label { font-size: 12px; font-weight: 600; } .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-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 { width: 100%; min-width: 760px; }
@ -426,8 +434,8 @@ onBeforeUnmount(() => {
.card-header { align-items: flex-start; flex-direction: column; } .card-header { align-items: flex-start; flex-direction: column; }
.score-visual { min-height: 290px; } .score-visual { min-height: 290px; }
.score-chart { min-height: 290px; } .score-chart { min-height: 290px; }
.score-summary { gap: 4px; strong { font-size: 22px; } } .score-summary { gap: 4px; }
.score-metric--teacher strong { font-size: 16px; } .score-summary strong { font-size: 22px; }
.progress-layout { grid-template-columns: 1fr; } .progress-layout { grid-template-columns: 1fr; }
.learning-time { min-height: 210px; } .learning-time { min-height: 210px; }
.progress-chart-wrap { grid-column: auto; } .progress-chart-wrap { grid-column: auto; }

@ -168,6 +168,150 @@
</template> </template>
</el-dialog> </el-dialog>
<el-dialog
v-model="batchAiScoreDialogVisible"
width="520px"
append-to-body
:show-close="false"
:close-on-click-modal="false"
:close-on-press-escape="false"
class="score-review-dialog batch-ai-score-dialog"
>
<template #header>
<div class="dialog-header">
<div class="dialog-header-main">
<span class="dialog-badge">
<el-icon><TrendCharts /></el-icon>
批量AI评分
</span>
<h3 class="dialog-title">已选择 {{ selectedStudents.length }} 名学生</h3>
</div>
<button
v-if="batchAiScoreProgress.status !== 'running'"
type="button"
class="dialog-close"
@click="closeBatchAiScoreDialog"
>
<el-icon><Close /></el-icon>
</button>
</div>
</template>
<div class="batch-ai-score-content">
<!-- 状态说明 -->
<div v-if="batchAiScoreProgress.status === 'idle'" class="batch-ai-info">
<p class="batch-ai-info__title">准备开始批量AI评分</p>
<p class="batch-ai-info__desc">AI将自动分析已选中学生的作业内容,并生成评分结果。此过程可能需要几分钟,请耐心等待。</p>
<ul class="batch-ai-info__list">
<li>AI会根据任务要求和评分标准进行评估</li>
<li>评分维度包括内容完整性、分析深度、创新性等</li>
<li>评分完成后可在学生列表中查看AI评分</li>
</ul>
</div>
<!-- 进度条和状态 -->
<div v-if="batchAiScoreProgress.status !== 'idle'" class="batch-ai-progress">
<div class="batch-ai-progress__stats">
<div class="batch-ai-progress__stat">
<span class="batch-ai-progress__label">总计</span>
<strong class="batch-ai-progress__value">{{ batchAiScoreProgress.total }}</strong>
</div>
<div class="batch-ai-progress__stat batch-ai-progress__stat--success">
<span class="batch-ai-progress__label">成功</span>
<strong class="batch-ai-progress__value">{{ batchAiScoreProgress.successCount }}</strong>
</div>
<div class="batch-ai-progress__stat batch-ai-progress__stat--fail">
<span class="batch-ai-progress__label">失败</span>
<strong class="batch-ai-progress__value">{{ batchAiScoreProgress.failCount }}</strong>
</div>
</div>
<div class="batch-ai-progress__bar-wrapper">
<el-progress
:percentage="Math.floor((batchAiScoreProgress.current / batchAiScoreProgress.total) * 100)"
:status="batchAiScoreProgress.status === 'completed' ? 'success' : batchAiScoreProgress.status === 'failed' ? 'exception' : undefined"
:stroke-width="12"
/>
<p class="batch-ai-progress__text">
{{ batchAiScoreProgress.current }} / {{ batchAiScoreProgress.total }}
</p>
</div>
<p v-if="batchAiScoreProgress.status === 'running'" class="batch-ai-progress__tip">
<el-icon class="is-loading"><Loading /></el-icon>
正在进行AI评分,请稍候...
</p>
<p v-if="batchAiScoreProgress.status === 'completed'" class="batch-ai-progress__tip batch-ai-progress__tip--success">
<el-icon><CircleCheck /></el-icon>
批量AI评分已完成!
</p>
<p v-if="batchAiScoreProgress.status === 'failed'" class="batch-ai-progress__tip batch-ai-progress__tip--error">
AI评分过程中出现错误,请重试
</p>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<button
v-if="batchAiScoreProgress.status === 'idle'"
type="button"
class="dialog-btn dialog-btn--cancel"
@click="batchAiScoreDialogVisible = false"
>
取消
</button>
<button
v-if="batchAiScoreProgress.status === 'idle'"
type="button"
class="dialog-btn dialog-btn--primary"
@click="startBatchAiScore"
>
<el-icon><TrendCharts /></el-icon>
开始AI评分
</button>
<button
v-if="batchAiScoreProgress.status === 'completed' || batchAiScoreProgress.status === 'failed'"
type="button"
class="dialog-btn dialog-btn--primary"
@click="closeBatchAiScoreDialog"
>
关闭
</button>
</div>
</template>
</el-dialog>
<el-dialog
v-model="aiAssistEvalDialogVisible"
width="420px"
append-to-body
:show-close="false"
:close-on-click-modal="false"
:close-on-press-escape="false"
class="score-review-dialog ai-assist-eval-dialog"
>
<template #header>
<div class="dialog-header">
<div class="dialog-header-main">
<span class="dialog-badge">
<el-icon><TrendCharts /></el-icon>
AI助评
</span>
<h3 class="dialog-title">{{ selectedStudent?.name || selectedStudent?.username }}</h3>
</div>
</div>
</template>
<div class="ai-assist-eval-content">
<div class="ai-assist-eval-loading">
<el-icon class="ai-assist-eval-loading__icon"><Loading /></el-icon>
<p class="ai-assist-eval-loading__text">AI正在分析学生作业...</p>
<p class="ai-assist-eval-loading__desc">这可能需要15-30秒,请稍候</p>
</div>
</div>
</el-dialog>
<el-dialog <el-dialog
v-model="aiScoreDetailDialogVisible" v-model="aiScoreDetailDialogVisible"
width="720px" width="720px"
@ -282,6 +426,17 @@ const aiScoreDetailData = ref({
dimensions: [], dimensions: [],
improvements: [] improvements: []
}); });
const batchAiScoreDialogVisible = ref(false);
const batchAiScoring = ref(false);
const batchAiScoreProgress = ref({
total: 0,
current: 0,
successCount: 0,
failCount: 0,
status: 'idle' // 'idle' | 'running' | 'completed' | 'failed'
});
const aiAssistEvalDialogVisible = ref(false);
const aiAssistEvaluating = ref(false);
const canManageClass = computed(() => { const canManageClass = computed(() => {
const currentClass = teachingClasses.value.find((item) => item.schoolClassId === filters.value.classId); const currentClass = teachingClasses.value.find((item) => item.schoolClassId === filters.value.classId);
@ -486,25 +641,128 @@ onBeforeUnmount(() => {
}); });
// ========== 新增方法 ========== // ========== 新增方法 ==========
function onBatchAiScore() {
if (selectedStudents.value.length === 0) {
ElMessage.warning("请先选择学生");
return;
}
ElMessage.info("批量AI评分功能开发中...");
// 后续接入 AI 评分接口
}
function onViewReferenceAnswer() { function onViewReferenceAnswer() {
if (!selectedStudent.value) return; if (!selectedStudent.value) return;
ElMessage.info("查看参考答案功能开发中..."); ElMessage.info("查看参考答案功能开发中...");
// 后续弹窗展示参考答案 // 后续弹窗展示参考答案
} }
function onAiAssistEvaluation() { function onBatchAiScore() {
if (!selectedStudents.value || selectedStudents.value.length === 0) return;
batchAiScoreDialogVisible.value = true;
batchAiScoreProgress.value = {
total: selectedStudents.value.length,
current: 0,
successCount: 0,
failCount: 0,
status: 'idle'
};
}
async function startBatchAiScore() {
batchAiScoring.value = true;
batchAiScoreProgress.value.status = 'running';
batchAiScoreProgress.value.current = 0;
batchAiScoreProgress.value.successCount = 0;
batchAiScoreProgress.value.failCount = 0;
try {
for (const student of selectedStudents.value) {
try {
// 模拟 AI 评分过程,后续替换为真实 API
await new Promise(resolve => setTimeout(resolve, 800 + Math.random() * 400));
// 调用真实 API: await teacherApi.aiScoreStudent({ ... });
batchAiScoreProgress.value.successCount++;
} catch (error) {
batchAiScoreProgress.value.failCount++;
}
batchAiScoreProgress.value.current++;
}
batchAiScoreProgress.value.status = 'completed';
ElMessage.success(
`AI评分完成!成功${batchAiScoreProgress.value.successCount}名学生,失败${batchAiScoreProgress.value.failCount}名`
);
// 延迟关闭对话框
setTimeout(() => {
batchAiScoreDialogVisible.value = false;
loadReview();
}, 2000);
} catch (error) {
batchAiScoreProgress.value.status = 'failed';
ElMessage.error('批量AI评分发生错误');
} finally {
batchAiScoring.value = false;
}
}
function closeBatchAiScoreDialog() {
if (batchAiScoreProgress.value.status === 'running') {
ElMessage.warning('正在进行中,请等待...');
return;
}
batchAiScoreDialogVisible.value = false;
}
async function onAiAssistEvaluation() {
if (!selectedStudent.value) return; if (!selectedStudent.value) return;
ElMessage.info("AI助评功能开发中...");
// 后续调用 AI 助评接口并展示结果 aiAssistEvalDialogVisible.value = true;
aiAssistEvaluating.value = true;
try {
// 模拟 AI 助评过程,后续替换为真实 API
await new Promise(resolve => setTimeout(resolve, 2000));
// 调用真实 API: const response = await teacherApi.aiAssistEvaluation({ ... });
// 模拟生成 AI 评分详情
aiScoreDetailData.value = {
totalScore: Math.floor(75 + Math.random() * 25),
totalFullScore: 100,
comment: "学生整体完成质量较好,在用户角度分析和市场反馈分析方面表现出色,技术与合规角度分析基本符合要求,结论总结分析有待提升。",
dimensions: [
{
name: "用户角度分析",
score: 25,
fullScore: 25,
analysis: "学生全面分析了产品的用户痛点、需求洞察和用户体验优化方向,能够从用户视角出发提出有价值的改进建议,符合用户为中心的产品设计思维。"
},
{
name: "技术与合规角度分析",
score: 23,
fullScore: 25,
analysis: "对产品的技术可行性和合规性进行了基本分析,识别了主要技术风险点,但在法律法规适配性方面的论述深度不够,建议补充更详细的合规性评估。"
},
{
name: "商业化及市场反馈角度分析",
score: 25,
fullScore: 25,
analysis: "学生准确把握了市场定位和商业模式,对竞争环境有清晰认知,提出的市场推广策略具有可操作性,商业价值分析完整。"
},
{
name: "结论总结分析",
score: 19,
fullScore: 25,
analysis: "总结部分提炼了主要观点,但逻辑性和结构性有待加强,建议按照问题-分析-结论的框架重新组织,增加可操作性建议。"
}
],
improvements: [
"补充技术合规性评估的详细内容,包括数据安全、隐私保护等方面的法规适配性分析",
"优化结论总结的逻辑结构,建议采用MECE原则(相互独立、完全穷尽)进行归纳",
"在商业化分析中增加财务可行性评估,包括成本收益分析和ROI预测"
]
};
aiAssistEvalDialogVisible.value = false;
aiScoreDetailDialogVisible.value = true;
} catch (error) {
ElMessage.error('AI助评失败,请重试');
} finally {
aiAssistEvaluating.value = false;
}
} }
async function showAiScoreDetail() { async function showAiScoreDetail() {
@ -1270,6 +1528,200 @@ async function showAiScoreDetail() {
} }
} }
/* ============================================================
* 批量AI评分对话框
* ============================================================ */
.batch-ai-score-content {
display: grid;
gap: 24px;
}
.batch-ai-info {
display: grid;
gap: 12px;
}
.batch-ai-info__title {
margin: 0;
color: #1f2937;
font-size: 15px;
font-weight: 600;
}
.batch-ai-info__desc {
margin: 0;
color: #6b7280;
font-size: 13px;
line-height: 1.6;
}
.batch-ai-info__list {
margin: 4px 0 0;
padding-left: 20px;
list-style: disc;
li {
margin-bottom: 6px;
color: #6b7280;
font-size: 12px;
line-height: 1.5;
&:last-child {
margin-bottom: 0;
}
}
}
.batch-ai-progress {
display: grid;
gap: 20px;
}
.batch-ai-progress__stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.batch-ai-progress__stat {
padding: 12px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #fafbfc;
text-align: center;
&.batch-ai-progress__stat--success {
background: #f0fdf4;
border-color: #dcfce7;
}
&.batch-ai-progress__stat--fail {
background: #fef2f2;
border-color: #fee2e2;
}
}
.batch-ai-progress__label {
display: block;
color: #9ca3af;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
margin-bottom: 4px;
}
.batch-ai-progress__value {
display: block;
color: #1f2937;
font-size: 20px;
font-weight: 700;
font-family: ui-monospace, "SF Mono", Consolas, monospace;
.batch-ai-progress__stat--success & {
color: #16a34a;
}
.batch-ai-progress__stat--fail & {
color: #dc2626;
}
}
.batch-ai-progress__bar-wrapper {
position: relative;
padding: 12px 0 8px;
:deep(.el-progress__bar) {
border-radius: 6px;
}
}
.batch-ai-progress__text {
margin: 0;
color: #6b7280;
font-size: 12px;
text-align: center;
font-weight: 500;
}
.batch-ai-progress__tip {
margin: 0;
padding: 12px;
border-radius: 8px;
background: #f3f4f6;
color: #6b7280;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
border: 1px solid #e5e7eb;
.el-icon {
font-size: 14px;
&.is-loading {
animation: rotating 2s linear infinite;
}
}
&.batch-ai-progress__tip--success {
background: #f0fdf4;
border-color: #dcfce7;
color: #16a34a;
}
&.batch-ai-progress__tip--error {
background: #fef2f2;
border-color: #fee2e2;
color: #dc2626;
}
}
@keyframes rotating {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* ============================================================
* AI助评加载对话框
* ============================================================ */
.ai-assist-eval-content {
min-height: 160px;
display: flex;
align-items: center;
justify-content: center;
}
.ai-assist-eval-loading {
text-align: center;
display: grid;
gap: 12px;
}
.ai-assist-eval-loading__icon {
font-size: 48px;
color: #3b82f6;
animation: rotating 2s linear infinite;
margin: 0 auto;
}
.ai-assist-eval-loading__text {
margin: 0;
color: #1f2937;
font-size: 14px;
font-weight: 600;
}
.ai-assist-eval-loading__desc {
margin: 0;
color: #9ca3af;
font-size: 12px;
}
@media (max-width: 1180px) { @media (max-width: 1180px) {
.task-review { .task-review {
grid-template-columns: 1fr; grid-template-columns: 1fr;

Loading…
Cancel
Save