fix: stabilize task review preview scrolling

bugfix/ai-assessment-evidence-rescore
陈沅 1 month ago
parent e71e7ffd8e
commit 031a5ccc89

@ -80,6 +80,7 @@
:key="studentPreviewSrc" :key="studentPreviewSrc"
class="task-review__student-preview" class="task-review__student-preview"
:src="studentPreviewSrc" :src="studentPreviewSrc"
:style="{ height: `${previewFrameHeight}px` }"
scrolling="yes" scrolling="yes"
title="学生任务页面只读预览" title="学生任务页面只读预览"
/> />
@ -129,7 +130,9 @@ const studentTableRef = ref();
const previewKey = ref(""); const previewKey = ref("");
const studentPreviewSrc = ref(""); const studentPreviewSrc = ref("");
const studentPreviewError = ref(""); const studentPreviewError = ref("");
const previewFrameHeight = ref(580);
let previewRequestVersion = 0; let previewRequestVersion = 0;
const PREVIEW_HEIGHT_MESSAGE = "teacher-task-review-preview-height";
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);
@ -159,6 +162,7 @@ async function refreshStudentPreview() {
const teachingClassId = filters.value.classId; const teachingClassId = filters.value.classId;
const student = selectedStudent.value; const student = selectedStudent.value;
studentPreviewError.value = ""; studentPreviewError.value = "";
previewFrameHeight.value = 580;
if (!student || !taskKey || !teachingClassId) { if (!student || !taskKey || !teachingClassId) {
previewKey.value = ""; previewKey.value = "";
studentPreviewSrc.value = ""; studentPreviewSrc.value = "";
@ -235,6 +239,13 @@ function selectStudent(row) {
function studentRowClass({ row }) { function studentRowClass({ row }) {
return row.studentUserId === selectedStudentId.value ? "task-review__selected-row" : ""; return row.studentUserId === selectedStudentId.value ? "task-review__selected-row" : "";
} }
function onPreviewMessage(event) {
if (event.origin !== window.location.origin) return;
const data = event.data || {};
if (data.type !== PREVIEW_HEIGHT_MESSAGE || data.previewKey !== previewKey.value) return;
const height = Number(data.height);
if (Number.isFinite(height) && height > 0) previewFrameHeight.value = Math.max(580, Math.ceil(height));
}
async function saveSingleScore(row) { async function saveSingleScore(row) {
const score = row.teacherScoreDraft; const score = row.teacherScoreDraft;
if (!Number.isInteger(score) || score < 0 || score > 100) { if (!Number.isInteger(score) || score < 0 || score > 100) {
@ -270,8 +281,14 @@ async function saveBatchScore() {
savingBatch.value = false; savingBatch.value = false;
} }
} }
onMounted(loadTeachingClasses); onMounted(() => {
onBeforeUnmount(() => clearTrainingReviewPreview(previewKey.value)); window.addEventListener("message", onPreviewMessage);
loadTeachingClasses();
});
onBeforeUnmount(() => {
window.removeEventListener("message", onPreviewMessage);
clearTrainingReviewPreview(previewKey.value);
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -293,8 +310,11 @@ onBeforeUnmount(() => clearTrainingReviewPreview(previewKey.value));
.task-review__scores { display: flex; gap: 10px; color: #b9d9eb; font-size: 13px; white-space: nowrap; } .task-review__scores { display: flex; gap: 10px; color: #b9d9eb; font-size: 13px; white-space: nowrap; }
.task-review__scores span { padding: 7px 10px; border: 1px solid rgba(50, 166, 223, 0.28); border-radius: 999px; background: rgba(2, 26, 43, 0.68); } .task-review__scores span { padding: 7px 10px; border: 1px solid rgba(50, 166, 223, 0.28); border-radius: 999px; background: rgba(2, 26, 43, 0.68); }
.task-review__scores strong { margin-left: 5px; color: #77d9ff; font-size: 17px; } .task-review__scores strong { margin-left: 5px; color: #77d9ff; font-size: 17px; }
.task-review__student-preview-scroll { height: calc(100vh - 260px); min-height: 580px; margin-top: 22px; overflow: auto; border: 1px solid rgba(54, 159, 212, 0.32); border-radius: 10px; } .task-review__student-preview-scroll { height: calc(100vh - 260px); min-height: 580px; margin-top: 22px; overflow: scroll; scrollbar-gutter: stable; scrollbar-color: #238db8 #061724; border: 1px solid rgba(54, 159, 212, 0.32); border-radius: 10px; }
.task-review__student-preview { display: block; width: 100%; min-width: 1280px; height: 100%; min-height: 580px; border: 0; } .task-review__student-preview-scroll::-webkit-scrollbar { width: 12px; height: 12px; }
.task-review__student-preview-scroll::-webkit-scrollbar-track { background: #061724; }
.task-review__student-preview-scroll::-webkit-scrollbar-thumb { border: 2px solid #061724; border-radius: 8px; background: #238db8; }
.task-review__student-preview { display: block; width: 100%; min-width: 1280px; min-height: 580px; border: 0; }
.task-review__batch-copy { margin: 0 0 18px; color: #dff6ff; } .task-review__batch-copy { margin: 0 0 18px; color: #dff6ff; }
.task-review__batch-input { width: 100%; } .task-review__batch-input { width: 100%; }
:deep(.task-review__selected-row > td) { background: rgba(0, 111, 177, 0.26) !important; } :deep(.task-review__selected-row > td) { background: rgba(0, 111, 177, 0.26) !important; }

@ -1,5 +1,5 @@
<template> <template>
<main class="student-task-readonly-preview" @click.capture="guardPreviewInteraction" @keydown.capture="guardPreviewInteraction"> <main ref="previewRoot" class="student-task-readonly-preview" @click.capture="guardPreviewInteraction" @keydown.capture="guardPreviewInteraction">
<component v-if="previewComponent" :is="previewComponent" v-bind="previewProps" /> <component v-if="previewComponent" :is="previewComponent" v-bind="previewProps" />
<el-empty v-else :description="emptyDescription" :image-size="112" /> <el-empty v-else :description="emptyDescription" :image-size="112" />
</main> </main>
@ -13,6 +13,11 @@ const route = useRoute();
const previewComponent = shallowRef(null); const previewComponent = shallowRef(null);
const previewProps = ref({}); const previewProps = ref({});
const emptyDescription = ref("正在加载学生任务页面"); const emptyDescription = ref("正在加载学生任务页面");
const previewRoot = ref();
const PREVIEW_HEIGHT_MESSAGE = "teacher-task-review-preview-height";
let resizeObserver;
let mutationObserver;
let animationFrame;
async function loadPreview() { async function loadPreview() {
previewComponent.value = null; previewComponent.value = null;
@ -30,6 +35,8 @@ async function loadPreview() {
try { try {
previewComponent.value = (await loader()).default; previewComponent.value = (await loader()).default;
previewProps.value = studentTaskPreviewPropsByKey[taskKey]?.(taskKey) || {}; previewProps.value = studentTaskPreviewPropsByKey[taskKey]?.(taskKey) || {};
await nextTick();
schedulePreviewHeight();
} catch (_) { } catch (_) {
emptyDescription.value = "学生任务页面加载失败,请刷新后重试"; emptyDescription.value = "学生任务页面加载失败,请刷新后重试";
} }
@ -37,6 +44,40 @@ async function loadPreview() {
watch(() => [route.query.taskKey, route.query.reviewPreviewKey], loadPreview, { immediate: true }); watch(() => [route.query.taskKey, route.query.reviewPreviewKey], loadPreview, { immediate: true });
onMounted(() => {
resizeObserver = new ResizeObserver(schedulePreviewHeight);
if (previewRoot.value) resizeObserver.observe(previewRoot.value);
mutationObserver = new MutationObserver(schedulePreviewHeight);
if (previewRoot.value) mutationObserver.observe(previewRoot.value, { childList: true, subtree: true });
schedulePreviewHeight();
});
onBeforeUnmount(() => {
resizeObserver?.disconnect();
mutationObserver?.disconnect();
if (animationFrame) cancelAnimationFrame(animationFrame);
});
function schedulePreviewHeight() {
if (animationFrame) cancelAnimationFrame(animationFrame);
animationFrame = requestAnimationFrame(reportPreviewHeight);
}
function reportPreviewHeight() {
animationFrame = 0;
const height = Math.ceil(Math.max(
previewRoot.value?.scrollHeight || 0,
document.body?.scrollHeight || 0,
document.documentElement?.scrollHeight || 0,
window.innerHeight || 0,
));
window.parent?.postMessage({
type: PREVIEW_HEIGHT_MESSAGE,
previewKey: String(route.query.reviewPreviewKey || ""),
height,
}, window.location.origin);
}
function guardPreviewInteraction(event) { function guardPreviewInteraction(event) {
const target = event.target instanceof Element ? event.target : null; const target = event.target instanceof Element ? event.target : null;
if (!target) return; if (!target) return;
@ -59,3 +100,9 @@ function isStepNavigation(element) {
return /(上一步|下一步|第\s*\d+\s*步|切换.*步骤|步骤.*切换)/.test(text); return /(上一步|下一步|第\s*\d+\s*步|切换.*步骤|步骤.*切换)/.test(text);
} }
</script> </script>
<style lang="scss" scoped>
.student-task-readonly-preview :deep(.training-ai-sidebar),
.student-task-readonly-preview :deep(.training-ai-evaluation) { display: none !important; }
.student-task-readonly-preview :deep(.student-training-shell) { grid-template-columns: minmax(0, 1fr) !important; }
</style>

@ -45,6 +45,10 @@ assert.match(previewShell, /function guardPreviewInteraction/);
assert.match(previewShell, /getAttribute\("aria-label"\)/); assert.match(previewShell, /getAttribute\("aria-label"\)/);
assert.doesNotMatch(previewShell, /:global\(html\)|:global\(body\)/, "预览壳不得改写学生页面的全局样式"); assert.doesNotMatch(previewShell, /:global\(html\)|:global\(body\)/, "预览壳不得改写学生页面的全局样式");
assert.doesNotMatch(previewShell, /pointer-events\s*:/, "预览壳不得用样式禁用学生页面控件"); assert.doesNotMatch(previewShell, /pointer-events\s*:/, "预览壳不得用样式禁用学生页面控件");
assert.match(previewShell, /training-ai-sidebar/, "评阅预览应隐藏 AI 助学");
assert.match(previewShell, /training-ai-evaluation/, "评阅预览应隐藏 AI 助评");
assert.match(previewShell, /teacher-task-review-preview-height/, "预览页应向外层同步内容高度");
assert.match(previewShell, /ResizeObserver/, "预览页内容变化后应重新同步高度");
assert.match(reviewPanel, /writeTrainingReviewPreview/); assert.match(reviewPanel, /writeTrainingReviewPreview/);
assert.match(reviewPanel, /listClassTrainingTasks\(filters\.value\.classId/); assert.match(reviewPanel, /listClassTrainingTasks\(filters\.value\.classId/);
assert.match(reviewPanel, /find\(\(task\) => task\.taskKey === taskKey\)/); assert.match(reviewPanel, /find\(\(task\) => task\.taskKey === taskKey\)/);
@ -55,6 +59,9 @@ assert.match(reviewPanel, /class="task-review__student-preview-scroll"/);
assert.match(reviewPanel, /<iframe[^>]+studentPreviewSrc/); assert.match(reviewPanel, /<iframe[^>]+studentPreviewSrc/);
assert.match(reviewPanel, /scrolling="yes"/); assert.match(reviewPanel, /scrolling="yes"/);
assert.match(reviewPanel, /min-width:\s*1280px/, "嵌入学生页面应保留桌面端视口,窄区域改为滚动查看"); assert.match(reviewPanel, /min-width:\s*1280px/, "嵌入学生页面应保留桌面端视口,窄区域改为滚动查看");
assert.match(reviewPanel, /previewFrameHeight/, "外层应使用嵌入页同步的真实高度");
assert.match(reviewPanel, /teacher-task-review-preview-height/, "外层应接收预览内容高度");
assert.match(reviewPanel, /overflow:\s*scroll/, "外层预览应始终显示滚动轨道");
assert.match(reviewPanel, /grid-template-columns: minmax\(360px, 400px\) minmax\(0, 1fr\)/); assert.match(reviewPanel, /grid-template-columns: minmax\(360px, 400px\) minmax\(0, 1fr\)/);
assert.match(reviewPanel, /class="task-review__student-table"/); assert.match(reviewPanel, /class="task-review__student-table"/);
assert.match(reviewPanel, /type="selection" width="38"/); assert.match(reviewPanel, /type="selection" width="38"/);

Loading…
Cancel
Save