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/layout/components/AppMain.vue

508 lines
16 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!--
* @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 :max="30">
<component v-if="!route.meta.link" v-show="!studentTaskLoading" :is="Component" :key="route.path" />
</keep-alive>
</router-view>
<div v-if="studentTaskLoading" class="student-task-loading" role="status" aria-live="polite">
<div class="student-task-loading__panel">
<div class="student-task-loading__spinner">
<span class="dot dot--1"></span>
<span class="dot dot--2"></span>
<span class="dot dot--3"></span>
</div>
<span class="student-task-loading__label">AI 正在思考</span>
</div>
</div>
<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 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 studentTaskLoading = ref(false);
const currentStudentTaskKey = computed(() => Number(role.value) === 4 ? String(route.meta?.pageKey || "") : "");
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";
// 超管端单独使用深蓝底色,与深色卡片协调;校管理员端(role 2)仍用 app-main2 浅色底图
if (Number(role.value) === 1) return "app-main2 platform-admin-main";
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) => {
// 已缓存的实训页重新激活时不会再次执行 onMounted 请求,不能仅因路由
// 切换而保留遮罩;真实请求会通过 student-task-config-loading 事件重新打开。
studentTaskLoading.value = false;
}, { immediate: true });
onMounted(() => window.addEventListener("student-task-config-loading", handleStudentTaskConfigLoading));
onBeforeUnmount(() => window.removeEventListener("student-task-config-loading", handleStudentTaskConfigLoading));
const module = ["市场需求挖掘", "产品规划", "产品投放测试", "供应渠道管理", "产品评估与考核"];
// onBeforeRouteUpdate 处理同组件内 query/params 变化的场景
onBeforeRouteUpdate((to, from) => {
tagsViewStore.addVisitedView(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}
.student-task-loading{position:fixed;inset:50px 0 0;z-index:90;display:grid;place-items:center;background:rgba(255,255,255,.72);backdrop-filter:blur(6px);animation:student-task-fade-in .2s ease-out}.student-task-loading__panel{display:flex;flex-direction:column;align-items:center;gap:18px;padding:28px 40px;border-radius:16px;background:#ffffff;border:1px solid #e5e7eb;box-shadow:0 8px 32px rgba(31,41,55,.12)}.student-task-loading__spinner{display:flex;align-items:center;gap:8px}.student-task-loading__spinner .dot{width:10px;height:10px;border-radius:50%;background:linear-gradient(135deg,#38bdf8,#0ea5e9);animation:student-task-dot-bounce 1.2s ease-in-out infinite}.student-task-loading__spinner .dot--2{animation-delay:.2s}.student-task-loading__spinner .dot--3{animation-delay:.4s}.student-task-loading__label{color:#1f2937;font-weight:600;font-size:14px;letter-spacing:.02em}@keyframes student-task-dot-bounce{0%,80%,100%{transform:scale(.7);opacity:.55}40%{transform:scale(1);opacity:1}}@keyframes student-task-fade-in{from{opacity:0}to{opacity:1}}
.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%;
}
// platform-admin-main 的 height/overflow/background 统一由 src/assets/styles/sidebar.scss 中
// #app .platform-admin-main 全局规则兜底(ID 优先级高于 scoped 0,3,0,且有 !important),
// 此处 scoped 仅保留类名绑定,不重复声明 body styles 避免双写来源冲突。
.platform-admin-main {}
.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 {
width: min(760px, calc(100vw - 32px));
max-height: calc(100vh - 32px);
overflow: hidden;
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 {
min-height: 0;
overflow-y: auto;
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>