424 lines
14 KiB
Vue
424 lines
14 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onMounted, ref } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { Bell, Operation } from '@element-plus/icons-vue'
|
||
import http from '../api/http'
|
||
import { useAuthStore } from '../stores/auth'
|
||
|
||
const unreadNotifCount = ref(0)
|
||
|
||
async function pollUnread() {
|
||
try {
|
||
const { data } = await http.get('/notifications/unread-count')
|
||
unreadNotifCount.value = data.count
|
||
} catch (_) { /* ignore */ }
|
||
}
|
||
|
||
interface NavigationItem {
|
||
path: string
|
||
label: string
|
||
}
|
||
|
||
interface NavigationGroup {
|
||
key: string
|
||
label: string
|
||
direct?: boolean
|
||
items: NavigationItem[]
|
||
}
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const auth = useAuthStore()
|
||
const mobileMenu = ref(false)
|
||
|
||
const roles = computed(() => auth.user?.roles ?? [])
|
||
const isStudent = computed(() => roles.value.includes('Student'))
|
||
const isTeacher = computed(() => roles.value.includes('Teacher'))
|
||
const isTeachingAdmin = computed(() =>
|
||
roles.value.some((role) => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'].includes(role)),
|
||
)
|
||
const isTimetableManager = computed(() =>
|
||
roles.value.some((role) =>
|
||
['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Leader'].includes(role),
|
||
),
|
||
)
|
||
|
||
const isStatisticsViewer = computed(() =>
|
||
roles.value.some((role) =>
|
||
['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Leader'].includes(role),
|
||
),
|
||
)
|
||
|
||
const canUseExperiments = computed(() =>
|
||
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Teacher', 'Student']),
|
||
)
|
||
|
||
function hasAnyRole(allowedRoles: string[]) {
|
||
return roles.value.some((role) => allowedRoles.includes(role))
|
||
}
|
||
|
||
function whenVisible(visible: boolean, item: NavigationItem) {
|
||
return visible ? [item] : []
|
||
}
|
||
|
||
const navigationGroups = computed<NavigationGroup[]>(() => [
|
||
{
|
||
key: 'overview',
|
||
label: '教务总览',
|
||
direct: true,
|
||
items: [{ path: '/dashboard', label: '教务总览' }],
|
||
},
|
||
...(isStatisticsViewer.value
|
||
? [{
|
||
key: 'statistics',
|
||
label: '统计报表',
|
||
direct: true,
|
||
items: [{ path: '/statistics', label: '统计报表' }],
|
||
} as NavigationGroup]
|
||
: []),
|
||
...(canUseExperiments.value
|
||
? [{
|
||
key: 'experiments',
|
||
label: isStudent.value ? '我的实验' : '实验管理',
|
||
items: [
|
||
{
|
||
path: '/experiments',
|
||
label: isStudent.value ? '实验安排' : '项目与场次',
|
||
},
|
||
{
|
||
path: '/experiment-grades',
|
||
label: '实验成绩',
|
||
},
|
||
],
|
||
} as NavigationGroup]
|
||
: []),
|
||
{
|
||
key: 'organization',
|
||
label: '组织与权限',
|
||
items: [
|
||
...whenVisible(
|
||
hasAnyRole(['SuperAdmin', 'AcademicAdmin']),
|
||
{ path: '/base-data/organization', label: '组织机构' },
|
||
),
|
||
...whenVisible(
|
||
hasAnyRole(['SuperAdmin', 'AcademicAdmin']),
|
||
{ path: '/base-data/terms', label: '学年学期' },
|
||
),
|
||
...whenVisible(
|
||
hasAnyRole(['SuperAdmin', 'AcademicAdmin']),
|
||
{ path: '/base-data/facilities', label: '教学场所' },
|
||
),
|
||
...whenVisible(
|
||
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor']),
|
||
{ path: '/teachers', label: '教师档案' },
|
||
),
|
||
...whenVisible(
|
||
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor']),
|
||
{ path: '/students', label: '学生档案' },
|
||
),
|
||
...whenVisible(auth.isSuperAdmin, { path: '/users', label: '用户与权限' }),
|
||
...whenVisible(auth.isSuperAdmin, { path: '/operations', label: '运维与审计' }),
|
||
],
|
||
},
|
||
{
|
||
key: 'teaching-construction',
|
||
label: '教学建设',
|
||
items: [
|
||
...whenVisible(
|
||
hasAnyRole(['SuperAdmin', 'AcademicAdmin']),
|
||
{ path: '/base-data/course-categories', label: '课程分类' },
|
||
),
|
||
{ path: '/courses', label: '课程库' },
|
||
...whenVisible(
|
||
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Teacher']),
|
||
{
|
||
path: '/teaching-preferences',
|
||
label: isTeacher.value ? '授课科目申报' : '授课资格审核',
|
||
},
|
||
),
|
||
...whenVisible(isTeachingAdmin.value, { path: '/curriculum', label: '培养方案' }),
|
||
...whenVisible(isStudent.value, { path: '/my-curriculum', label: '我的培养方案' }),
|
||
...whenVisible(isTeachingAdmin.value, { path: '/teaching-tasks', label: '教学任务' }),
|
||
],
|
||
},
|
||
{
|
||
key: 'teaching-operation',
|
||
label: '教学运行',
|
||
items: [
|
||
...whenVisible(
|
||
hasAnyRole(['SuperAdmin', 'AcademicAdmin']),
|
||
{ path: '/schedules', label: '排课与课表' },
|
||
),
|
||
...whenVisible(isStudent.value || isTeacher.value, { path: '/my-timetable', label: isTeacher.value ? '我的授课课表' : '我的课表' }),
|
||
...whenVisible(isStudent.value, { path: '/free-classrooms', label: '空闲教室' }),
|
||
{
|
||
path: '/classroom-reservations',
|
||
label: hasAnyRole(['CollegeAdmin']) ? '教室借用审核' : '教室借用',
|
||
},
|
||
...whenVisible(isTeacher.value || isTeachingAdmin.value || hasAnyRole(['Counselor']), { path: '/teacher-attendance', label: '教学点名' }),
|
||
...whenVisible(isStudent.value, { path: '/my-attendance', label: '我的考勤' }),
|
||
{
|
||
path: '/evaluations',
|
||
label: isStudent.value ? '学生评教' : isTeacher.value ? '评教结果' : '教学评价',
|
||
},
|
||
...whenVisible(isTeacher.value, { path: '/teacher-roster', label: '选课名单' }),
|
||
...whenVisible(!isStudent.value, {
|
||
path: '/class-timetable',
|
||
label: isTimetableManager.value ? '课表查询中心' : '班级课表查询',
|
||
}),
|
||
...whenVisible(
|
||
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Student']),
|
||
{
|
||
path: '/course-selections',
|
||
label: isTeachingAdmin.value ? '选课管理' : '学生选课',
|
||
},
|
||
),
|
||
...whenVisible(
|
||
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Teacher', 'Student', 'Counselor']),
|
||
{ path: '/grades', label: isStudent.value ? '学业成绩' : isTeacher.value ? '成绩录入' : '成绩管理' },
|
||
),
|
||
...whenVisible(
|
||
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'Student']),
|
||
{ path: '/other-exams', label: '其他考试成绩' },
|
||
),
|
||
...whenVisible(
|
||
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'Teacher', 'Student']),
|
||
{
|
||
path: '/exams',
|
||
label: isStudent.value ? '我的考试' : isTeacher.value ? '我的监考' : '考试管理',
|
||
},
|
||
),
|
||
...whenVisible(
|
||
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'Teacher', 'Student']),
|
||
{
|
||
path: '/makeup-exams',
|
||
label: isStudent.value ? '我的补考' : isTeacher.value ? '补考监考' : '补考安排',
|
||
},
|
||
),
|
||
...whenVisible(
|
||
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Teacher']),
|
||
{
|
||
path: '/course-adjustments',
|
||
label: isTeacher.value ? '我的调停课' : '调停课审核',
|
||
},
|
||
),
|
||
...whenVisible(
|
||
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor', 'Teacher', 'Student']),
|
||
{ path: '/approvals', label: '审批中心' },
|
||
),
|
||
...whenVisible(
|
||
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'Counselor', 'Student']),
|
||
{ path: '/warnings', label: '学业预警' },
|
||
),
|
||
],
|
||
},
|
||
{
|
||
key: 'student-status',
|
||
label: '学籍管理',
|
||
items: [
|
||
...whenVisible(
|
||
isStudent.value,
|
||
{ path: '/my-profile', label: '个人信息' },
|
||
),
|
||
...whenVisible(
|
||
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor', 'Student']),
|
||
{ path: '/student-status-changes', label: isStudent.value ? '学籍异动' : '异动审核' },
|
||
),
|
||
...whenVisible(
|
||
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Student']),
|
||
{ path: '/official-documents', label: isStudent.value ? '电子成绩单与证明' : '官方凭证签发' },
|
||
),
|
||
],
|
||
},
|
||
{
|
||
key: 'graduation',
|
||
label: '毕业管理',
|
||
items: [
|
||
...whenVisible(
|
||
isStudent.value,
|
||
{ path: '/academic-planning', label: '学业规划与毕业模拟' },
|
||
),
|
||
...whenVisible(
|
||
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Student']),
|
||
{ path: '/graduation-audits', label: isStudent.value ? '毕业资格' : '毕业审核' },
|
||
),
|
||
...whenVisible(
|
||
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Student']),
|
||
{ path: '/degree-awards', label: isStudent.value ? '学位结果' : '学位授予' },
|
||
),
|
||
...whenVisible(
|
||
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor', 'Student']),
|
||
{ path: '/graduation-clearance', label: isStudent.value ? '毕业离校' : '离校办理' },
|
||
),
|
||
],
|
||
},
|
||
].filter((group) => group.items.length > 0))
|
||
|
||
const activeGroupKey = computed(
|
||
() => navigationGroups.value.find((group) =>
|
||
group.items.some((item) => item.path === route.path),
|
||
)?.key ?? '',
|
||
)
|
||
|
||
const workspaceLabel = computed(() => {
|
||
if (roles.value.includes('SuperAdmin')) return '系统全域管理'
|
||
if (roles.value.includes('AcademicAdmin')) return '校级教务管理'
|
||
if (roles.value.includes('CollegeAdmin')) return '学院教务管理'
|
||
if (roles.value.includes('Counselor')) return '辅导员班级工作'
|
||
if (roles.value.includes('Teacher')) return '教师教学工作'
|
||
if (roles.value.includes('Student')) return '学生学业服务'
|
||
return '教务工作台'
|
||
})
|
||
|
||
const pageTitle = computed(() => {
|
||
if (route.path === '/account') return '个人账户'
|
||
const matchedItem = navigationGroups.value
|
||
.flatMap((group) => group.items)
|
||
.find((item) => item.path === route.path)
|
||
return matchedItem?.label ?? '教务管理'
|
||
})
|
||
|
||
function signOut() {
|
||
auth.logout()
|
||
router.push('/login')
|
||
}
|
||
|
||
onMounted(() => {
|
||
auth.refresh().catch(() => undefined)
|
||
pollUnread()
|
||
setInterval(pollUnread, 60000)
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="shell">
|
||
<header class="app-header">
|
||
<div class="topbar">
|
||
<button
|
||
class="menu-toggle mobile-only"
|
||
type="button"
|
||
aria-label="打开导航菜单"
|
||
@click="mobileMenu = true"
|
||
>
|
||
<el-icon><Operation /></el-icon>
|
||
</button>
|
||
|
||
<div class="brand top-brand">
|
||
<div class="brand-mark" aria-hidden="true">
|
||
<span v-for="index in 9" :key="index" />
|
||
</div>
|
||
<div>
|
||
<strong>明序教务</strong>
|
||
<small>ACADEMIC OFFICE</small>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="workspace-context">
|
||
<span>{{ workspaceLabel }}</span>
|
||
<b>{{ pageTitle }}</b>
|
||
</div>
|
||
|
||
<div class="user-block">
|
||
<el-badge :value="unreadNotifCount" :hidden="!unreadNotifCount" :max="99">
|
||
<el-button :icon="Bell" circle text @click="router.push('/notifications')" title="消息中心" />
|
||
</el-badge>
|
||
<div class="avatar">{{ auth.user?.displayName?.slice(0, 1) ?? '管' }}</div>
|
||
<div class="user-copy">
|
||
<b>{{ auth.user?.displayName ?? '系统管理员' }}</b>
|
||
<span>{{ auth.user?.roles?.[0] ?? '教务人员' }}</span>
|
||
</div>
|
||
<el-button text @click="router.push('/account')">个人账户</el-button>
|
||
<el-button text @click="signOut">退出</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<nav class="desktop-navigation desktop-only" aria-label="主导航">
|
||
<el-menu
|
||
router
|
||
mode="horizontal"
|
||
menu-trigger="hover"
|
||
unique-opened
|
||
:default-active="route.path"
|
||
:ellipsis="false"
|
||
class="horizontal-menu"
|
||
>
|
||
<template v-for="group in navigationGroups" :key="group.key">
|
||
<el-menu-item v-if="group.direct" :index="group.items[0].path">
|
||
{{ group.label }}
|
||
</el-menu-item>
|
||
<el-sub-menu
|
||
v-else
|
||
:index="group.key"
|
||
:show-timeout="80"
|
||
:hide-timeout="180"
|
||
>
|
||
<template #title>{{ group.label }}</template>
|
||
<el-menu-item
|
||
v-for="item in group.items"
|
||
:key="item.path"
|
||
:index="item.path"
|
||
>
|
||
{{ item.label }}
|
||
</el-menu-item>
|
||
</el-sub-menu>
|
||
</template>
|
||
</el-menu>
|
||
</nav>
|
||
</header>
|
||
|
||
<aside class="mobile-navigation" :class="{ 'is-mobile-open': mobileMenu }">
|
||
<div class="mobile-navigation-head">
|
||
<div class="brand">
|
||
<div class="brand-mark" aria-hidden="true">
|
||
<span v-for="index in 9" :key="index" />
|
||
</div>
|
||
<div>
|
||
<strong>明序教务</strong>
|
||
<small>ACADEMIC OFFICE</small>
|
||
</div>
|
||
</div>
|
||
<button type="button" aria-label="关闭导航菜单" @click="mobileMenu = false">×</button>
|
||
</div>
|
||
|
||
<div class="term-stamp">
|
||
<span>当前工作区</span>
|
||
<b>{{ workspaceLabel }}</b>
|
||
</div>
|
||
|
||
<el-menu
|
||
router
|
||
:default-active="route.path"
|
||
:default-openeds="activeGroupKey ? [activeGroupKey] : []"
|
||
unique-opened
|
||
class="mobile-nav-menu"
|
||
@select="mobileMenu = false"
|
||
>
|
||
<template v-for="group in navigationGroups" :key="group.key">
|
||
<el-menu-item v-if="group.direct" :index="group.items[0].path">
|
||
{{ group.label }}
|
||
</el-menu-item>
|
||
<el-sub-menu v-else :index="group.key">
|
||
<template #title>{{ group.label }}</template>
|
||
<el-menu-item
|
||
v-for="item in group.items"
|
||
:key="item.path"
|
||
:index="item.path"
|
||
>
|
||
{{ item.label }}
|
||
</el-menu-item>
|
||
</el-sub-menu>
|
||
</template>
|
||
</el-menu>
|
||
|
||
</aside>
|
||
|
||
<div v-if="mobileMenu" class="mobile-mask" @click="mobileMenu = false" />
|
||
|
||
<main class="main-area">
|
||
<section class="content">
|
||
<RouterView />
|
||
</section>
|
||
</main>
|
||
</div>
|
||
</template>
|