refactor(task-review-panel): update student table data handling and enhance pagination

- Changed the data source for the student table from `review.students` to `paginatedStudents` for improved performance and clarity.
- Added pagination controls to the task review panel to manage large student lists more effectively.
- Streamlined dialog component structures for better readability and maintenance.
- Minor adjustments to button and dialog layouts for improved user experience.
dev-QQq
qinzhenpen 7 hours ago
parent cb1d33da5a
commit c3a33335a0

@ -26,7 +26,7 @@
<el-button type="primary" size="small" :disabled="!canBatchScore" @click="batchDialogVisible = true">批量打分</el-button> <el-button type="primary" size="small" :disabled="!canBatchScore" @click="batchDialogVisible = true">批量打分</el-button>
</div> </div>
</div> </div>
<el-table ref="studentTableRef" class="task-review__student-table" :data="review.students" border size="small" row-key="studentUserId" highlight-current-row :row-class-name="studentRowClass" @selection-change="onSelectionChange" @row-click="selectStudent"> <el-table ref="studentTableRef" class="task-review__student-table" :data="paginatedStudents" border size="small" row-key="studentUserId" highlight-current-row :row-class-name="studentRowClass" @selection-change="onSelectionChange" @row-click="selectStudent">
<el-table-column type="selection" width="42" :selectable="canSelectStudent" /> <el-table-column type="selection" width="42" :selectable="canSelectStudent" />
<el-table-column prop="name" label="姓名" width="86" show-overflow-tooltip /> <el-table-column prop="name" label="姓名" width="86" show-overflow-tooltip />
<el-table-column prop="username" label="学号" width="102" show-overflow-tooltip /> <el-table-column prop="username" label="学号" width="102" show-overflow-tooltip />
@ -41,6 +41,7 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<el-pagination v-if="review.students.length > pagination.pageSize" v-model:current-page="pagination.currentPage" v-model:page-size="pagination.pageSize" :page-sizes="[10, 20, 50, 100]" :total="review.students.length" layout="total, sizes, prev, pager, next, jumper" class="task-review__pagination" small background @current-change="onPageChange" @size-change="onPageSizeChange" />
<p v-if="!canManageClass && filters.classId" class="task-review__readonly-tip">仅班级创建教师可评分</p> <p v-if="!canManageClass && filters.classId" class="task-review__readonly-tip">仅班级创建教师可评分</p>
</aside> </aside>
@ -168,15 +169,7 @@
</template> </template>
</el-dialog> </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">
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> <template #header>
<div class="dialog-header"> <div class="dialog-header">
<div class="dialog-header-main"> <div class="dialog-header-main">
@ -186,12 +179,7 @@
</span> </span>
<h3 class="dialog-title">已选择 {{ selectedStudents.length }} 名学生</h3> <h3 class="dialog-title">已选择 {{ selectedStudents.length }} 名学生</h3>
</div> </div>
<button <button v-if="batchAiScoreProgress.status !== 'running'" type="button" class="dialog-close" @click="closeBatchAiScoreDialog">
v-if="batchAiScoreProgress.status !== 'running'"
type="button"
class="dialog-close"
@click="closeBatchAiScoreDialog"
>
<el-icon><Close /></el-icon> <el-icon><Close /></el-icon>
</button> </button>
</div> </div>
@ -227,14 +215,8 @@
</div> </div>
<div class="batch-ai-progress__bar-wrapper"> <div class="batch-ai-progress__bar-wrapper">
<el-progress <el-progress :percentage="Math.floor((batchAiScoreProgress.current / batchAiScoreProgress.total) * 100)" :status="batchAiScoreProgress.status === 'completed' ? 'success' : batchAiScoreProgress.status === 'failed' ? 'exception' : undefined" :stroke-width="12" />
:percentage="Math.floor((batchAiScoreProgress.current / batchAiScoreProgress.total) * 100)" <p class="batch-ai-progress__text">{{ batchAiScoreProgress.current }} / {{ batchAiScoreProgress.total }}</p>
: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> </div>
<p v-if="batchAiScoreProgress.status === 'running'" class="batch-ai-progress__tip"> <p v-if="batchAiScoreProgress.status === 'running'" class="batch-ai-progress__tip">
@ -245,52 +227,23 @@
<el-icon><CircleCheck /></el-icon> <el-icon><CircleCheck /></el-icon>
批量AI评分已完成! 批量AI评分已完成!
</p> </p>
<p v-if="batchAiScoreProgress.status === 'failed'" class="batch-ai-progress__tip batch-ai-progress__tip--error"> <p v-if="batchAiScoreProgress.status === 'failed'" class="batch-ai-progress__tip batch-ai-progress__tip--error">AI评分过程中出现错误,请重试</p>
AI评分过程中出现错误,请重试
</p>
</div> </div>
</div> </div>
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<button <button v-if="batchAiScoreProgress.status === 'idle'" type="button" class="dialog-btn dialog-btn--cancel" @click="batchAiScoreDialogVisible = false">取消</button>
v-if="batchAiScoreProgress.status === 'idle'" <button v-if="batchAiScoreProgress.status === 'idle'" type="button" class="dialog-btn dialog-btn--primary" @click="startBatchAiScore">
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> <el-icon><TrendCharts /></el-icon>
开始AI评分 开始AI评分
</button> </button>
<button <button v-if="batchAiScoreProgress.status === 'completed' || batchAiScoreProgress.status === 'failed'" type="button" class="dialog-btn dialog-btn--primary" @click="closeBatchAiScoreDialog">关闭</button>
v-if="batchAiScoreProgress.status === 'completed' || batchAiScoreProgress.status === 'failed'"
type="button"
class="dialog-btn dialog-btn--primary"
@click="closeBatchAiScoreDialog"
>
关闭
</button>
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
<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">
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> <template #header>
<div class="dialog-header"> <div class="dialog-header">
<div class="dialog-header-main"> <div class="dialog-header-main">
@ -312,13 +265,7 @@
</div> </div>
</el-dialog> </el-dialog>
<el-dialog <el-dialog v-model="aiScoreDetailDialogVisible" width="720px" append-to-body :show-close="false" class="ai-score-detail-dialog">
v-model="aiScoreDetailDialogVisible"
width="720px"
append-to-body
:show-close="false"
class="ai-score-detail-dialog"
>
<template #header> <template #header>
<div class="dialog-header"> <div class="dialog-header">
<div class="dialog-header-main"> <div class="dialog-header-main">
@ -340,7 +287,7 @@
<div class="ai-score-summary__main"> <div class="ai-score-summary__main">
<span class="ai-score-summary__label">综合评分</span> <span class="ai-score-summary__label">综合评分</span>
<div class="ai-score-summary__score"> <div class="ai-score-summary__score">
<span class="ai-score-summary__number">{{ aiScoreDetailData.totalScore ?? '--' }}</span> <span class="ai-score-summary__number">{{ aiScoreDetailData.totalScore ?? "--" }}</span>
<span class="ai-score-summary__total">/{{ aiScoreDetailData.totalFullScore }}</span> <span class="ai-score-summary__total">/{{ aiScoreDetailData.totalFullScore }}</span>
</div> </div>
</div> </div>
@ -349,15 +296,11 @@
<!-- 各维度评分 --> <!-- 各维度评分 -->
<div v-if="aiScoreDetailData.dimensions.length > 0" class="ai-score-dimensions"> <div v-if="aiScoreDetailData.dimensions.length > 0" class="ai-score-dimensions">
<div <div v-for="(dimension, index) in aiScoreDetailData.dimensions" :key="index" class="ai-score-dimension">
v-for="(dimension, index) in aiScoreDetailData.dimensions"
:key="index"
class="ai-score-dimension"
>
<div class="ai-score-dimension__head"> <div class="ai-score-dimension__head">
<h4 class="ai-score-dimension__title">{{ dimension.name }}</h4> <h4 class="ai-score-dimension__title">{{ dimension.name }}</h4>
<span class="ai-score-dimension__score"> <span class="ai-score-dimension__score">
<strong>{{ dimension.score ?? '--' }}</strong> <strong>{{ dimension.score ?? "--" }}</strong>
<span>/{{ dimension.fullScore }}</span> <span>/{{ dimension.fullScore }}</span>
</span> </span>
</div> </div>
@ -424,7 +367,7 @@ const aiScoreDetailData = ref({
totalFullScore: 100, totalFullScore: 100,
comment: "", comment: "",
dimensions: [], dimensions: [],
improvements: [] improvements: [],
}); });
const batchAiScoreDialogVisible = ref(false); const batchAiScoreDialogVisible = ref(false);
const batchAiScoring = ref(false); const batchAiScoring = ref(false);
@ -433,10 +376,14 @@ const batchAiScoreProgress = ref({
current: 0, current: 0,
successCount: 0, successCount: 0,
failCount: 0, failCount: 0,
status: 'idle' // 'idle' | 'running' | 'completed' | 'failed' status: "idle", // 'idle' | 'running' | 'completed' | 'failed'
}); });
const aiAssistEvalDialogVisible = ref(false); const aiAssistEvalDialogVisible = ref(false);
const aiAssistEvaluating = ref(false); const aiAssistEvaluating = ref(false);
const pagination = ref({
currentPage: 1,
pageSize: 10,
});
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);
@ -444,6 +391,12 @@ const canManageClass = computed(() => {
}); });
const selectedStudent = computed(() => review.value.students.find((row) => row.studentUserId === selectedStudentId.value) || review.value.students[0] || null); const selectedStudent = computed(() => review.value.students.find((row) => row.studentUserId === selectedStudentId.value) || review.value.students[0] || null);
const canBatchScore = computed(() => canManageClass.value && selectedStudents.value.length > 0 && Boolean(filters.value.taskId)); const canBatchScore = computed(() => canManageClass.value && selectedStudents.value.length > 0 && Boolean(filters.value.taskId));
const paginatedStudents = computed(() => {
const students = review.value.students;
const start = (pagination.value.currentPage - 1) * pagination.value.pageSize;
const end = start + pagination.value.pageSize;
return students.slice(start, end);
});
watch( watch(
teachingClasses, teachingClasses,
@ -505,6 +458,8 @@ function normalizeReview(data) {
if (!students.some((row) => row.studentUserId === selectedStudentId.value)) selectedStudentId.value = students[0]?.studentUserId || ""; if (!students.some((row) => row.studentUserId === selectedStudentId.value)) selectedStudentId.value = students[0]?.studentUserId || "";
selectedStudents.value = []; selectedStudents.value = [];
studentTableRef.value?.clearSelection(); studentTableRef.value?.clearSelection();
// 重置分页到第一页
pagination.value.currentPage = 1;
} }
async function loadReview() { async function loadReview() {
if (!filters.value.classId) return; if (!filters.value.classId) return;
@ -565,6 +520,13 @@ function canSelectStudent(row) {
function selectStudent(row) { function selectStudent(row) {
selectedStudentId.value = row.studentUserId; selectedStudentId.value = row.studentUserId;
} }
function onPageChange(page) {
pagination.value.currentPage = page;
}
function onPageSizeChange(pageSize) {
pagination.value.pageSize = pageSize;
pagination.value.currentPage = 1;
}
function studentRowClass({ row }) { function studentRowClass({ row }) {
return row.studentUserId === selectedStudentId.value ? "task-review__selected-row" : ""; return row.studentUserId === selectedStudentId.value ? "task-review__selected-row" : "";
} }
@ -655,13 +617,13 @@ function onBatchAiScore() {
current: 0, current: 0,
successCount: 0, successCount: 0,
failCount: 0, failCount: 0,
status: 'idle' status: "idle",
}; };
} }
async function startBatchAiScore() { async function startBatchAiScore() {
batchAiScoring.value = true; batchAiScoring.value = true;
batchAiScoreProgress.value.status = 'running'; batchAiScoreProgress.value.status = "running";
batchAiScoreProgress.value.current = 0; batchAiScoreProgress.value.current = 0;
batchAiScoreProgress.value.successCount = 0; batchAiScoreProgress.value.successCount = 0;
batchAiScoreProgress.value.failCount = 0; batchAiScoreProgress.value.failCount = 0;
@ -670,7 +632,7 @@ async function startBatchAiScore() {
for (const student of selectedStudents.value) { for (const student of selectedStudents.value) {
try { try {
// 模拟 AI 评分过程,后续替换为真实 API // 模拟 AI 评分过程,后续替换为真实 API
await new Promise(resolve => setTimeout(resolve, 800 + Math.random() * 400)); await new Promise((resolve) => setTimeout(resolve, 800 + Math.random() * 400));
// 调用真实 API: await teacherApi.aiScoreStudent({ ... }); // 调用真实 API: await teacherApi.aiScoreStudent({ ... });
batchAiScoreProgress.value.successCount++; batchAiScoreProgress.value.successCount++;
@ -680,10 +642,8 @@ async function startBatchAiScore() {
batchAiScoreProgress.value.current++; batchAiScoreProgress.value.current++;
} }
batchAiScoreProgress.value.status = 'completed'; batchAiScoreProgress.value.status = "completed";
ElMessage.success( ElMessage.success(`AI评分完成!成功${batchAiScoreProgress.value.successCount}名学生,失败${batchAiScoreProgress.value.failCount}名`);
`AI评分完成!成功${batchAiScoreProgress.value.successCount}名学生,失败${batchAiScoreProgress.value.failCount}名`
);
// 延迟关闭对话框 // 延迟关闭对话框
setTimeout(() => { setTimeout(() => {
@ -691,16 +651,16 @@ async function startBatchAiScore() {
loadReview(); loadReview();
}, 2000); }, 2000);
} catch (error) { } catch (error) {
batchAiScoreProgress.value.status = 'failed'; batchAiScoreProgress.value.status = "failed";
ElMessage.error('批量AI评分发生错误'); ElMessage.error("批量AI评分发生错误");
} finally { } finally {
batchAiScoring.value = false; batchAiScoring.value = false;
} }
} }
function closeBatchAiScoreDialog() { function closeBatchAiScoreDialog() {
if (batchAiScoreProgress.value.status === 'running') { if (batchAiScoreProgress.value.status === "running") {
ElMessage.warning('正在进行中,请等待...'); ElMessage.warning("正在进行中,请等待...");
return; return;
} }
batchAiScoreDialogVisible.value = false; batchAiScoreDialogVisible.value = false;
@ -714,7 +674,7 @@ async function onAiAssistEvaluation() {
try { try {
// 模拟 AI 助评过程,后续替换为真实 API // 模拟 AI 助评过程,后续替换为真实 API
await new Promise(resolve => setTimeout(resolve, 2000)); await new Promise((resolve) => setTimeout(resolve, 2000));
// 调用真实 API: const response = await teacherApi.aiAssistEvaluation({ ... }); // 调用真实 API: const response = await teacherApi.aiAssistEvaluation({ ... });
@ -728,38 +688,34 @@ async function onAiAssistEvaluation() {
name: "用户角度分析", name: "用户角度分析",
score: 25, score: 25,
fullScore: 25, fullScore: 25,
analysis: "学生全面分析了产品的用户痛点、需求洞察和用户体验优化方向,能够从用户视角出发提出有价值的改进建议,符合用户为中心的产品设计思维。" analysis: "学生全面分析了产品的用户痛点、需求洞察和用户体验优化方向,能够从用户视角出发提出有价值的改进建议,符合用户为中心的产品设计思维。",
}, },
{ {
name: "技术与合规角度分析", name: "技术与合规角度分析",
score: 23, score: 23,
fullScore: 25, fullScore: 25,
analysis: "对产品的技术可行性和合规性进行了基本分析,识别了主要技术风险点,但在法律法规适配性方面的论述深度不够,建议补充更详细的合规性评估。" analysis: "对产品的技术可行性和合规性进行了基本分析,识别了主要技术风险点,但在法律法规适配性方面的论述深度不够,建议补充更详细的合规性评估。",
}, },
{ {
name: "商业化及市场反馈角度分析", name: "商业化及市场反馈角度分析",
score: 25, score: 25,
fullScore: 25, fullScore: 25,
analysis: "学生准确把握了市场定位和商业模式,对竞争环境有清晰认知,提出的市场推广策略具有可操作性,商业价值分析完整。" analysis: "学生准确把握了市场定位和商业模式,对竞争环境有清晰认知,提出的市场推广策略具有可操作性,商业价值分析完整。",
}, },
{ {
name: "结论总结分析", name: "结论总结分析",
score: 19, score: 19,
fullScore: 25, fullScore: 25,
analysis: "总结部分提炼了主要观点,但逻辑性和结构性有待加强,建议按照问题-分析-结论的框架重新组织,增加可操作性建议。" analysis: "总结部分提炼了主要观点,但逻辑性和结构性有待加强,建议按照问题-分析-结论的框架重新组织,增加可操作性建议。",
} },
], ],
improvements: [ improvements: ["补充技术合规性评估的详细内容,包括数据安全、隐私保护等方面的法规适配性分析", "优化结论总结的逻辑结构,建议采用MECE原则(相互独立、完全穷尽)进行归纳", "在商业化分析中增加财务可行性评估,包括成本收益分析和ROI预测"],
"补充技术合规性评估的详细内容,包括数据安全、隐私保护等方面的法规适配性分析",
"优化结论总结的逻辑结构,建议采用MECE原则(相互独立、完全穷尽)进行归纳",
"在商业化分析中增加财务可行性评估,包括成本收益分析和ROI预测"
]
}; };
aiAssistEvalDialogVisible.value = false; aiAssistEvalDialogVisible.value = false;
aiScoreDetailDialogVisible.value = true; aiScoreDetailDialogVisible.value = true;
} catch (error) { } catch (error) {
ElMessage.error('AI助评失败,请重试'); ElMessage.error("AI助评失败,请重试");
} finally { } finally {
aiAssistEvaluating.value = false; aiAssistEvaluating.value = false;
} }
@ -778,32 +734,28 @@ async function showAiScoreDetail() {
name: "用户角度分析", name: "用户角度分析",
score: 25, score: 25,
fullScore: 25, fullScore: 25,
analysis: "学生全面分析了产品的用户痛点、需求洞察和用户体验优化方向,能够从用户视角出发提出有价值的改进建议,符合用户为中心的产品设计思维。" analysis: "学生全面分析了产品的用户痛点、需求洞察和用户体验优化方向,能够从用户视角出发提出有价值的改进建议,符合用户为中心的产品设计思维。",
}, },
{ {
name: "技术与合规角度分析", name: "技术与合规角度分析",
score: 23, score: 23,
fullScore: 25, fullScore: 25,
analysis: "对产品的技术可行性和合规性进行了基本分析,识别了主要技术风险点,但在法律法规适配性方面的论述深度不够,建议补充更详细的合规性评估。" analysis: "对产品的技术可行性和合规性进行了基本分析,识别了主要技术风险点,但在法律法规适配性方面的论述深度不够,建议补充更详细的合规性评估。",
}, },
{ {
name: "商业化及市场反馈角度分析", name: "商业化及市场反馈角度分析",
score: 25, score: 25,
fullScore: 25, fullScore: 25,
analysis: "学生准确把握了市场定位和商业模式,对竞争环境有清晰认知,提出的市场推广策略具有可操作性,商业价值分析完整。" analysis: "学生准确把握了市场定位和商业模式,对竞争环境有清晰认知,提出的市场推广策略具有可操作性,商业价值分析完整。",
}, },
{ {
name: "结论总结分析", name: "结论总结分析",
score: 19, score: 19,
fullScore: 25, fullScore: 25,
analysis: "总结部分提炼了主要观点,但逻辑性和结构性有待加强,建议按照问题-分析-结论的框架重新组织,增加可操作性建议。" analysis: "总结部分提炼了主要观点,但逻辑性和结构性有待加强,建议按照问题-分析-结论的框架重新组织,增加可操作性建议。",
} },
], ],
improvements: [ improvements: ["补充技术合规性评估的详细内容,包括数据安全、隐私保护等方面的法规适配性分析", "优化结论总结的逻辑结构,建议采用MECE原则(相互独立、完全穷尽)进行归纳", "在商业化分析中增加财务可行性评估,包括成本收益分析和ROI预测"],
"补充技术合规性评估的详细内容,包括数据安全、隐私保护等方面的法规适配性分析",
"优化结论总结的逻辑结构,建议采用MECE原则(相互独立、完全穷尽)进行归纳",
"在商业化分析中增加财务可行性评估,包括成本收益分析和ROI预测"
]
}; };
aiScoreDetailDialogVisible.value = true; aiScoreDetailDialogVisible.value = true;
@ -923,6 +875,16 @@ async function showAiScoreDetail() {
font-size: var(--fs-body); font-size: var(--fs-body);
} }
.task-review__pagination {
margin: 12px 0 0;
display: flex;
justify-content: flex-end;
:deep(.el-pagination) {
font-size: var(--fs-body);
}
}
/* ===== 右侧预览区 ===== */ /* ===== 右侧预览区 ===== */
.task-review__content { .task-review__content {
min-width: 0; min-width: 0;

Loading…
Cancel
Save