|
|
<!--
|
|
|
* @Author: qinzhenpen qzp1807@126.com
|
|
|
* @Date: 2024-07-15 15:44:46
|
|
|
* @LastEditors: qinzhenpen qzp1807@126.com
|
|
|
* @LastEditTime: 2025-05-21 18:07:48
|
|
|
* @FilePath: \vue3\src\layout\components\AppMain.vue
|
|
|
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
|
|
-->
|
|
|
<template>
|
|
|
<section :class="mainClass">
|
|
|
<div v-if="isDemo" class="student-demo-banner">演示模式:{{ demoClassName }}<button @click="exitDemo">退出演示</button></div>
|
|
|
<router-view v-slot="{ Component, route }">
|
|
|
<keep-alive :include="tagsViewStore.cachedViews">
|
|
|
<component v-if="!route.meta.link" :is="Component" :key="route.path" />
|
|
|
</keep-alive>
|
|
|
</router-view>
|
|
|
<DynamicTrainingStepNavigator v-if="Number(role) === 4 && route.meta?.pageKey" :task-key="route.meta.pageKey" />
|
|
|
<iframe-toggle />
|
|
|
</section>
|
|
|
</template>
|
|
|
<script setup>
|
|
|
import * as indexApi from "@/api/index";
|
|
|
const { proxy } = getCurrentInstance();
|
|
|
import { dynamicRoutes } from "@/router";
|
|
|
import iframeToggle from "./IframeToggle/index";
|
|
|
import DynamicTrainingStepNavigator from "@/views/components/DynamicTrainingStepNavigator.vue";
|
|
|
import useTagsViewStore from "@/store/modules/tagsView";
|
|
|
import { getUserInfo } from "@/utils/auth";
|
|
|
import useUserStore from "@/store/modules/user";
|
|
|
import { clearStudentDemoSession, getStudentDemoSession, isStudentDemo } from "@/utils/studentDemo";
|
|
|
const userStore = useUserStore();
|
|
|
const tagsViewStore = useTagsViewStore();
|
|
|
const route = useRoute();
|
|
|
const router = useRouter();
|
|
|
const role = ref(JSON.parse(getUserInfo()).roleId);
|
|
|
const isDemo = computed(() => isStudentDemo());
|
|
|
const demoClassName = computed(() => getStudentDemoSession()?.userInfo?.className || "教学班");
|
|
|
function exitDemo() {
|
|
|
clearStudentDemoSession();
|
|
|
window.close();
|
|
|
// 仅在浏览器不允许关闭窗口时兜底,避免停留在演示会话中转页。
|
|
|
window.setTimeout(() => {
|
|
|
if (!window.closed) window.location.replace("/index");
|
|
|
}, 100);
|
|
|
}
|
|
|
const mainClass = computed(() => {
|
|
|
if (Number(role.value) === 4) return "app-main";
|
|
|
if (Number(role.value) === 3) return "teacher-main";
|
|
|
return "app-main2";
|
|
|
});
|
|
|
const module = ["市场需求挖掘", "产品规划", "产品投放测试", "供应渠道管理", "产品评估与考核"];
|
|
|
// 全局监听路由进入和离开
|
|
|
onBeforeRouteUpdate((to, from) => {
|
|
|
tagsViewStore.addVisitedView(to);
|
|
|
tagsViewStore.addCachedView(to);
|
|
|
});
|
|
|
function findParentByTitle(routes, targetTitle) {
|
|
|
for (const route of routes) {
|
|
|
if (route.children && route.children.length) {
|
|
|
for (const child of route.children) {
|
|
|
// 如果 child 还有 children,则递归查找
|
|
|
if (child.children && child.children.length) {
|
|
|
const found = findParentByTitle([child], targetTitle);
|
|
|
if (found) return found;
|
|
|
} else if (child.meta && child.meta.title === targetTitle) {
|
|
|
// 找到了目标 title,返回它的父级 route
|
|
|
return route;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
return null;
|
|
|
}
|
|
|
function useTimeRecorder() {
|
|
|
const startTime = ref(null);
|
|
|
const start = () => {
|
|
|
startTime.value = new Date();
|
|
|
console.log("计时开始");
|
|
|
};
|
|
|
const end = () => {
|
|
|
if (!startTime.value) {
|
|
|
console.warn("请先调用 start() 方法!");
|
|
|
return null;
|
|
|
}
|
|
|
const durationMs = new Date() - startTime.value; // 毫秒差值
|
|
|
const durationMinutes = (durationMs / (1000 * 60)).toFixed(2); // 转为分钟,保留2位小数
|
|
|
console.log(`计时结束,耗时: ${durationMinutes} 秒`);
|
|
|
return parseFloat(durationMinutes); // 返回字符串(如 "3.14")
|
|
|
};
|
|
|
const reset = () => {
|
|
|
startTime.value = null;
|
|
|
console.log("计时器已重置");
|
|
|
};
|
|
|
return { start, end, reset };
|
|
|
}
|
|
|
const { start, end, reset } = useTimeRecorder();
|
|
|
const getCurrentTeachingClassId = async () => {
|
|
|
if (isDemo.value) return getStudentDemoSession()?.userInfo?.demoTeachingClassId || "";
|
|
|
const cachedClassId = proxy.$cache.session.get("currentTeachingClassId");
|
|
|
if (cachedClassId) return cachedClassId;
|
|
|
const res = await indexApi.getCurrentTeachingClass({ userId: userStore.userInfo.userId });
|
|
|
const classId = res?.data?.schoolClassId || "";
|
|
|
if (classId) {
|
|
|
proxy.$cache.session.set("currentTeachingClassId", classId);
|
|
|
}
|
|
|
return classId;
|
|
|
};
|
|
|
const updateVisitCountInBackground = (projectName) => {
|
|
|
getCurrentTeachingClassId()
|
|
|
.then((currentClassId) => {
|
|
|
if (!currentClassId) return;
|
|
|
return indexApi.updateVisitCount({
|
|
|
classId: currentClassId,
|
|
|
projectName,
|
|
|
schoolId: userStore.userInfo.schoolId,
|
|
|
userId: userStore.userInfo.userId,
|
|
|
});
|
|
|
})
|
|
|
.catch((error) => {
|
|
|
console.warn("更新模块访问次数失败", error);
|
|
|
});
|
|
|
};
|
|
|
router.beforeEach((to, from, next) => {
|
|
|
if (isDemo.value) { next(); return; }
|
|
|
const disabledTitles = proxy.$cache.session.getJSON("disabledTitles");
|
|
|
const routess = Array.isArray(disabledTitles) ? disabledTitles : [];
|
|
|
// 获取当前和上一个模块
|
|
|
const toModule = findParentByTitle(dynamicRoutes, to.meta.title)?.meta?.title;
|
|
|
const fromModule = findParentByTitle(dynamicRoutes, from.meta.title)?.meta?.title;
|
|
|
// 离开模块时结束计时
|
|
|
if (module.includes(fromModule)) {
|
|
|
const timeData = end();
|
|
|
if (timeData) {
|
|
|
// 发送学习时间到后端
|
|
|
indexApi
|
|
|
.updateStudyTime({
|
|
|
time: Number(timeData),
|
|
|
projectName: fromModule,
|
|
|
userId: userStore.userInfo.userId,
|
|
|
})
|
|
|
.then((res) => {});
|
|
|
}
|
|
|
}
|
|
|
// 进入新模块时开始计时
|
|
|
if (module.includes(toModule)) {
|
|
|
// 判断是否有禁用的模块
|
|
|
if (routess.includes(to.meta.title)) {
|
|
|
proxy.$modal.msgError("该模块示暂未开放");
|
|
|
return;
|
|
|
}
|
|
|
start(toModule);
|
|
|
if (module.includes(fromModule)) {
|
|
|
// 访问统计不影响页面跳转,避免等待当前教学班接口。
|
|
|
updateVisitCountInBackground(fromModule);
|
|
|
}
|
|
|
}
|
|
|
next();
|
|
|
});
|
|
|
</script>
|
|
|
<style lang="scss" scoped>
|
|
|
.app-main {
|
|
|
/* 50= navbar 50 */
|
|
|
min-height: calc(100vh - 50px);
|
|
|
height: calc(100vh - 50px);
|
|
|
width: 100%;
|
|
|
position: relative;
|
|
|
overflow: auto;
|
|
|
background: url("@/assets/images/背景.webp") no-repeat;
|
|
|
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}
|
|
|
.app-main2 {
|
|
|
min-height: calc(100vh - 50px);
|
|
|
width: 100%;
|
|
|
position: relative;
|
|
|
overflow: hidden;
|
|
|
background: url("@/assets/images/背景f6.png") no-repeat;
|
|
|
background-size: 100% 100%;
|
|
|
}
|
|
|
.teacher-main {
|
|
|
min-height: calc(100vh - 50px);
|
|
|
width: 100%;
|
|
|
position: relative;
|
|
|
overflow: auto;
|
|
|
padding: 22px 24px 28px;
|
|
|
background:
|
|
|
radial-gradient(circle at 12% 18%, rgba(14, 74, 112, 0.5), transparent 34%),
|
|
|
radial-gradient(circle at 78% 6%, rgba(25, 92, 170, 0.34), transparent 28%),
|
|
|
linear-gradient(135deg, #03070e 0%, #061827 48%, #02060d 100%);
|
|
|
}
|
|
|
.fixed-header + .app-main {
|
|
|
padding-top: 50px;
|
|
|
}
|
|
|
.hasTagsView {
|
|
|
.app-main {
|
|
|
/* 84 = navbar + tags-view = 50 + 34 */
|
|
|
min-height: calc(100vh - 50px);
|
|
|
}
|
|
|
.fixed-header + .app-main {
|
|
|
padding-top: 84px;
|
|
|
}
|
|
|
}
|
|
|
</style>
|
|
|
<style lang="scss">
|
|
|
// fix css style bug in open el-dialog
|
|
|
.el-popup-parent--hidden {
|
|
|
.fixed-header {
|
|
|
padding-right: 6px;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
::-webkit-scrollbar {
|
|
|
width: 6px;
|
|
|
height: 6px;
|
|
|
}
|
|
|
|
|
|
::-webkit-scrollbar-track {
|
|
|
background-color: #f1f1f1;
|
|
|
}
|
|
|
|
|
|
::-webkit-scrollbar-thumb {
|
|
|
background-color: #c0c0c0;
|
|
|
border-radius: 3px;
|
|
|
}
|
|
|
|
|
|
.teacher-main {
|
|
|
color: #e8f5ff;
|
|
|
|
|
|
.app-container {
|
|
|
min-height: calc(100vh - 96px);
|
|
|
padding: 18px 20px 22px;
|
|
|
border: 1px solid rgba(39, 172, 238, 0.28);
|
|
|
border-radius: 14px;
|
|
|
background: linear-gradient(135deg, rgba(6, 23, 36, 0.84), rgba(4, 12, 23, 0.78));
|
|
|
box-shadow: 0 18px 48px rgba(0, 5, 16, 0.34), inset 0 1px 0 rgba(153, 220, 255, 0.08);
|
|
|
backdrop-filter: blur(12px);
|
|
|
}
|
|
|
|
|
|
.school,
|
|
|
.teacher,
|
|
|
.task,
|
|
|
.score,
|
|
|
.class-page,
|
|
|
.teachers {
|
|
|
height: auto !important;
|
|
|
min-height: calc(100vh - 96px);
|
|
|
background: transparent !important;
|
|
|
}
|
|
|
|
|
|
.df,
|
|
|
.online,
|
|
|
.query-bar,
|
|
|
.table-actions,
|
|
|
.demo-form-inline {
|
|
|
color: #cfeaff;
|
|
|
}
|
|
|
|
|
|
.df,
|
|
|
.query-bar {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
align-items: center;
|
|
|
gap: 12px;
|
|
|
padding: 14px 16px;
|
|
|
border: 1px solid rgba(44, 169, 226, 0.28) !important;
|
|
|
border-radius: 10px;
|
|
|
background: rgba(2, 18, 31, 0.72) !important;
|
|
|
}
|
|
|
|
|
|
.online {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
gap: 8px;
|
|
|
}
|
|
|
|
|
|
.online span,
|
|
|
.el-form-item__label {
|
|
|
color: #cfeaff;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
.mt10,
|
|
|
.table-actions {
|
|
|
margin-top: 14px;
|
|
|
}
|
|
|
|
|
|
.el-input__wrapper,
|
|
|
.el-select__wrapper,
|
|
|
.el-textarea__inner {
|
|
|
background: rgba(3, 27, 43, 0.88);
|
|
|
border: 1px solid rgba(61, 171, 220, 0.42);
|
|
|
box-shadow: none;
|
|
|
}
|
|
|
|
|
|
.el-input__inner,
|
|
|
.el-select__placeholder,
|
|
|
.el-select__selected-item,
|
|
|
.el-textarea__inner {
|
|
|
color: #eaf7ff;
|
|
|
}
|
|
|
|
|
|
.el-input__inner::placeholder {
|
|
|
color: rgba(189, 221, 244, 0.58);
|
|
|
}
|
|
|
|
|
|
.el-button--primary {
|
|
|
border-color: rgba(48, 185, 255, 0.85);
|
|
|
background: linear-gradient(135deg, #078dce, #1557d6);
|
|
|
box-shadow: 0 8px 18px rgba(0, 110, 220, 0.24);
|
|
|
}
|
|
|
|
|
|
.el-button--warning {
|
|
|
border-color: rgba(245, 166, 35, 0.75);
|
|
|
background: linear-gradient(135deg, #d99620, #b76418);
|
|
|
}
|
|
|
|
|
|
.el-button.is-text {
|
|
|
color: #80d7ff;
|
|
|
}
|
|
|
|
|
|
.el-button--danger.is-text {
|
|
|
color: #ff8d8d;
|
|
|
}
|
|
|
|
|
|
.el-table {
|
|
|
--el-table-border-color: rgba(54, 159, 212, 0.2);
|
|
|
--el-table-header-bg-color: rgba(9, 48, 73, 0.96);
|
|
|
--el-table-tr-bg-color: rgba(2, 16, 27, 0.78);
|
|
|
--el-fill-color-lighter: rgba(2, 16, 27, 0.72);
|
|
|
--el-table-row-hover-bg-color: rgba(0, 111, 177, 0.18);
|
|
|
margin-top: 16px !important;
|
|
|
color: #dff4ff;
|
|
|
border-radius: 10px;
|
|
|
overflow: hidden;
|
|
|
background: transparent;
|
|
|
}
|
|
|
|
|
|
.el-table th.el-table__cell {
|
|
|
color: #dff6ff !important;
|
|
|
font-weight: 700;
|
|
|
background: linear-gradient(180deg, rgba(13, 70, 103, 0.98), rgba(8, 43, 67, 0.98)) !important;
|
|
|
}
|
|
|
|
|
|
.el-table tr,
|
|
|
.el-table td.el-table__cell {
|
|
|
background: rgba(2, 16, 27, 0.72);
|
|
|
}
|
|
|
|
|
|
.el-table__empty-text {
|
|
|
color: #9abed8;
|
|
|
}
|
|
|
|
|
|
.el-card {
|
|
|
color: #e9f7ff;
|
|
|
border: 1px solid rgba(54, 159, 212, 0.28);
|
|
|
background: rgba(4, 20, 33, 0.78);
|
|
|
}
|
|
|
|
|
|
.el-card__header {
|
|
|
border-bottom-color: rgba(54, 159, 212, 0.22);
|
|
|
}
|
|
|
|
|
|
.el-pagination {
|
|
|
margin-top: 16px;
|
|
|
justify-content: flex-end;
|
|
|
color: #cdeaff;
|
|
|
}
|
|
|
|
|
|
.el-tabs__item {
|
|
|
color: #a9cbe1;
|
|
|
}
|
|
|
|
|
|
.el-tabs__item.is-active {
|
|
|
color: #7fe0ff;
|
|
|
}
|
|
|
|
|
|
.el-tabs__active-bar {
|
|
|
background-color: #21c8ff;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
<style lang="scss">
|
|
|
.teacher-dialog {
|
|
|
--el-dialog-bg-color: #071522;
|
|
|
--el-text-color-primary: #eef7ff;
|
|
|
--el-text-color-regular: #d5ecff;
|
|
|
}
|
|
|
|
|
|
.teacher-dialog.el-dialog,
|
|
|
.teacher-dialog .el-dialog {
|
|
|
border: 1px solid rgba(52, 177, 238, 0.42);
|
|
|
border-radius: 12px;
|
|
|
background: linear-gradient(135deg, rgba(6, 24, 38, 0.98), rgba(4, 13, 24, 0.98));
|
|
|
box-shadow: 0 20px 60px rgba(0, 4, 14, 0.55), inset 0 1px 0 rgba(169, 226, 255, 0.08);
|
|
|
}
|
|
|
|
|
|
.teacher-dialog .el-dialog__header {
|
|
|
padding: 18px 22px 12px;
|
|
|
border-bottom: 1px solid rgba(52, 177, 238, 0.18);
|
|
|
background: transparent;
|
|
|
}
|
|
|
|
|
|
.teacher-dialog .el-dialog__title {
|
|
|
color: #eef9ff;
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.teacher-dialog .el-dialog__headerbtn .el-dialog__close {
|
|
|
color: #bdeaff;
|
|
|
}
|
|
|
|
|
|
.teacher-dialog .el-dialog__headerbtn:hover .el-dialog__close {
|
|
|
color: #ffffff;
|
|
|
}
|
|
|
|
|
|
.teacher-dialog .el-dialog__body {
|
|
|
color: #d9efff;
|
|
|
background: transparent;
|
|
|
}
|
|
|
|
|
|
.teacher-dialog .el-dialog__footer {
|
|
|
background: transparent;
|
|
|
}
|
|
|
|
|
|
.teacher-dialog .el-form-item__label {
|
|
|
color: #cfeaff;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
.teacher-dialog .el-input__wrapper,
|
|
|
.teacher-dialog .el-select__wrapper,
|
|
|
.teacher-dialog .el-textarea__inner {
|
|
|
background: rgba(3, 27, 43, 0.92);
|
|
|
border: 1px solid rgba(61, 171, 220, 0.42);
|
|
|
box-shadow: none;
|
|
|
}
|
|
|
|
|
|
.teacher-dialog .el-input__inner,
|
|
|
.teacher-dialog .el-select__placeholder,
|
|
|
.teacher-dialog .el-select__selected-item,
|
|
|
.teacher-dialog .el-textarea__inner {
|
|
|
color: #eaf7ff;
|
|
|
}
|
|
|
|
|
|
.teacher-dialog .el-input__inner::placeholder {
|
|
|
color: rgba(189, 221, 244, 0.58);
|
|
|
}
|
|
|
|
|
|
.teacher-dialog .el-input.is-disabled .el-input__wrapper,
|
|
|
.teacher-dialog .el-select.is-disabled .el-select__wrapper {
|
|
|
background: rgba(3, 20, 33, 0.72);
|
|
|
border-color: rgba(61, 171, 220, 0.22);
|
|
|
}
|
|
|
|
|
|
.teacher-dialog .el-input.is-disabled .el-input__inner,
|
|
|
.teacher-dialog .el-select.is-disabled .el-select__selected-item {
|
|
|
color: rgba(220, 242, 255, 0.62);
|
|
|
}
|
|
|
|
|
|
.teacher-dialog .el-button:not(.el-button--primary) {
|
|
|
color: #dff5ff;
|
|
|
background: rgba(4, 24, 38, 0.92);
|
|
|
border-color: rgba(88, 178, 224, 0.45);
|
|
|
}
|
|
|
|
|
|
.teacher-dialog .el-button:not(.el-button--primary):hover,
|
|
|
.teacher-dialog .el-button:not(.el-button--primary):focus {
|
|
|
color: #ffffff;
|
|
|
background: rgba(10, 48, 72, 0.96);
|
|
|
border-color: rgba(103, 213, 255, 0.72);
|
|
|
}
|
|
|
</style>
|