fix: show loading state while student task config loads

fix/non-blocking-route-navigation
chenyuan 5 days ago
parent 844b2a5c7e
commit 822cb61bc1

@ -4,6 +4,11 @@ import request from "@/utils/request";
// 仅合并进行中的请求:教师保存后下一次读取仍会获得最新配置,不引入陈旧缓存。 // 仅合并进行中的请求:教师保存后下一次读取仍会获得最新配置,不引入陈旧缓存。
const pendingTaskRequests = new Map(); const pendingTaskRequests = new Map();
function notifyStudentTaskConfigLoading(taskKey, loading) {
if (typeof window === "undefined") return;
window.dispatchEvent(new CustomEvent("student-task-config-loading", { detail: { taskKey, loading } }));
}
export function listTrainingTasks(params) { export function listTrainingTasks(params) {
return request({ return request({
url: "/api/training-tasks", url: "/api/training-tasks",
@ -17,14 +22,21 @@ export function getTrainingTaskByKey(taskKey) {
if (pendingTaskRequests.has(normalizedTaskKey)) { if (pendingTaskRequests.has(normalizedTaskKey)) {
return pendingTaskRequests.get(normalizedTaskKey); return pendingTaskRequests.get(normalizedTaskKey);
} }
notifyStudentTaskConfigLoading(normalizedTaskKey, true);
const requestPromise = request({ const requestPromise = request({
url: `/api/training-tasks/key/${normalizedTaskKey}`, url: `/api/training-tasks/key/${normalizedTaskKey}`,
method: "get", method: "get",
}); });
pendingTaskRequests.set(normalizedTaskKey, requestPromise); pendingTaskRequests.set(normalizedTaskKey, requestPromise);
requestPromise.then( requestPromise.then(
() => pendingTaskRequests.delete(normalizedTaskKey), () => {
() => pendingTaskRequests.delete(normalizedTaskKey) pendingTaskRequests.delete(normalizedTaskKey);
notifyStudentTaskConfigLoading(normalizedTaskKey, false);
},
() => {
pendingTaskRequests.delete(normalizedTaskKey);
notifyStudentTaskConfigLoading(normalizedTaskKey, false);
}
); );
return requestPromise; return requestPromise;
} }

@ -11,10 +11,19 @@
<div v-if="isDemo" class="student-demo-banner">演示模式:{{ demoClassName }}<button @click="exitDemo">退</button></div> <div v-if="isDemo" class="student-demo-banner">演示模式:{{ demoClassName }}<button @click="exitDemo">退</button></div>
<router-view v-slot="{ Component, route }"> <router-view v-slot="{ Component, route }">
<keep-alive :include="tagsViewStore.cachedViews"> <keep-alive :include="tagsViewStore.cachedViews">
<component v-if="!route.meta.link" :is="Component" :key="route.path" /> <component v-if="!route.meta.link" v-show="!studentTaskLoading" :is="Component" :key="route.path" />
</keep-alive> </keep-alive>
</router-view> </router-view>
<DynamicTrainingStepNavigator v-if="Number(role) === 4 && route.meta?.pageKey" :task-key="route.meta.pageKey" /> <DynamicTrainingStepNavigator v-if="Number(role) === 4 && route.meta?.pageKey" v-show="!studentTaskLoading" :task-key="route.meta.pageKey" />
<div v-if="studentTaskLoading" class="student-task-loading" role="status" aria-live="polite">
<div class="student-task-loading__panel">
<span class="student-task-loading__label">正在加载当前实训配置</span>
<span class="student-task-loading__line student-task-loading__line--title"></span>
<span class="student-task-loading__line"></span>
<span class="student-task-loading__line"></span>
<span class="student-task-loading__line student-task-loading__line--short"></span>
</div>
</div>
<iframe-toggle /> <iframe-toggle />
</section> </section>
</template> </template>
@ -33,6 +42,8 @@ const tagsViewStore = useTagsViewStore();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const role = ref(JSON.parse(getUserInfo()).roleId); const role = ref(JSON.parse(getUserInfo()).roleId);
const studentTaskLoading = ref(false);
const currentStudentTaskKey = computed(() => Number(role.value) === 4 ? String(route.meta?.pageKey || "") : "");
const isDemo = computed(() => isStudentDemo()); const isDemo = computed(() => isStudentDemo());
const demoClassName = computed(() => getStudentDemoSession()?.userInfo?.className || "教学班"); const demoClassName = computed(() => getStudentDemoSession()?.userInfo?.className || "教学班");
function exitDemo() { function exitDemo() {
@ -48,6 +59,16 @@ const mainClass = computed(() => {
if (Number(role.value) === 3) return "teacher-main"; if (Number(role.value) === 3) return "teacher-main";
return "app-main2"; return "app-main2";
}); });
function handleStudentTaskConfigLoading(event) {
const detail = event?.detail || {};
if (!currentStudentTaskKey.value || detail.taskKey !== currentStudentTaskKey.value) return;
studentTaskLoading.value = Boolean(detail.loading);
}
watch(currentStudentTaskKey, (taskKey) => {
studentTaskLoading.value = Boolean(taskKey);
}, { immediate: true });
onMounted(() => window.addEventListener("student-task-config-loading", handleStudentTaskConfigLoading));
onBeforeUnmount(() => window.removeEventListener("student-task-config-loading", handleStudentTaskConfigLoading));
const module = ["市场需求挖掘", "产品规划", "产品投放测试", "供应渠道管理", "产品评估与考核"]; const module = ["市场需求挖掘", "产品规划", "产品投放测试", "供应渠道管理", "产品评估与考核"];
// //
onBeforeRouteUpdate((to, from) => { onBeforeRouteUpdate((to, from) => {
@ -169,6 +190,7 @@ router.beforeEach((to, from, next) => {
background-size: 100% 100%; background-size: 100% 100%;
} }
.student-demo-banner{position:sticky;top:0;z-index:30;display:flex;justify-content:space-between;padding:8px 18px;color:#fff;background:#b45309}.student-demo-banner button{border:0;border-radius:4px;padding:3px 10px;cursor:pointer} .student-demo-banner{position:sticky;top:0;z-index:30;display:flex;justify-content:space-between;padding:8px 18px;color:#fff;background:#b45309}.student-demo-banner button{border:0;border-radius:4px;padding:3px 10px;cursor:pointer}
.student-task-loading{position:fixed;inset:50px 0 0;z-index:90;display:grid;place-items:start center;padding-top:96px;background:linear-gradient(135deg,rgba(2,15,26,.985),rgba(4,31,47,.985));}.student-task-loading__panel{display:grid;gap:18px;width:min(820px,calc(100vw - 72px));padding:32px;border:1px solid rgba(66,201,250,.34);border-radius:18px;background:rgba(4,27,42,.8);box-shadow:0 22px 52px rgba(0,0,0,.32)}.student-task-loading__label{color:#bdefff;font-weight:700;letter-spacing:.04em}.student-task-loading__line{display:block;height:18px;border-radius:99px;background:linear-gradient(90deg,rgba(26,107,143,.32),rgba(88,208,255,.58),rgba(26,107,143,.32));background-size:200% 100%;animation:student-task-loading-shimmer 1.2s linear infinite}.student-task-loading__line--title{width:42%;height:30px}.student-task-loading__line--short{width:68%}@keyframes student-task-loading-shimmer{to{background-position:-200% 0}}
.app-main2 { .app-main2 {
min-height: calc(100vh - 50px); min-height: calc(100vh - 50px);
width: 100%; width: 100%;

Loading…
Cancel
Save