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/Sidebar/index.vue

624 lines
22 KiB
Vue

2 years ago
<!--
* @Author: qinzhenpen qzp1807@126.com
* @Date: 2024-08-09 17:36:23
* @LastEditors: qinzhenpen qzp1807@126.com
2 months ago
* @LastEditTime: 2025-05-21 17:19:46
2 years ago
* @FilePath: \vue3\src\layout\components\Sidebar\index.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<div :class="[{ 'has-logo': showLogo }, { 'student-sidebar-shell': isCourseStyleSidebar }]" :style="sidebarStyle">
<!-- <logo v-if="showLogo" :collapse="isCollapse" /> -->
<template v-if="isCourseStyleSidebar">
<el-scrollbar class="theme-dark student-sidebar-scroll" wrap-class="scrollbar-wrapper">
<nav class="student-course-nav">
<template v-for="section in currentCourseNav" :key="section.path || section.title">
<button v-if="section.path" type="button" :class="['student-course-heading', { active: isStudentNavActive(section) }]" @click="goStudentNav(section.path)">
<span class="student-course-icon student-course-icon--heading">
<component :is="getStudentNavIcon(section)" />
</span>
<span class="student-course-text">{{ getCourseNavTitle(section) }}</span>
</button>
<div v-else class="student-course-heading">
<span class="student-course-icon student-course-icon--heading">
<component :is="getStudentNavIcon(section)" />
</span>
<span class="student-course-text">{{ getCourseNavTitle(section) }}</span>
</div>
<div v-if="section.children?.length" class="student-course-list">
<template v-for="child in section.children" :key="child.path || child.title">
<button v-if="child.path" type="button" :class="['student-course-link', { active: isStudentNavActive(child) }]" @click="goStudentNav(child.path)">
<span class="student-course-icon">
<component :is="getStudentNavIcon(child)" />
</span>
<span class="student-course-text">{{ getCourseNavTitle(child) }}</span>
</button>
<div v-else class="student-course-link disabled">
<span class="student-course-icon">
<component :is="getStudentNavIcon(child)" />
</span>
<span class="student-course-text">{{ getCourseNavTitle(child) }}</span>
</div>
</template>
</div>
</template>
</nav>
</el-scrollbar>
</template>
<el-scrollbar v-else :class="sideTheme" wrap-class="scrollbar-wrapper">
<el-menu :key="sidebarMenuKey" :default-openeds="['/demand/demand', '/product/product', '/test/test', '/channel/channel', '/assessment/assessment']" :default-active="activeMenu" :collapse="isCollapse" :background-color="sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground" :text-color="sideTheme === 'theme-dark' ? variables.menuColor : variables.menuLightColor" :unique-opened="false" :active-text-color="theme" :collapse-transition="false" mode="vertical">
<sidebar-item v-for="(route, index) in visibleSidebarRouters" :key="route.path + index" :item="route" :base-path="route.path" />
</el-menu>
</el-scrollbar>
5 years ago
</div>
</template>
<script setup>
2 months ago
import Logo from "./Logo";
import SidebarItem from "./SidebarItem";
import variables from "@/assets/styles/variables.module.scss";
import useAppStore from "@/store/modules/app";
import * as indexApi from "@/api/teacher";
import useSettingsStore from "@/store/modules/settings";
import usePermissionStore from "@/store/modules/permission";
import { constantRoutes, dynamicRoutes, teacherRoutes, platformAdminRoutes, schoolAdminRoutes } from "@/router/index.js";
import useUserStore from "@/store/modules/user";
import { getCachedStudentTrainingTasks, loadStudentTrainingTasks } from "@/api/trainingTask";
import { buildStudentTaskSections } from "@/utils/studentTrainingNavigation";
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
const { proxy } = getCurrentInstance();
const userStore = useUserStore();
const route = useRoute();
const router = useRouter();
const appStore = useAppStore();
2 months ago
const settingsStore = useSettingsStore();
const permissionStore = usePermissionStore();
const sidebarRouters = ref([...constantRoutes]);
const visibleSidebarRouters = computed(() => filterRoutesByOrganizationMode(sidebarRouters.value));
const sidebarMenuKey = computed(() => visibleSidebarRouters.value.map((route) => route.path).join("|"));
const routerStatus = {
2 months ago
行业需求分析: [
{
id: "9f413263-e6ff-4ee5-984a-7a231cad6818",
module: "行业需求分析-蛋糕指数",
sort: 1,
status: 0,
parentDisabled: true,
},
],
竞争环境分析: [
{
id: "ae274ec6-971a-4a33-aa63-2e449f146487",
module: "竞争环境分析-赫芬达尔指数",
sort: 2,
status: 0,
},
],
消费人群分析: [
{
id: "b91026aa-275c-4779-8602-61051a931d3a",
module: "消费人群分析-波士顿矩阵分析",
sort: 3,
status: 1,
},
{
id: "191026aa-275c-4779-8602-61051a931d3a",
classId: "1",
schoolId: "1",
module: "消费人群分析-RFM分析",
status: 0,
sort: 3,
},
],
用户数据分析: [
{
id: "4b4a685a-b9a3-4edc-9a68-a670d8c84c94",
module: "用户数据分析-描述性统计",
sort: 4,
status: 0,
},
{
id: "1c6099e7-fc8a-4485-b828-8d1aef8e34b4",
module: "用户数据分析-聚类分析",
sort: 5,
status: 1,
},
],
数据化选品: [
{
id: "fc9f19cb-8f77-4810-a538-eec97f840c0a",
module: "数据化选品-线性预测法-销售预测",
sort: 6,
status: 0,
},
],
产品定价: [
{
id: "b95af20c-6242-42cd-a7c2-ddec7b9a0957",
module: "产品定价-PSM模型",
sort: 7,
status: 0,
},
],
产品差异化定位: [
{
id: "4ca53e70-c168-455a-8dbd-f7d6475c5903",
module: "产品差异化定位-SPAN模型",
sort: 8,
status: 0,
},
],
产品结构规划: [
{
id: "31f289b7-04a7-4b8b-b5b4-6333f7c3d76e",
module: "产品结构规划",
sort: 9,
status: 0,
},
],
发布渠道评估: [
{
id: "b58ef210-624b-43f2-bbac-f788c8d96cc8",
module: "发布渠道评估-指标评估法-熵值法",
sort: 10,
status: 0,
},
],
产品推广测试: [
{
id: "de9206fe-1e95-454f-912c-54dbd0a383cc",
module: "产品推广测试-A/B测试",
sort: 11,
status: 0,
},
],
渠道表现分析: [
{
id: "2063efde-f8ba-4f69-be9b-1d87586044f6",
module: "渠道表现分析-转化分析-漏斗模型",
sort: 12,
status: 0,
},
],
产品营销定位: [
{
id: "e99dee9f-a7a9-4e8c-a77b-8db6dfbc64d7",
module: "产品营销定位-四象限法则",
sort: 13,
status: 0,
},
],
采购需求预测: [
{
id: "c8835d65-db31-4952-9d8d-5d07bf4a9c90",
module: "采购需求预测-季节指数预测法",
sort: 14,
status: 0,
},
],
采购产品定位: [
{
id: "acd195cf-5e43-4c5e-8ec9-e5ad85612063",
module: "采购产品定位-卡拉杰克模型",
sort: 15,
status: 0,
},
],
供应商评估与选择: [
{
id: "206e5b9a-95a0-41d7-b2f2-19f5cf10331a",
module: "供应商评估与选择-线性权重法",
sort: 16,
status: 0,
},
],
产品表现分析: [
{
id: "0aca8b42-c665-4759-b1b4-b35a0daf2e80",
module: "产品表现分析-功效系数法",
sort: 17,
status: 0,
},
],
产品问题诊断: [
{
id: "55a0ffd4-9863-40d8-bc4c-7830c731eaa7",
module: "产品问题诊断-五维四率法",
sort: 18,
status: 1,
},
{
id: "205f28ce-31d8-449a-8053-7d6f85e98a68",
module: "产品问题诊断-KANO模型",
sort: 19,
status: 0,
},
{
id: "2768d846-dfe8-42c1-b31f-f4deef85f34d",
module: "产品问题诊断-市场机会矩阵",
sort: 20,
status: 0,
},
{
id: "d0fc3cb4-2cde-401f-b243-33bdb5ef1094",
module: "产品问题诊断-流量数据分析法",
sort: 21,
status: 0,
},
{
id: "25896e1a-b099-4778-bd87-3b92aa8845da",
module: "产品问题诊断-用户评论分析",
sort: 22,
status: 0,
},
],
品类结构优化: [
{
id: "6bbeebf1-662a-46d6-aa4a-e5e8c4c3d5f1",
module: "品类结构优化-帕累托/ABC分析",
sort: 23,
status: 1,
},
{
id: "90212d7c-d8b5-4455-bc7a-693ded4b3f8f",
module: "品类结构优化-购物篮分析",
sort: 24,
status: 0,
},
],
};
const showLogo = computed(() => settingsStore.sidebarLogo);
const sideTheme = computed(() => settingsStore.sideTheme);
const theme = computed(() => settingsStore.theme);
const isCollapse = computed(() => !appStore.sidebar.opened);
const isTeacherRole = computed(() => Number(userStore.userInfo.roleId) === 3);
const isStudentRole = computed(() => ![1, 2, 3].includes(Number(userStore.userInfo.roleId)));
const isCourseStyleSidebar = computed(() => isStudentRole.value || isTeacherRole.value);
const sidebarStyle = computed(() => {
if (isCourseStyleSidebar.value) return {};
return {
backgroundColor: sideTheme.value === "theme-dark" ? variables.menuBackground : variables.menuLightBackground,
};
});
const studentCourseNav = [
{ title: "AI智能助手", path: "/assistant/index", activePrefix: "/assistant" },
{
title: "互联网产品开发基础",
children: [
{ title: "新产品调查与分析", path: "/foundation/new-product-survey" },
{ title: "产品开发关键因素", path: "/foundation/product-development-factors" },
{ title: "产品开发主要流程", path: "/foundation/product-development-process" },
],
},
{
title: "市场洞察与需求分析",
children: [
{ title: "行业需求分析", path: "/demand/index" },
{ title: "竞争环境分析", path: "/demand/environment" },
{ title: "市场机会识别与选品", path: "/product/index" },
{ title: "商业可行性论证", path: "/demand/business-feasibility" },
{ title: "目标用户画像", path: "/demand/people" },
{ title: "消费者需求与行为", path: "/demand/consumer-behavior" },
{ title: "消费场景解构", path: "/demand/consumer-scenario" },
{ title: "需求转化为功能", path: "/demand/feature-conversion" },
{ title: "产品价值主张及定位", path: "/product/differentiation" },
],
},
{
title: "产品规划与设计",
children: [
{ title: "产品架构设计", path: "/product/structure" },
{ title: "售后服务流程设计", path: "/product/after-sales-service" },
{ title: "产品需求说明书", path: "/product/requirements-doc" },
],
},
{
title: "产品开发与测试验证",
children: [
{ title: "智能硬件成本估算", path: "/product/hardware-cost" },
{ title: "供应渠道评估", path: "/channel/supplier" },
{ title: "采购需求预测", path: "/channel/index" },
{ title: "产品定价", path: "/product/price" },
],
},
{
title: "产品上线与运营推广",
children: [
{ title: "品类结构优化", path: "/assessment/optimization" },
{ title: "产品发布渠道评估", path: "/test/index" },
{ title: "投放效果评估与优化", path: "/test/promotion" },
{ title: "产品问题诊断与迭代", path: "/assessment/diagnosis" },
],
},
{
title: "综合实训",
children: [
{ title: "综合案例实训", path: "/comprehensive/index" },
],
},
];
const teacherManageNav = [
{
title: "教学管理",
children: [
{ title: "院系管理", path: "/school/index", requiresFacultyManagement: true },
{ title: "专业管理", path: "/major/index", requiresMajorManagement: true },
{ title: "班级管理", path: "/class/index" },
{ title: "教师管理", path: "/teacher/index" },
{ title: "学生管理", path: "/student/index" },
{ title: "实训任务管理", path: "/training-task/index" },
{ title: "任务分配", path: "/task/index" },
{ title: "成绩中心", path: "/score/index" },
],
},
];
const trainingTaskKeyByPath = {
"/comprehensive/index": "comprehensive-case-training",
"/foundation/new-product-survey": "new-product-survey",
"/foundation/product-development-factors": "product-development-factors",
"/foundation/product-development-process": "product-development-process",
"/demand/index": "industry-demand-analysis",
"/demand/environment": "competitive-environment-analysis",
"/product/index": "market-opportunity-selection",
"/demand/business-feasibility": "business-feasibility",
"/demand/people": "target-user-profile",
"/demand/consumer-behavior": "consumer-behavior",
"/demand/consumer-scenario": "consumer-scenario",
"/demand/feature-conversion": "feature-conversion",
"/product/differentiation": "product-value-positioning",
"/product/structure": "product-structure",
"/product/after-sales-service": "after-sales-service",
"/product/requirements-doc": "requirements-doc",
"/product/hardware-cost": "hardware-cost",
"/channel/supplier": "supplier-evaluation",
"/channel/index": "purchase-demand-forecast",
"/product/price": "product-pricing",
"/assessment/optimization": "category-optimization",
"/test/index": "release-channel-evaluation",
"/test/promotion": "promotion-effect-optimization",
"/assessment/diagnosis": "product-diagnosis-iteration",
};
const studentCourseBaseNav = [
{ title: "AI智能助手", path: "/assistant/index", activePrefix: "/assistant" },
];
const studentTaskSections = ref([]);
const currentCourseNav = computed(() => (isTeacherRole.value
? filterTeacherManageNavigation(teacherManageNav)
: [...studentCourseBaseNav, ...studentTaskSections.value]));
const studentIconByPath = {
"/assistant/index": "MagicStick",
"/comprehensive/index": "School",
"/foundation/new-product-survey": "Search",
"/foundation/product-development-factors": "SetUp",
"/foundation/product-development-process": "Guide",
"/demand/index": "DataAnalysis",
"/demand/environment": "TrendCharts",
"/product/index": "Opportunity",
"/demand/business-feasibility": "Histogram",
"/demand/people": "UserFilled",
"/demand/consumer-behavior": "ShoppingCart",
"/demand/consumer-scenario": "MapLocation",
"/demand/feature-conversion": "Connection",
"/product/differentiation": "PriceTag",
"/product/structure": "Box",
"/product/after-sales-service": "Service",
"/product/requirements-doc": "Document",
"/product/hardware-cost": "Cpu",
"/channel/supplier": "Ship",
"/channel/index": "ShoppingBag",
"/product/price": "Money",
"/assessment/optimization": "CollectionTag",
"/test/index": "Promotion",
"/test/promotion": "DataLine",
"/assessment/diagnosis": "Tools",
"/report/index": "DocumentChecked",
"/school/index": "OfficeBuilding",
"/major/index": "Collection",
"/class/index": "Notebook",
"/teacher/index": "UserFilled",
"/student/index": "Avatar",
"/training-task/index": "DocumentChecked",
"/task/index": "Tickets",
"/score/index": "DataBoard",
};
const studentIconBySectionStart = [
{ path: "/training/", icon: "Document" },
{ path: "/foundation/", icon: "Collection" },
{ path: "/demand/", icon: "Search" },
{ path: "/product/", icon: "Goods" },
{ path: "/channel/", icon: "Connection" },
{ path: "/assessment/", icon: "DataBoard" },
{ path: "/test/", icon: "Finished" },
{ path: "/school/", icon: "OfficeBuilding" },
{ path: "/major/", icon: "Collection" },
{ path: "/class/", icon: "Notebook" },
{ path: "/teacher/", icon: "UserFilled" },
{ path: "/student/", icon: "Avatar" },
{ path: "/training-task/", icon: "DocumentChecked" },
{ path: "/task/", icon: "Tickets" },
{ path: "/score/", icon: "DataBoard" },
];
function getStudentNavIcon(item) {
const path = item.path || item.children?.[0]?.path || "";
const exactIcon = studentIconByPath[path];
const sectionIcon = studentIconBySectionStart.find((section) => path.startsWith(section.path))?.icon;
return ElementPlusIconsVue[exactIcon || sectionIcon] || ElementPlusIconsVue.Menu;
}
const activeMenu = computed(() => {
const { meta, path } = route;
5 years ago
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu;
}
return path;
});
function isStudentNavActive(item) {
if (item.activePrefix) {
return route.path.startsWith(item.activePrefix);
}
return route.path === item.path;
}
function goStudentNav(path) {
if (!path || route.path === path) return;
router.push(path);
}
function getCourseNavTitle(item) {
return item?.title || "";
}
async function loadStudentTaskNavigation() {
if (!isStudentRole.value) {
studentTaskSections.value = [];
return;
}
const cachedTasks = getCachedStudentTrainingTasks();
if (cachedTasks.length) {
studentTaskSections.value = buildStudentTaskSections(cachedTasks);
}
try {
const res = await loadStudentTrainingTasks();
studentTaskSections.value = buildStudentTaskSections(res.data || []);
} catch (error) {
if (!cachedTasks.length) {
studentTaskSections.value = [];
}
}
}
function groupAndSortModules(modules) {
2 months ago
const grouped = modules.reduce((acc, item) => {
const prefix = item.module.split("-")[0].trim();
if (!acc[prefix]) {
acc[prefix] = [];
}
acc[prefix].push({
id: item.id,
module: item.module,
sort: item.sort,
status: item.disabledStatus,
});
5 years ago
2 months ago
return acc;
}, {});
// 对每个分组按sort排序
Object.keys(grouped).forEach((key) => {
grouped[key].sort((a, b) => a.sort - b.sort);
});
return grouped;
}
function processModuleStatus(groupedData) {
const result = {
data: JSON.parse(JSON.stringify(groupedData)), // 原始数据副本
disabledParents: [], // 被禁用的父模块列表(所有子模块都禁用)
activeParents: [], // 活跃的父模块列表
disabledChildren: [], // 被禁用的子模块(仅当父模块有多个子模块时)
parentStatus: {}, // 每个父模块的详细状态
};
// 分析每个父模块状态
for (const [parentModule, children] of Object.entries(result.data)) {
const disabledCount = children.filter((child) => child.status === 1).length;
const totalCount = children.length;
result.parentStatus[parentModule] = {
disabledCount,
totalCount,
isDisabled: disabledCount === totalCount && totalCount > 0,
};
// 只有当父模块有多个子模块时,才收集被禁用的子模块
if (totalCount > 1) {
children.forEach((child) => {
if (child.status === 1) {
result.disabledChildren.push({
parentModule,
...child,
module: child.module.split("-")[1].trim(),
});
}
});
}
if (result.parentStatus[parentModule].isDisabled) {
result.disabledParents.push(parentModule);
children.forEach((child) => {
child.parentDisabled = true; // 标记父模块禁用状态
});
} else {
result.activeParents.push(parentModule);
}
}
return result;
}
/**
* 过滤掉 children meta.title 在排除列表里的路由
* @param {Array} routes 原始路由数组
* @param {Array} excludeTitles 要排除的 meta.title 列表
* @returns {Array} 新的路由数组
*/
function excludeRoutesByMetaTitle(routes, excludeTitles) {
return routes.reduce((result, route) => {
if (Array.isArray(route.children)) {
const filteredChildren = route.children.filter((child) => !excludeTitles.includes(child.meta?.title));
if (filteredChildren.length > 0) {
result.push({ ...route, children: filteredChildren });
}
} else {
result.push(route); // 没有 children 的保留原样
}
return result;
}, []);
}
const getTaskList = async () => {
if (userStore.userInfo.roleId == 1) {
sidebarRouters.value = [...constantRoutes, ...platformAdminRoutes];
} else if (userStore.userInfo.roleId == 2) {
await userStore.ensureSchoolProductConfig().catch(() => null);
sidebarRouters.value = [...constantRoutes, ...filterRoutesByOrganizationMode(schoolAdminRoutes)];
} else if (userStore.userInfo.roleId != 3) {
await loadStudentTaskNavigation();
sidebarRouters.value = [...constantRoutes, ...dynamicRoutes];
} else {
await userStore.ensureSchoolProductConfig().catch(() => null);
sidebarRouters.value = [...constantRoutes, ...filterRoutesByOrganizationMode(teacherRoutes)];
}
};
function isSchoolNavigationVisible(item) {
const config = userStore.schoolProductConfig;
const facultyEnabled = config?.facultyManagementEnabled === true;
const majorEnabled = config?.majorManagementEnabled === true;
const adminClassEnabled = config?.adminClassManagementEnabled === true;
const capability = item.meta || item;
if (capability.requiresManagerTeacher && userStore.userInfo.teacherAdmin !== true) return false;
if (capability.requiresFacultyManagement && !facultyEnabled) return false;
if (capability.requiresMajorManagement && !majorEnabled) return false;
if (capability.requiresAdminClass && !adminClassEnabled) return false;
return true;
}
function filterRoutesByOrganizationMode(routes) {
return routes.filter(isSchoolNavigationVisible);
}
function filterTeacherManageNavigation(sections) {
return sections
.map((section) => {
if (!section.children) return section;
return {
...section,
children: section.children.filter(isSchoolNavigationVisible),
};
})
.filter((section) => (section.children ? section.children.length > 0 : isSchoolNavigationVisible(section)));
}
watch(
() => userStore.schoolProductConfig?.organizationMode,
(organizationMode, previousOrganizationMode) => {
if (organizationMode === previousOrganizationMode) return;
if ([2, 3].includes(Number(userStore.userInfo.roleId))) {
getTaskList();
}
}
);
onMounted(() => {
getTaskList();
});
5 years ago
</script>