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/taskReview/StudentTaskReadonlyPreview.vue

109 lines
4.0 KiB
Vue

<template>
<main ref="previewRoot" class="student-task-readonly-preview" @click.capture="guardPreviewInteraction" @keydown.capture="guardPreviewInteraction">
<component v-if="previewComponent" :is="previewComponent" v-bind="previewProps" />
<el-empty v-else :description="emptyDescription" :image-size="112" />
</main>
</template>
<script setup>
import { getTrainingReviewPreviewContext } from "@/utils/trainingReviewPreview";
import { studentTaskPreviewComponentByKey, studentTaskPreviewPropsByKey } from "./studentTaskPreviewMap";
const route = useRoute();
const previewComponent = shallowRef(null);
const previewProps = 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() {
previewComponent.value = null;
const taskKey = String(route.query.taskKey || "");
const preview = getTrainingReviewPreviewContext();
const loader = studentTaskPreviewComponentByKey[taskKey];
if (!preview || preview.taskKey !== taskKey) {
emptyDescription.value = "学生作答预览已失效,请返回任务评阅重新选择学生";
return;
}
if (!loader) {
emptyDescription.value = "该实训任务暂不支持页面预览";
return;
}
try {
previewComponent.value = (await loader()).default;
previewProps.value = studentTaskPreviewPropsByKey[taskKey]?.(taskKey) || {};
await nextTick();
schedulePreviewHeight();
} catch (_) {
emptyDescription.value = "学生任务页面加载失败,请刷新后重试";
}
}
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) {
const target = event.target instanceof Element ? event.target : null;
if (!target) return;
if (target.closest("input, textarea, select, [contenteditable='true'], .el-input, .el-textarea, .el-select, .el-checkbox, .el-radio, .el-switch")) {
event.preventDefault();
event.stopImmediatePropagation();
return;
}
const action = target.closest("button, a, [role='button'], [role='tab'], .el-step");
if (action && !isStepNavigation(action)) {
event.preventDefault();
event.stopImmediatePropagation();
}
}
function isStepNavigation(element) {
if (element.matches("[role='tab'], .el-step")) return true;
const label = String(element.getAttribute("aria-label") || "");
const text = `${label} ${String(element.textContent || "")}`.replace(/\s+/g, " ").trim();
return /(上一步|下一步|第\s*\d+\s*步|切换.*步骤|步骤.*切换)/.test(text);
}
</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>