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 5 hours ago
parent cb1d33da5a
commit c3a33335a0

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

Loading…
Cancel
Save