修改样式
This commit is contained in:
Vendored
+1
@@ -31,6 +31,7 @@ declare module 'vue' {
|
|||||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
|
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
ElTable: typeof import('element-plus/es')['ElTable']
|
ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||||
|
|||||||
+244
-181
@@ -1,59 +1,151 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import {
|
import { Operation } from '@element-plus/icons-vue'
|
||||||
Collection,
|
|
||||||
DataAnalysis,
|
|
||||||
OfficeBuilding,
|
|
||||||
Operation,
|
|
||||||
Reading,
|
|
||||||
Tickets,
|
|
||||||
Calendar,
|
|
||||||
CircleCheck,
|
|
||||||
DocumentChecked,
|
|
||||||
AlarmClock,
|
|
||||||
RefreshRight,
|
|
||||||
Medal,
|
|
||||||
User,
|
|
||||||
UserFilled,
|
|
||||||
} from '@element-plus/icons-vue'
|
|
||||||
import { useAuthStore } from '../stores/auth'
|
import { useAuthStore } from '../stores/auth'
|
||||||
|
|
||||||
|
interface NavigationItem {
|
||||||
|
path: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NavigationGroup {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
direct?: boolean
|
||||||
|
items: NavigationItem[]
|
||||||
|
}
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const collapsed = ref(false)
|
|
||||||
const mobileMenu = ref(false)
|
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)),
|
||||||
|
)
|
||||||
|
|
||||||
|
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: '教务总览' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'organization',
|
||||||
|
label: '组织与权限',
|
||||||
|
items: [
|
||||||
|
...whenVisible(
|
||||||
|
hasAnyRole(['SuperAdmin', 'AcademicAdmin']),
|
||||||
|
{ path: '/base-data', label: '基础数据' },
|
||||||
|
),
|
||||||
|
...whenVisible(
|
||||||
|
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor']),
|
||||||
|
{ path: '/personnel', label: '人员档案' },
|
||||||
|
),
|
||||||
|
...whenVisible(auth.isSuperAdmin, { path: '/users', label: '用户与权限' }),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'teaching-construction',
|
||||||
|
label: '教学建设',
|
||||||
|
items: [
|
||||||
|
{ path: '/courses', label: '课程库' },
|
||||||
|
...whenVisible(isTeachingAdmin.value, { path: '/curriculum', label: '培养方案' }),
|
||||||
|
...whenVisible(isTeachingAdmin.value, { path: '/teaching-tasks', label: '教学任务' }),
|
||||||
|
...whenVisible(
|
||||||
|
hasAnyRole(['SuperAdmin', 'AcademicAdmin']),
|
||||||
|
{ path: '/schedules', label: '排课与课表' },
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'teaching-operation',
|
||||||
|
label: '教学运行',
|
||||||
|
items: [
|
||||||
|
...whenVisible(
|
||||||
|
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Student']),
|
||||||
|
{
|
||||||
|
path: '/course-selections',
|
||||||
|
label: isTeachingAdmin.value ? '选课管理' : '学生选课',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
...whenVisible(
|
||||||
|
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Teacher', 'Student']),
|
||||||
|
{ path: '/grades', label: isStudent.value ? '学业成绩' : '成绩管理' },
|
||||||
|
),
|
||||||
|
...whenVisible(
|
||||||
|
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'Teacher', 'Student']),
|
||||||
|
{
|
||||||
|
path: '/exams',
|
||||||
|
label: isStudent.value ? '我的考试' : isTeacher.value ? '我的监考' : '考试管理',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'student-status',
|
||||||
|
label: '学籍管理',
|
||||||
|
items: [
|
||||||
|
...whenVisible(
|
||||||
|
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor', 'Student']),
|
||||||
|
{ path: '/student-status-changes', label: isStudent.value ? '学籍异动' : '异动审核' },
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'graduation',
|
||||||
|
label: '毕业管理',
|
||||||
|
items: [
|
||||||
|
...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(() => {
|
const workspaceLabel = computed(() => {
|
||||||
const roles = auth.user?.roles ?? []
|
if (roles.value.includes('SuperAdmin')) return '系统全域管理'
|
||||||
if (roles.includes('SuperAdmin')) return '系统全域管理'
|
if (roles.value.includes('AcademicAdmin')) return '校级教务管理'
|
||||||
if (roles.includes('AcademicAdmin')) return '校级教务管理'
|
if (roles.value.includes('CollegeAdmin')) return '学院教务管理'
|
||||||
if (roles.includes('CollegeAdmin')) return '学院教务管理'
|
if (roles.value.includes('Counselor')) return '辅导员班级工作'
|
||||||
if (roles.includes('Counselor')) return '辅导员班级工作'
|
if (roles.value.includes('Teacher')) return '教师教学工作'
|
||||||
if (roles.includes('Teacher')) return '教师教学工作'
|
if (roles.value.includes('Student')) return '学生学业服务'
|
||||||
if (roles.includes('Student')) return '学生学业服务'
|
|
||||||
return '教务工作台'
|
return '教务工作台'
|
||||||
})
|
})
|
||||||
|
|
||||||
const pageTitle = computed(() => {
|
const pageTitle = computed(() => {
|
||||||
const titles: Record<string, string> = {
|
const matchedItem = navigationGroups.value
|
||||||
dashboard: '教务总览',
|
.flatMap((group) => group.items)
|
||||||
'base-data': '基础数据',
|
.find((item) => item.path === route.path)
|
||||||
personnel: '人员档案',
|
return matchedItem?.label ?? '教务管理'
|
||||||
courses: '课程库',
|
|
||||||
curriculum: '培养方案',
|
|
||||||
'teaching-tasks': '教学任务',
|
|
||||||
schedules: '排课与课表',
|
|
||||||
'course-selections': '选课与教学班',
|
|
||||||
grades: '成绩与学业档案',
|
|
||||||
exams: '考试与考场',
|
|
||||||
'student-status-changes': '学籍异动',
|
|
||||||
'graduation-audits': '毕业资格审核',
|
|
||||||
'degree-awards': '学位授予审核',
|
|
||||||
'graduation-clearance': '毕业离校办理',
|
|
||||||
users: '用户与权限',
|
|
||||||
}
|
|
||||||
return titles[String(route.name)] ?? '教务管理'
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function signOut() {
|
function signOut() {
|
||||||
@@ -65,148 +157,33 @@ onMounted(() => auth.refresh().catch(() => undefined))
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="shell" :class="{ 'is-collapsed': collapsed }">
|
<div class="shell">
|
||||||
<aside class="sidebar" :class="{ 'is-mobile-open': mobileMenu }">
|
<header class="app-header">
|
||||||
<div class="brand">
|
<div class="topbar">
|
||||||
<div class="brand-mark" aria-hidden="true">
|
<button
|
||||||
<span v-for="index in 9" :key="index" />
|
class="menu-toggle mobile-only"
|
||||||
</div>
|
type="button"
|
||||||
<div v-if="!collapsed">
|
aria-label="打开导航菜单"
|
||||||
<strong>明序教务</strong>
|
@click="mobileMenu = true"
|
||||||
<small>ACADEMIC OFFICE</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="!collapsed" class="term-stamp">
|
|
||||||
<span>当前工作区</span>
|
|
||||||
<b>{{ workspaceLabel }}</b>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-menu
|
|
||||||
router
|
|
||||||
:default-active="route.path"
|
|
||||||
:collapse="collapsed"
|
|
||||||
class="nav-menu"
|
|
||||||
@select="mobileMenu = false"
|
|
||||||
>
|
|
||||||
<el-menu-item index="/dashboard">
|
|
||||||
<el-icon><DataAnalysis /></el-icon>
|
|
||||||
<template #title>教务总览</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item
|
|
||||||
v-if="auth.user?.roles.some((role) => ['SuperAdmin', 'AcademicAdmin'].includes(role))"
|
|
||||||
index="/base-data"
|
|
||||||
>
|
>
|
||||||
<el-icon><OfficeBuilding /></el-icon>
|
|
||||||
<template #title>基础数据</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item
|
|
||||||
v-if="auth.user?.roles.some((role) => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor'].includes(role))"
|
|
||||||
index="/personnel"
|
|
||||||
>
|
|
||||||
<el-icon><UserFilled /></el-icon>
|
|
||||||
<template #title>人员档案</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="/courses">
|
|
||||||
<el-icon><Collection /></el-icon>
|
|
||||||
<template #title>课程库</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item
|
|
||||||
v-if="auth.user?.roles.some((role) => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'].includes(role))"
|
|
||||||
index="/curriculum"
|
|
||||||
>
|
|
||||||
<el-icon><Reading /></el-icon>
|
|
||||||
<template #title>培养方案</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item
|
|
||||||
v-if="auth.user?.roles.some((role) => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'].includes(role))"
|
|
||||||
index="/teaching-tasks"
|
|
||||||
>
|
|
||||||
<el-icon><Tickets /></el-icon>
|
|
||||||
<template #title>教学任务</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item
|
|
||||||
v-if="auth.user?.roles.some((role) => ['SuperAdmin', 'AcademicAdmin'].includes(role))"
|
|
||||||
index="/schedules"
|
|
||||||
>
|
|
||||||
<el-icon><Calendar /></el-icon>
|
|
||||||
<template #title>排课与课表</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item
|
|
||||||
v-if="auth.user?.roles.some((role) => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Student'].includes(role))"
|
|
||||||
index="/course-selections"
|
|
||||||
>
|
|
||||||
<el-icon><CircleCheck /></el-icon>
|
|
||||||
<template #title>{{ auth.user?.roles.some((role) => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'].includes(role)) ? '选课管理' : '学生选课' }}</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item
|
|
||||||
v-if="auth.user?.roles.some((role) => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Teacher', 'Student'].includes(role))"
|
|
||||||
index="/grades"
|
|
||||||
>
|
|
||||||
<el-icon><DocumentChecked /></el-icon>
|
|
||||||
<template #title>{{ auth.user?.roles.includes('Student') ? '学业成绩' : '成绩管理' }}</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item
|
|
||||||
v-if="auth.user?.roles.some((role) => ['SuperAdmin', 'AcademicAdmin', 'Teacher', 'Student'].includes(role))"
|
|
||||||
index="/exams"
|
|
||||||
>
|
|
||||||
<el-icon><AlarmClock /></el-icon>
|
|
||||||
<template #title>{{ auth.user?.roles.includes('Student') ? '我的考试' : auth.user?.roles.includes('Teacher') ? '我的监考' : '考试管理' }}</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item
|
|
||||||
v-if="auth.user?.roles.some((role) => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor', 'Student'].includes(role))"
|
|
||||||
index="/student-status-changes"
|
|
||||||
>
|
|
||||||
<el-icon><RefreshRight /></el-icon>
|
|
||||||
<template #title>{{ auth.user?.roles.includes('Student') ? '学籍异动' : '异动审核' }}</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item
|
|
||||||
v-if="auth.user?.roles.some((role) => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Student'].includes(role))"
|
|
||||||
index="/graduation-audits"
|
|
||||||
>
|
|
||||||
<el-icon><Medal /></el-icon>
|
|
||||||
<template #title>{{ auth.user?.roles.includes('Student') ? '毕业资格' : '毕业审核' }}</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item
|
|
||||||
v-if="auth.user?.roles.some((role) => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Student'].includes(role))"
|
|
||||||
index="/degree-awards"
|
|
||||||
>
|
|
||||||
<el-icon><Medal /></el-icon>
|
|
||||||
<template #title>{{ auth.user?.roles.includes('Student') ? '学位结果' : '学位授予' }}</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item
|
|
||||||
v-if="auth.user?.roles.some((role) => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor', 'Student'].includes(role))"
|
|
||||||
index="/graduation-clearance"
|
|
||||||
>
|
|
||||||
<el-icon><CircleCheck /></el-icon>
|
|
||||||
<template #title>{{ auth.user?.roles.includes('Student') ? '毕业离校' : '离校办理' }}</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item v-if="auth.isSuperAdmin" index="/users">
|
|
||||||
<el-icon><User /></el-icon>
|
|
||||||
<template #title>用户与权限</template>
|
|
||||||
</el-menu-item>
|
|
||||||
</el-menu>
|
|
||||||
|
|
||||||
<div v-if="!collapsed" class="phase-note">
|
|
||||||
<span>第一阶段 · 核心可用版</span>
|
|
||||||
<p>教学运行、学籍与毕业审核已就绪</p>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<div v-if="mobileMenu" class="mobile-mask" @click="mobileMenu = false" />
|
|
||||||
|
|
||||||
<main class="main-area">
|
|
||||||
<header class="topbar">
|
|
||||||
<button class="menu-toggle desktop-only" type="button" @click="collapsed = !collapsed">
|
|
||||||
<el-icon><Operation /></el-icon>
|
<el-icon><Operation /></el-icon>
|
||||||
</button>
|
</button>
|
||||||
<button class="menu-toggle mobile-only" type="button" @click="mobileMenu = true">
|
|
||||||
<el-icon><Operation /></el-icon>
|
<div class="brand top-brand">
|
||||||
</button>
|
<div class="brand-mark" aria-hidden="true">
|
||||||
<div class="page-heading">
|
<span v-for="index in 9" :key="index" />
|
||||||
<span>教务工作台</span>
|
</div>
|
||||||
<h1>{{ pageTitle }}</h1>
|
<div>
|
||||||
|
<strong>明序教务</strong>
|
||||||
|
<small>ACADEMIC OFFICE</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="workspace-context">
|
||||||
|
<span>{{ workspaceLabel }}</span>
|
||||||
|
<b>{{ pageTitle }}</b>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="user-block">
|
<div class="user-block">
|
||||||
<div class="avatar">{{ auth.user?.displayName?.slice(0, 1) ?? '管' }}</div>
|
<div class="avatar">{{ auth.user?.displayName?.slice(0, 1) ?? '管' }}</div>
|
||||||
<div class="user-copy">
|
<div class="user-copy">
|
||||||
@@ -215,7 +192,93 @@ onMounted(() => auth.refresh().catch(() => undefined))
|
|||||||
</div>
|
</div>
|
||||||
<el-button text @click="signOut">退出</el-button>
|
<el-button text @click="signOut">退出</el-button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</div>
|
||||||
|
|
||||||
|
<nav class="desktop-navigation desktop-only" aria-label="主导航">
|
||||||
|
<el-menu
|
||||||
|
router
|
||||||
|
mode="horizontal"
|
||||||
|
menu-trigger="click"
|
||||||
|
: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">
|
||||||
|
<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>
|
||||||
|
<div class="navigation-status">
|
||||||
|
<span>CORE SYSTEM</span>
|
||||||
|
<b>核心业务已就绪</b>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<div class="phase-note">
|
||||||
|
<span>第一阶段 · 核心可用版</span>
|
||||||
|
<p>教学运行、学籍与毕业审核已就绪</p>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div v-if="mobileMenu" class="mobile-mask" @click="mobileMenu = false" />
|
||||||
|
|
||||||
|
<main class="main-area">
|
||||||
<section class="content">
|
<section class="content">
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
+102
-34
@@ -24,49 +24,112 @@ button, input, textarea, select { font: inherit; }
|
|||||||
button { cursor: pointer; }
|
button { cursor: pointer; }
|
||||||
#app { min-height: 100vh; }
|
#app { min-height: 100vh; }
|
||||||
|
|
||||||
.shell { min-height: 100vh; display: grid; grid-template-columns: 256px 1fr; transition: grid-template-columns .2s ease; }
|
.shell { min-height: 100vh; }
|
||||||
.shell.is-collapsed { grid-template-columns: 72px 1fr; }
|
.app-header {
|
||||||
.sidebar {
|
position: sticky; top: 0; z-index: 20;
|
||||||
position: fixed; inset: 0 auto 0 0; width: 256px; z-index: 20; overflow: hidden;
|
box-shadow: 0 5px 18px rgba(19,34,73,.08);
|
||||||
display: flex; flex-direction: column; color: #eef2ff;
|
|
||||||
background:
|
|
||||||
linear-gradient(rgba(255,255,255,.025) 1px, transparent 1px),
|
|
||||||
linear-gradient(90deg, rgba(255,255,255,.025) 1px, transparent 1px),
|
|
||||||
var(--indigo-deep);
|
|
||||||
background-size: 26px 26px;
|
|
||||||
transition: width .2s ease, transform .25s ease;
|
|
||||||
}
|
}
|
||||||
.is-collapsed .sidebar { width: 72px; }
|
.topbar {
|
||||||
.brand { height: 86px; padding: 0 24px; display: flex; align-items: center; gap: 13px; border-bottom: 1px solid rgba(255,255,255,.1); white-space: nowrap; }
|
height: 70px; padding: 0 32px; display: flex; align-items: center; gap: 24px;
|
||||||
.is-collapsed .brand { padding: 0 17px; }
|
color: white; background:
|
||||||
|
linear-gradient(90deg, rgba(255,255,255,.025) 1px, transparent 1px),
|
||||||
|
linear-gradient(112deg, #152550 0%, #1d326c 72%, #176a70 130%);
|
||||||
|
background-size: 28px 28px, auto;
|
||||||
|
}
|
||||||
|
.brand {
|
||||||
|
height: 70px; display: flex; align-items: center; gap: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.top-brand { min-width: 224px; padding-right: 28px; border-right: 1px solid rgba(255,255,255,.14); }
|
||||||
.brand strong { display: block; font-family: "STZhongsong", "Songti SC", serif; font-size: 21px; letter-spacing: .12em; color: white; }
|
.brand strong { display: block; font-family: "STZhongsong", "Songti SC", serif; font-size: 21px; letter-spacing: .12em; color: white; }
|
||||||
.brand small { display: block; margin-top: 3px; font: 9px/1.2 Consolas, monospace; letter-spacing: .16em; color: #9faad1; }
|
.brand small { display: block; margin-top: 3px; font: 9px/1.2 Consolas, monospace; letter-spacing: .16em; color: #9faad1; }
|
||||||
.brand-mark { flex: 0 0 auto; width: 31px; height: 31px; display: grid; grid-template-columns: repeat(3,1fr); gap: 3px; }
|
.brand-mark { flex: 0 0 auto; width: 31px; height: 31px; display: grid; grid-template-columns: repeat(3,1fr); gap: 3px; }
|
||||||
.brand-mark span { border: 1px solid #9eabd8; }
|
.brand-mark span { border: 1px solid #9eabd8; }
|
||||||
.brand-mark span:nth-child(2), .brand-mark span:nth-child(5), .brand-mark span:nth-child(8) { background: #3ec1ae; border-color: #3ec1ae; }
|
.brand-mark span:nth-child(2), .brand-mark span:nth-child(5), .brand-mark span:nth-child(8) { background: #3ec1ae; border-color: #3ec1ae; }
|
||||||
.brand-mark.large { width: 42px; height: 42px; gap: 4px; }
|
.brand-mark.large { width: 42px; height: 42px; gap: 4px; }
|
||||||
|
.workspace-context { min-width: 180px; display: grid; gap: 4px; }
|
||||||
|
.workspace-context span { color: #aeb9d8; font-size: 10px; letter-spacing: .08em; }
|
||||||
|
.workspace-context b { font-size: 14px; font-weight: 550; letter-spacing: .04em; }
|
||||||
|
.user-block { margin-left: auto; display: flex; align-items: center; gap: 11px; }
|
||||||
|
.avatar { width: 36px; height: 36px; display: grid; place-items: center; border-radius: 50%; color: #123b3e; background: #59d3c1; font-weight: 750; }
|
||||||
|
.user-copy b, .user-copy span { display: block; }
|
||||||
|
.user-copy b { color: white; font-size: 13px; }
|
||||||
|
.user-copy span { margin-top: 3px; color: #aeb9d8; font-size: 10px; }
|
||||||
|
.topbar .user-block .el-button { color: #d4daf0; }
|
||||||
|
.topbar .user-block .el-button:hover { color: white; background: rgba(255,255,255,.08); }
|
||||||
|
.desktop-navigation {
|
||||||
|
height: 51px; padding: 0 32px; display: flex; align-items: stretch;
|
||||||
|
border-bottom: 1px solid var(--line); background: rgba(255,255,255,.98);
|
||||||
|
}
|
||||||
|
.horizontal-menu.el-menu {
|
||||||
|
flex: 1; min-width: 0; height: 50px; border-bottom: none;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.horizontal-menu.el-menu--horizontal > .el-menu-item,
|
||||||
|
.horizontal-menu.el-menu--horizontal > .el-sub-menu .el-sub-menu__title {
|
||||||
|
height: 50px; padding: 0 20px; border-bottom: 3px solid transparent;
|
||||||
|
color: #333d50; font-size: 13px;
|
||||||
|
}
|
||||||
|
.horizontal-menu.el-menu--horizontal > .el-menu-item:hover,
|
||||||
|
.horizontal-menu.el-menu--horizontal > .el-sub-menu .el-sub-menu__title:hover {
|
||||||
|
color: var(--indigo); background: #f6f8fb;
|
||||||
|
}
|
||||||
|
.horizontal-menu.el-menu--horizontal > .el-menu-item.is-active,
|
||||||
|
.horizontal-menu.el-menu--horizontal > .el-sub-menu.is-active .el-sub-menu__title {
|
||||||
|
color: var(--indigo); border-bottom-color: var(--teal);
|
||||||
|
background: linear-gradient(180deg, #fff, #f5faf9);
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
.horizontal-menu .el-sub-menu__icon-arrow { margin-left: 6px; }
|
||||||
|
.navigation-status {
|
||||||
|
flex: 0 0 auto; min-width: 152px; padding-left: 24px;
|
||||||
|
display: grid; align-content: center; gap: 2px;
|
||||||
|
border-left: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.navigation-status span { color: var(--teal); font: 700 8px/1.2 Consolas, monospace; letter-spacing: .13em; }
|
||||||
|
.navigation-status b { color: #677083; font-size: 10px; font-weight: 500; }
|
||||||
|
.mobile-navigation {
|
||||||
|
position: fixed; inset: 0 auto 0 0; z-index: 30; width: 286px;
|
||||||
|
display: flex; flex-direction: column; overflow-y: auto;
|
||||||
|
color: #eef2ff; background:
|
||||||
|
linear-gradient(rgba(255,255,255,.025) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(255,255,255,.025) 1px, transparent 1px),
|
||||||
|
var(--indigo-deep);
|
||||||
|
background-size: 26px 26px;
|
||||||
|
transform: translateX(-100%); transition: transform .25s ease;
|
||||||
|
}
|
||||||
|
.mobile-navigation.is-mobile-open { transform: translateX(0); }
|
||||||
|
.mobile-navigation-head {
|
||||||
|
min-height: 70px; padding: 0 18px 0 20px; display: flex; align-items: center;
|
||||||
|
justify-content: space-between; border-bottom: 1px solid rgba(255,255,255,.1);
|
||||||
|
}
|
||||||
|
.mobile-navigation-head button {
|
||||||
|
width: 34px; height: 34px; border: 1px solid rgba(255,255,255,.17);
|
||||||
|
border-radius: 6px; color: #cbd3ea; background: rgba(255,255,255,.05);
|
||||||
|
font-size: 23px; line-height: 1;
|
||||||
|
}
|
||||||
.term-stamp { margin: 24px 20px 11px; padding: 13px 15px; border-left: 2px solid #d89b42; background: rgba(255,255,255,.06); }
|
.term-stamp { margin: 24px 20px 11px; padding: 13px 15px; border-left: 2px solid #d89b42; background: rgba(255,255,255,.06); }
|
||||||
.term-stamp span { display: block; font-size: 11px; color: #aab3d4; }
|
.term-stamp span { display: block; font-size: 11px; color: #aab3d4; }
|
||||||
.term-stamp b { display: block; margin-top: 5px; font-size: 13px; font-weight: 500; color: white; }
|
.term-stamp b { display: block; margin-top: 5px; font-size: 13px; font-weight: 500; color: white; }
|
||||||
.nav-menu.el-menu { border: none; background: transparent; padding: 8px 10px; }
|
.mobile-nav-menu.el-menu { border: none; background: transparent; padding: 8px 10px; }
|
||||||
.nav-menu .el-menu-item { height: 48px; margin: 4px 0; border-radius: 7px; color: #bbc4e2; }
|
.mobile-nav-menu .el-menu-item,
|
||||||
.nav-menu .el-menu-item:hover { color: white; background: rgba(255,255,255,.07); }
|
.mobile-nav-menu .el-sub-menu__title {
|
||||||
.nav-menu .el-menu-item.is-active { color: white; background: #2d478d; box-shadow: inset 3px 0 #46c6b5; }
|
height: 45px; margin: 2px 0; border-radius: 6px; color: #bbc4e2; background: transparent;
|
||||||
.nav-menu .el-icon { font-size: 18px; }
|
}
|
||||||
|
.mobile-nav-menu .el-menu-item:hover,
|
||||||
|
.mobile-nav-menu .el-sub-menu__title:hover { color: white; background: rgba(255,255,255,.07); }
|
||||||
|
.mobile-nav-menu .el-menu-item.is-active { color: white; background: #2d478d; box-shadow: inset 3px 0 #46c6b5; }
|
||||||
|
.mobile-nav-menu .el-sub-menu .el-menu { background: rgba(0,0,0,.1); }
|
||||||
|
.mobile-nav-menu .el-sub-menu .el-menu-item { min-width: 0; padding-left: 40px !important; font-size: 12px; }
|
||||||
.phase-note { margin: auto 20px 24px; padding-top: 17px; border-top: 1px solid rgba(255,255,255,.12); }
|
.phase-note { margin: auto 20px 24px; padding-top: 17px; border-top: 1px solid rgba(255,255,255,.12); }
|
||||||
.phase-note span { color: #45c6b5; font-size: 11px; font-weight: 700; letter-spacing: .06em; }
|
.phase-note span { color: #45c6b5; font-size: 11px; font-weight: 700; letter-spacing: .06em; }
|
||||||
.phase-note p { margin: 7px 0 0; color: #9faad0; font-size: 12px; line-height: 1.6; }
|
.phase-note p { margin: 7px 0 0; color: #9faad0; font-size: 12px; line-height: 1.6; }
|
||||||
|
|
||||||
.main-area { grid-column: 2; min-width: 0; }
|
.main-area { min-width: 0; }
|
||||||
.topbar { height: 86px; padding: 0 32px; display: flex; align-items: center; gap: 20px; background: rgba(255,255,255,.96); border-bottom: 1px solid var(--line); position: sticky; top: 0; z-index: 10; backdrop-filter: blur(10px); }
|
.menu-toggle {
|
||||||
.menu-toggle { width: 38px; height: 38px; border: 1px solid var(--line); border-radius: 7px; color: var(--indigo); background: white; }
|
width: 38px; height: 38px; border: 1px solid rgba(255,255,255,.18);
|
||||||
.page-heading span { display: block; color: #9aa1af; font-size: 10px; letter-spacing: .14em; text-transform: uppercase; }
|
border-radius: 7px; color: white; background: rgba(255,255,255,.07);
|
||||||
.page-heading h1 { margin: 3px 0 0; font-size: 20px; font-weight: 650; letter-spacing: .04em; }
|
}
|
||||||
.user-block { margin-left: auto; display: flex; align-items: center; gap: 11px; }
|
|
||||||
.avatar { width: 38px; height: 38px; display: grid; place-items: center; border-radius: 50%; color: white; background: var(--teal); font-weight: 700; }
|
|
||||||
.user-copy b, .user-copy span { display: block; }
|
|
||||||
.user-copy b { font-size: 13px; }
|
|
||||||
.user-copy span { margin-top: 3px; color: var(--muted); font-size: 11px; }
|
|
||||||
.content { padding: 28px 32px 48px; max-width: 1540px; margin: 0 auto; }
|
.content { padding: 28px 32px 48px; max-width: 1540px; margin: 0 auto; }
|
||||||
.mobile-only { display: none; }
|
.mobile-only { display: none; }
|
||||||
|
|
||||||
@@ -795,15 +858,20 @@ button { cursor: pointer; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 980px) {
|
@media (max-width: 980px) {
|
||||||
.shell, .shell.is-collapsed { display: block; }
|
.app-header { box-shadow: 0 3px 14px rgba(19,34,73,.1); }
|
||||||
.sidebar, .is-collapsed .sidebar { width: 256px; transform: translateX(-100%); }
|
|
||||||
.sidebar.is-mobile-open { transform: translateX(0); }
|
|
||||||
.main-area { width: 100%; }
|
.main-area { width: 100%; }
|
||||||
.mobile-mask { position: fixed; inset: 0; z-index: 15; background: rgba(13,24,48,.45); }
|
.mobile-mask { position: fixed; inset: 0; z-index: 25; background: rgba(13,24,48,.52); }
|
||||||
.desktop-only { display: none; }
|
.desktop-only { display: none; }
|
||||||
.mobile-only { display: inline-grid; place-items: center; }
|
.mobile-only { display: inline-grid; place-items: center; }
|
||||||
.topbar { height: 72px; padding: 0 16px; }
|
.topbar { height: 66px; padding: 0 16px; gap: 13px; }
|
||||||
|
.top-brand { min-width: 0; padding-right: 0; border-right: none; }
|
||||||
|
.top-brand .brand-mark { width: 27px; height: 27px; }
|
||||||
|
.top-brand strong { font-size: 17px; }
|
||||||
|
.top-brand small { display: none; }
|
||||||
|
.workspace-context { display: none; }
|
||||||
.user-copy { display: none; }
|
.user-copy { display: none; }
|
||||||
|
.avatar { width: 32px; height: 32px; font-size: 12px; }
|
||||||
|
.topbar .user-block { gap: 4px; }
|
||||||
.content { padding: 18px 14px 35px; }
|
.content { padding: 18px 14px 35px; }
|
||||||
.term-hero { min-height: 220px; padding: 25px; flex-direction: column; align-items: flex-start; justify-content: flex-end; }
|
.term-hero { min-height: 220px; padding: 25px; flex-direction: column; align-items: flex-start; justify-content: flex-end; }
|
||||||
.term-hero button { margin: 22px 0 0; width: 100%; }
|
.term-hero button { margin: 22px 0 0; width: 100%; }
|
||||||
|
|||||||
Reference in New Issue
Block a user