feat(frontend): 响应式布局与移动端适配优化
1) 新增 useBreakpoints 统一断点管理;2) 管理/教师/园校/家长端布局支持移动端抽屉菜单与顶部导航;3) 全局 html/body/#app overflow 与 safe-area 处理,避免横向滚动和刘海遮挡;4) 各端内容区仅内部滚动,提升大屏与小屏的浏览体验 Made-with: Cursor
This commit is contained in:
parent
1b566be4dc
commit
31d4ed76f0
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<title>幼儿阅读教学服务平台</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@ -12,16 +12,26 @@ const AConfigProvider = ConfigProvider;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
/* 安全区域:刘海屏/横屏时留出边距 */
|
||||
padding-left: env(safe-area-inset-left);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
}
|
||||
|
||||
#app {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue',
|
||||
Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
44
reading-platform-frontend/src/components.d.ts
vendored
44
reading-platform-frontend/src/components.d.ts
vendored
@ -7,75 +7,31 @@ export {}
|
||||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AAlert: typeof import('ant-design-vue/es')['Alert']
|
||||
AAvatar: typeof import('ant-design-vue/es')['Avatar']
|
||||
ABadge: typeof import('ant-design-vue/es')['Badge']
|
||||
AButton: typeof import('ant-design-vue/es')['Button']
|
||||
AButtonGroup: typeof import('ant-design-vue/es')['ButtonGroup']
|
||||
ACard: typeof import('ant-design-vue/es')['Card']
|
||||
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
|
||||
ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup']
|
||||
ACol: typeof import('ant-design-vue/es')['Col']
|
||||
ACollapse: typeof import('ant-design-vue/es')['Collapse']
|
||||
ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel']
|
||||
ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
|
||||
ADescriptions: typeof import('ant-design-vue/es')['Descriptions']
|
||||
ADescriptionsItem: typeof import('ant-design-vue/es')['DescriptionsItem']
|
||||
ADivider: typeof import('ant-design-vue/es')['Divider']
|
||||
ADrawer: typeof import('ant-design-vue/es')['Drawer']
|
||||
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
|
||||
AEmpty: typeof import('ant-design-vue/es')['Empty']
|
||||
AForm: typeof import('ant-design-vue/es')['Form']
|
||||
AFormItem: typeof import('ant-design-vue/es')['FormItem']
|
||||
AImage: typeof import('ant-design-vue/es')['Image']
|
||||
AImagePreviewGroup: typeof import('ant-design-vue/es')['ImagePreviewGroup']
|
||||
AInput: typeof import('ant-design-vue/es')['Input']
|
||||
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
|
||||
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
|
||||
AInputSearch: typeof import('ant-design-vue/es')['InputSearch']
|
||||
ALayout: typeof import('ant-design-vue/es')['Layout']
|
||||
ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
|
||||
ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader']
|
||||
ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider']
|
||||
AList: typeof import('ant-design-vue/es')['List']
|
||||
AListItem: typeof import('ant-design-vue/es')['ListItem']
|
||||
AListItemMeta: typeof import('ant-design-vue/es')['ListItemMeta']
|
||||
AMenu: typeof import('ant-design-vue/es')['Menu']
|
||||
AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider']
|
||||
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
|
||||
AModal: typeof import('ant-design-vue/es')['Modal']
|
||||
APageHeader: typeof import('ant-design-vue/es')['PageHeader']
|
||||
APagination: typeof import('ant-design-vue/es')['Pagination']
|
||||
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
|
||||
AProgress: typeof import('ant-design-vue/es')['Progress']
|
||||
ARadio: typeof import('ant-design-vue/es')['Radio']
|
||||
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
|
||||
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
|
||||
ARangePicker: typeof import('ant-design-vue/es')['RangePicker']
|
||||
ARate: typeof import('ant-design-vue/es')['Rate']
|
||||
AResult: typeof import('ant-design-vue/es')['Result']
|
||||
ARow: typeof import('ant-design-vue/es')['Row']
|
||||
ASelect: typeof import('ant-design-vue/es')['Select']
|
||||
ASelectOptGroup: typeof import('ant-design-vue/es')['SelectOptGroup']
|
||||
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
|
||||
ASkeleton: typeof import('ant-design-vue/es')['Skeleton']
|
||||
ASpace: typeof import('ant-design-vue/es')['Space']
|
||||
ASpin: typeof import('ant-design-vue/es')['Spin']
|
||||
AStatistic: typeof import('ant-design-vue/es')['Statistic']
|
||||
AStep: typeof import('ant-design-vue/es')['Step']
|
||||
ASteps: typeof import('ant-design-vue/es')['Steps']
|
||||
ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
|
||||
ASwitch: typeof import('ant-design-vue/es')['Switch']
|
||||
ATable: typeof import('ant-design-vue/es')['Table']
|
||||
ATabPane: typeof import('ant-design-vue/es')['TabPane']
|
||||
ATabs: typeof import('ant-design-vue/es')['Tabs']
|
||||
ATag: typeof import('ant-design-vue/es')['Tag']
|
||||
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
||||
ATimeRangePicker: typeof import('ant-design-vue/es')['TimeRangePicker']
|
||||
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
||||
ATypographyText: typeof import('ant-design-vue/es')['TypographyText']
|
||||
AUpload: typeof import('ant-design-vue/es')['Upload']
|
||||
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
|
||||
FilePreviewModal: typeof import('./components/FilePreviewModal.vue')['default']
|
||||
FileUploader: typeof import('./components/course/FileUploader.vue')['default']
|
||||
LessonConfigPanel: typeof import('./components/course/LessonConfigPanel.vue')['default']
|
||||
|
||||
47
reading-platform-frontend/src/composables/useBreakpoints.ts
Normal file
47
reading-platform-frontend/src/composables/useBreakpoints.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
|
||||
/** 统一断点(与 CSS 媒体查询一致) */
|
||||
export const BREAKPOINTS = {
|
||||
/** 手机竖屏 */
|
||||
MOBILE: 768,
|
||||
/** 平板 / 小屏 */
|
||||
TABLET: 1024,
|
||||
/** 桌面 */
|
||||
DESKTOP: 1200,
|
||||
} as const;
|
||||
|
||||
export type BreakpointKey = keyof typeof BREAKPOINTS;
|
||||
|
||||
/**
|
||||
* 响应式断点:根据窗口宽度判断 isMobile / isTablet / isDesktop,
|
||||
* 并随 resize 更新。全项目统一使用 768 为移动端分界。
|
||||
*/
|
||||
export function useBreakpoints() {
|
||||
const width = ref(typeof window !== 'undefined' ? window.innerWidth : 1024);
|
||||
|
||||
const isMobile = ref(width.value < BREAKPOINTS.MOBILE);
|
||||
const isTablet = ref(
|
||||
width.value >= BREAKPOINTS.MOBILE && width.value < BREAKPOINTS.TABLET
|
||||
);
|
||||
const isDesktop = ref(width.value >= BREAKPOINTS.TABLET);
|
||||
|
||||
const update = () => {
|
||||
if (typeof window === 'undefined') return;
|
||||
const w = window.innerWidth;
|
||||
width.value = w;
|
||||
isMobile.value = w < BREAKPOINTS.MOBILE;
|
||||
isTablet.value = w >= BREAKPOINTS.MOBILE && w < BREAKPOINTS.TABLET;
|
||||
isDesktop.value = w >= BREAKPOINTS.TABLET;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
update();
|
||||
window.addEventListener('resize', update);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', update);
|
||||
});
|
||||
|
||||
return { width, isMobile, isTablet, isDesktop, BREAKPOINTS };
|
||||
}
|
||||
@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<a-layout class="admin-layout">
|
||||
<!-- 桌面端侧边栏 -->
|
||||
<a-layout-sider
|
||||
v-if="!isMobile"
|
||||
v-model:collapsed="collapsed"
|
||||
:trigger="null"
|
||||
collapsible
|
||||
@ -75,8 +77,63 @@
|
||||
</div>
|
||||
</a-layout-sider>
|
||||
|
||||
<!-- 移动端抽屉菜单 -->
|
||||
<a-drawer
|
||||
v-if="isMobile"
|
||||
v-model:open="drawerVisible"
|
||||
placement="left"
|
||||
:closable="false"
|
||||
:width="280"
|
||||
class="admin-drawer"
|
||||
:body-style="{ padding: 0 }"
|
||||
>
|
||||
<template #title>
|
||||
<div class="drawer-header">
|
||||
<img src="/logo.png" alt="Logo" class="drawer-logo" />
|
||||
<span class="drawer-title">服务管理后台</span>
|
||||
</div>
|
||||
</template>
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
mode="inline"
|
||||
theme="light"
|
||||
@select="handleDrawerMenuSelect"
|
||||
class="drawer-menu"
|
||||
>
|
||||
<a-menu-item key="dashboard">
|
||||
<template #icon><LayoutDashboard :size="18" :stroke-width="1.5" /></template>
|
||||
<span>数据看板</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="courses">
|
||||
<template #icon><BookOpen :size="18" :stroke-width="1.5" /></template>
|
||||
<span>课程包管理</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="packages">
|
||||
<template #icon><DatabaseOutlined /></template>
|
||||
<span>套餐管理</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="themes">
|
||||
<template #icon><FormatPainterOutlined /></template>
|
||||
<span>主题字典</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="tenants">
|
||||
<template #icon><Building2 :size="18" :stroke-width="1.5" /></template>
|
||||
<span>租户管理</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="resources">
|
||||
<template #icon><FolderOpen :size="18" :stroke-width="1.5" /></template>
|
||||
<span>资源库</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="settings">
|
||||
<template #icon><Settings :size="18" :stroke-width="1.5" /></template>
|
||||
<span>系统设置</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-drawer>
|
||||
|
||||
<a-layout class="admin-layout-right">
|
||||
<a-layout-header class="admin-header">
|
||||
<!-- 桌面端顶部 -->
|
||||
<a-layout-header v-if="!isMobile" class="admin-header">
|
||||
<div class="header-left">
|
||||
<MenuUnfoldOutlined
|
||||
v-if="collapsed"
|
||||
@ -122,7 +179,27 @@
|
||||
</div>
|
||||
</a-layout-header>
|
||||
|
||||
<a-layout-content class="admin-content">
|
||||
<!-- 移动端顶部 -->
|
||||
<a-layout-header v-if="isMobile" class="admin-mobile-header">
|
||||
<MenuOutlined class="menu-trigger" @click="drawerVisible = true" />
|
||||
<span class="mobile-title">少儿智慧阅读</span>
|
||||
<a-dropdown>
|
||||
<a-space class="user-info" style="cursor: pointer;">
|
||||
<a-avatar :size="32" class="user-avatar">
|
||||
<template #icon><UserOutlined /></template>
|
||||
</a-avatar>
|
||||
</a-space>
|
||||
<template #overlay>
|
||||
<a-menu @click="handleUserMenuClick">
|
||||
<a-menu-item key="profile"><UserOutlined /> 个人信息</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item key="logout"><LogoutOutlined /> 退出登录</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-layout-header>
|
||||
|
||||
<a-layout-content :class="['admin-content', { 'admin-content-mobile': isMobile }]">
|
||||
<router-view />
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
@ -135,6 +212,7 @@ import { useRouter, useRoute } from 'vue-router';
|
||||
import {
|
||||
MenuUnfoldOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuOutlined,
|
||||
BellOutlined,
|
||||
UserOutlined,
|
||||
DownOutlined,
|
||||
@ -150,13 +228,20 @@ import {
|
||||
} from 'lucide-vue-next';
|
||||
import { DatabaseOutlined, FormatPainterOutlined } from '@ant-design/icons-vue';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { useBreakpoints } from '@/composables/useBreakpoints';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const userStore = useUserStore();
|
||||
const { isMobile } = useBreakpoints();
|
||||
|
||||
const collapsed = ref(false);
|
||||
const selectedKeys = ref<string[]>(['dashboard']);
|
||||
const drawerVisible = ref(false);
|
||||
|
||||
watch(isMobile, (mobile) => {
|
||||
if (!mobile) drawerVisible.value = false;
|
||||
});
|
||||
|
||||
// 根据路由设置选中的菜单
|
||||
watch(
|
||||
@ -181,9 +266,7 @@ watch(
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const handleMenuSelect = ({ key }: { key: string | number }) => {
|
||||
const keyStr = String(key);
|
||||
const routeMap: Record<string, string> = {
|
||||
const routeMap: Record<string, string> = {
|
||||
dashboard: '/admin/dashboard',
|
||||
courses: '/admin/courses',
|
||||
packages: '/admin/packages',
|
||||
@ -191,10 +274,17 @@ const handleMenuSelect = ({ key }: { key: string | number }) => {
|
||||
tenants: '/admin/tenants',
|
||||
resources: '/admin/resources',
|
||||
settings: '/admin/settings',
|
||||
};
|
||||
};
|
||||
|
||||
if (routeMap[keyStr]) {
|
||||
router.push(routeMap[keyStr]);
|
||||
const handleMenuSelect = ({ key }: { key: string | number }) => {
|
||||
const keyStr = String(key);
|
||||
if (routeMap[keyStr]) router.push(routeMap[keyStr]);
|
||||
};
|
||||
|
||||
const handleDrawerMenuSelect = ({ key }: { key: string | number }) => {
|
||||
if (routeMap[String(key)]) {
|
||||
router.push(routeMap[String(key)]);
|
||||
drawerVisible.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
@ -395,5 +485,76 @@ $bg-dark: #111827;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.admin-content-mobile {
|
||||
margin: 12px;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
// 移动端抽屉与顶部
|
||||
.admin-drawer {
|
||||
.drawer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
.drawer-logo {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.drawer-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: $text-color;
|
||||
}
|
||||
}
|
||||
.drawer-menu {
|
||||
border-right: none !important;
|
||||
padding: 8px 0;
|
||||
:deep(.ant-menu-item) {
|
||||
margin: 4px 8px;
|
||||
border-radius: 8px;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.admin-mobile-header {
|
||||
flex-shrink: 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
background: white;
|
||||
padding: 0 16px;
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
border-bottom: 1px solid $border-color;
|
||||
.menu-trigger {
|
||||
font-size: 22px;
|
||||
color: $text-secondary;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.mobile-title {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
color: $text-color;
|
||||
}
|
||||
.user-avatar {
|
||||
background: linear-gradient(135deg, $primary-color 0%, $primary-dark 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.admin-layout :deep(.ant-layout-sider) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -178,7 +178,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, onUnmounted, shallowRef } from 'vue';
|
||||
import { ref, computed, watch, shallowRef } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { message } from 'ant-design-vue';
|
||||
import {
|
||||
@ -196,15 +196,16 @@ import {
|
||||
MenuOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { useBreakpoints } from '@/composables/useBreakpoints';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const userStore = useUserStore();
|
||||
const { isMobile } = useBreakpoints();
|
||||
|
||||
const collapsed = ref(false);
|
||||
const selectedKeys = ref(['dashboard']);
|
||||
const notifications = ref(0);
|
||||
const isMobile = ref(false);
|
||||
const drawerVisible = ref(false);
|
||||
|
||||
const userName = computed(() => userStore.user?.name || '家长');
|
||||
@ -232,13 +233,9 @@ const navItems = [
|
||||
{ key: 'children', icon: shallowRef(TeamOutlined), text: '孩子' },
|
||||
];
|
||||
|
||||
// 检测屏幕宽度
|
||||
const checkMobile = () => {
|
||||
isMobile.value = window.innerWidth < 768;
|
||||
if (!isMobile.value) {
|
||||
drawerVisible.value = false;
|
||||
}
|
||||
};
|
||||
watch(isMobile, (mobile) => {
|
||||
if (!mobile) drawerVisible.value = false;
|
||||
});
|
||||
|
||||
// 根据路由设置选中的菜单
|
||||
watch(
|
||||
@ -286,14 +283,6 @@ const handleLogout = () => {
|
||||
router.push('/login');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
checkMobile();
|
||||
window.addEventListener('resize', checkMobile);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', checkMobile);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<a-layout class="school-layout">
|
||||
<a-layout-sider
|
||||
v-if="!isMobile"
|
||||
v-model:collapsed="collapsed"
|
||||
:trigger="null"
|
||||
collapsible
|
||||
@ -118,8 +119,58 @@
|
||||
</div>
|
||||
</a-layout-sider>
|
||||
|
||||
<a-drawer
|
||||
v-if="isMobile"
|
||||
v-model:open="drawerVisible"
|
||||
placement="left"
|
||||
:closable="false"
|
||||
:width="280"
|
||||
class="school-drawer"
|
||||
:body-style="{ padding: 0 }"
|
||||
>
|
||||
<template #title>
|
||||
<div class="drawer-header">
|
||||
<img src="/logo.png" alt="Logo" class="drawer-logo" />
|
||||
<span class="drawer-title">管理后台</span>
|
||||
</div>
|
||||
</template>
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
v-model:openKeys="openKeys"
|
||||
mode="inline"
|
||||
theme="light"
|
||||
@click="handleDrawerMenuClick"
|
||||
class="drawer-menu"
|
||||
>
|
||||
<a-menu-item key="dashboard"><template #icon><DashboardOutlined /></template><span>数据概览</span></a-menu-item>
|
||||
<a-sub-menu key="staff"><template #icon><TeamOutlined /></template><template #title>人员管理</template>
|
||||
<a-menu-item key="teachers"><template #icon><SolutionOutlined /></template><span>教师管理</span></a-menu-item>
|
||||
<a-menu-item key="students"><template #icon><UserOutlined /></template><span>学生管理</span></a-menu-item>
|
||||
<a-menu-item key="parents"><template #icon><IdcardOutlined /></template><span>家长管理</span></a-menu-item>
|
||||
<a-menu-item key="classes"><template #icon><HomeOutlined /></template><span>班级管理</span></a-menu-item>
|
||||
</a-sub-menu>
|
||||
<a-sub-menu key="teaching"><template #icon><BookOutlined /></template><template #title>教学管理</template>
|
||||
<a-menu-item key="courses"><template #icon><ReadOutlined /></template><span>课程管理</span></a-menu-item>
|
||||
<a-menu-item key="school-courses"><template #icon><FolderAddOutlined /></template><span>校本课程包</span></a-menu-item>
|
||||
<a-menu-item key="schedule"><template #icon><CalendarOutlined /></template><span>课程排期</span></a-menu-item>
|
||||
<a-menu-item key="tasks"><template #icon><CheckSquareOutlined /></template><span>阅读任务</span></a-menu-item>
|
||||
<a-menu-item key="task-templates"><template #icon><CopyOutlined /></template><span>任务模板</span></a-menu-item>
|
||||
<a-menu-item key="feedback"><template #icon><MessageOutlined /></template><span>课程反馈</span></a-menu-item>
|
||||
</a-sub-menu>
|
||||
<a-sub-menu key="data"><template #icon><BarChartOutlined /></template><template #title>数据中心</template>
|
||||
<a-menu-item key="reports"><template #icon><FileTextOutlined /></template><span>数据报告</span></a-menu-item>
|
||||
<a-menu-item key="growth"><template #icon><FileImageOutlined /></template><span>成长档案</span></a-menu-item>
|
||||
</a-sub-menu>
|
||||
<a-sub-menu key="system"><template #icon><SettingOutlined /></template><template #title>系统管理</template>
|
||||
<a-menu-item key="packages"><template #icon><GiftOutlined /></template><span>套餐管理</span></a-menu-item>
|
||||
<a-menu-item key="operation-logs"><template #icon><HistoryOutlined /></template><span>操作日志</span></a-menu-item>
|
||||
<a-menu-item key="settings"><template #icon><ToolOutlined /></template><span>系统设置</span></a-menu-item>
|
||||
</a-sub-menu>
|
||||
</a-menu>
|
||||
</a-drawer>
|
||||
|
||||
<a-layout class="school-layout-right">
|
||||
<a-layout-header class="school-header">
|
||||
<a-layout-header v-if="!isMobile" class="school-header">
|
||||
<div class="header-left">
|
||||
<MenuUnfoldOutlined
|
||||
v-if="collapsed"
|
||||
@ -165,7 +216,26 @@
|
||||
</div>
|
||||
</a-layout-header>
|
||||
|
||||
<a-layout-content class="school-content">
|
||||
<a-layout-header v-if="isMobile" class="school-mobile-header">
|
||||
<MenuOutlined class="menu-trigger" @click="drawerVisible = true" />
|
||||
<span class="mobile-title">少儿智慧阅读</span>
|
||||
<a-dropdown>
|
||||
<a-space class="user-info" style="cursor: pointer;">
|
||||
<a-avatar :size="32" class="user-avatar">
|
||||
<template #icon><UserOutlined /></template>
|
||||
</a-avatar>
|
||||
</a-space>
|
||||
<template #overlay>
|
||||
<a-menu @click="handleUserMenuClick">
|
||||
<a-menu-item key="profile"><UserOutlined /> 个人信息</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item key="logout"><LogoutOutlined /> 退出登录</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-layout-header>
|
||||
|
||||
<a-layout-content :class="['school-content', { 'school-content-mobile': isMobile }]">
|
||||
<router-view />
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
@ -190,6 +260,7 @@ import {
|
||||
MessageOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuOutlined,
|
||||
BellOutlined,
|
||||
LogoutOutlined,
|
||||
DownOutlined,
|
||||
@ -204,19 +275,26 @@ import {
|
||||
FolderAddOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { useBreakpoints } from '@/composables/useBreakpoints';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const userStore = useUserStore();
|
||||
const { isMobile } = useBreakpoints();
|
||||
|
||||
const collapsed = ref(false);
|
||||
const selectedKeys = ref(['dashboard']);
|
||||
const openKeys = ref<string[]>(['staff', 'teaching', 'data', 'system']);
|
||||
const notifications = ref(0);
|
||||
const drawerVisible = ref(false);
|
||||
|
||||
const userName = computed(() => userStore.user?.name || '管理员');
|
||||
const tenantName = computed(() => userStore.user?.tenantName || '');
|
||||
|
||||
watch(isMobile, (mobile) => {
|
||||
if (!mobile) drawerVisible.value = false;
|
||||
});
|
||||
|
||||
// 路由到菜单的映射关系
|
||||
const routeToMenuMap: Record<string, { key: string; parentKey: string }> = {
|
||||
'/school/teachers': { key: 'teachers', parentKey: 'staff' },
|
||||
@ -261,11 +339,22 @@ const handleMenuClick = ({ key }: { key: string | number }) => {
|
||||
} else if (keyStr === 'operation-logs') {
|
||||
router.push('/school/operation-logs');
|
||||
} else if (!['staff', 'teaching', 'data', 'system'].includes(keyStr)) {
|
||||
// 只有点击叶子菜单项才跳转
|
||||
router.push(`/school/${keyStr}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrawerMenuClick = ({ key }: { key: string | number }) => {
|
||||
const keyStr = String(key);
|
||||
if (keyStr === 'packages') {
|
||||
router.push('/school/package');
|
||||
} else if (keyStr === 'operation-logs') {
|
||||
router.push('/school/operation-logs');
|
||||
} else if (!['staff', 'teaching', 'data', 'system'].includes(keyStr)) {
|
||||
router.push(`/school/${keyStr}`);
|
||||
drawerVisible.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleUserMenuClick = ({ key }: { key: string | number }) => {
|
||||
const keyStr = String(key);
|
||||
if (keyStr === 'logout') {
|
||||
@ -500,5 +589,51 @@ $bg-light: #FAFAFA;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.school-content-mobile {
|
||||
margin: 12px;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.school-drawer {
|
||||
.drawer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
.drawer-logo { width: 36px; height: 36px; object-fit: contain; }
|
||||
.drawer-title { font-size: 16px; font-weight: 600; color: $text-color; }
|
||||
}
|
||||
.drawer-menu {
|
||||
border-right: none !important;
|
||||
padding: 8px 0;
|
||||
:deep(.ant-menu-item),
|
||||
:deep(.ant-menu-submenu-title) { margin: 4px 8px; border-radius: 8px; height: 48px; line-height: 48px; }
|
||||
:deep(.ant-menu-sub) .ant-menu-item { height: 44px; line-height: 44px; }
|
||||
}
|
||||
}
|
||||
|
||||
.school-mobile-header {
|
||||
flex-shrink: 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
background: white;
|
||||
padding: 0 16px;
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
border-bottom: 1px solid $border-color;
|
||||
.menu-trigger { font-size: 22px; color: $text-secondary; padding: 8px; cursor: pointer; }
|
||||
.mobile-title { font-size: 17px; font-weight: 600; color: $text-color; }
|
||||
.user-avatar { background: linear-gradient(135deg, $primary-color 0%, $primary-dark 100%); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.school-layout :deep(.ant-layout-sider) { display: none; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<a-layout class="teacher-layout">
|
||||
<a-layout-sider
|
||||
v-if="!isMobile"
|
||||
v-model:collapsed="collapsed"
|
||||
:trigger="null"
|
||||
collapsible
|
||||
@ -64,8 +65,42 @@
|
||||
</div>
|
||||
</a-layout-sider>
|
||||
|
||||
<a-drawer
|
||||
v-if="isMobile"
|
||||
v-model:open="drawerVisible"
|
||||
placement="left"
|
||||
:closable="false"
|
||||
:width="280"
|
||||
class="teacher-drawer"
|
||||
:body-style="{ padding: 0 }"
|
||||
>
|
||||
<template #title>
|
||||
<div class="drawer-header">
|
||||
<img src="/logo.png" alt="Logo" class="drawer-logo" />
|
||||
<span class="drawer-title">服务平台</span>
|
||||
</div>
|
||||
</template>
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
mode="inline"
|
||||
theme="light"
|
||||
@click="handleDrawerMenuClick"
|
||||
class="drawer-menu"
|
||||
>
|
||||
<a-menu-item key="dashboard"><template #icon><HomeOutlined /></template><span>首页</span></a-menu-item>
|
||||
<a-menu-item key="classes"><template #icon><TeamOutlined /></template><span>我的班级</span></a-menu-item>
|
||||
<a-menu-item key="courses"><template #icon><BookOutlined /></template><span>课程中心</span></a-menu-item>
|
||||
<a-menu-item key="school-courses"><template #icon><FolderAddOutlined /></template><span>校本课程包</span></a-menu-item>
|
||||
<a-menu-item key="lessons"><template #icon><CalendarOutlined /></template><span>上课记录</span></a-menu-item>
|
||||
<a-menu-item key="schedule"><template #icon><ScheduleOutlined /></template><span>我的课表</span></a-menu-item>
|
||||
<a-menu-item key="tasks"><template #icon><CheckSquareOutlined /></template><span>阅读任务</span></a-menu-item>
|
||||
<a-menu-item key="feedback"><template #icon><FileTextOutlined /></template><span>课程反馈</span></a-menu-item>
|
||||
<a-menu-item key="growth"><template #icon><CameraOutlined /></template><span>成长档案</span></a-menu-item>
|
||||
</a-menu>
|
||||
</a-drawer>
|
||||
|
||||
<a-layout class="teacher-layout-right">
|
||||
<a-layout-header class="teacher-header">
|
||||
<a-layout-header v-if="!isMobile" class="teacher-header">
|
||||
<div class="header-left">
|
||||
<MenuUnfoldOutlined
|
||||
v-if="collapsed"
|
||||
@ -111,7 +146,26 @@
|
||||
</div>
|
||||
</a-layout-header>
|
||||
|
||||
<a-layout-content class="teacher-content">
|
||||
<a-layout-header v-if="isMobile" class="teacher-mobile-header">
|
||||
<MenuOutlined class="menu-trigger" @click="drawerVisible = true" />
|
||||
<span class="mobile-title">少儿智慧阅读</span>
|
||||
<a-dropdown>
|
||||
<a-space class="user-info" style="cursor: pointer;">
|
||||
<a-avatar :size="32" class="user-avatar">
|
||||
<template #icon><UserOutlined /></template>
|
||||
</a-avatar>
|
||||
</a-space>
|
||||
<template #overlay>
|
||||
<a-menu @click="handleUserMenuClick">
|
||||
<a-menu-item key="profile"><UserOutlined /> 个人信息</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item key="logout"><LogoutOutlined /> 退出登录</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-layout-header>
|
||||
|
||||
<a-layout-content :class="['teacher-content', { 'teacher-content-mobile': isMobile }]">
|
||||
<router-view />
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
@ -130,6 +184,7 @@ import {
|
||||
FileTextOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuOutlined,
|
||||
BellOutlined,
|
||||
UserOutlined,
|
||||
LogoutOutlined,
|
||||
@ -140,18 +195,25 @@ import {
|
||||
FolderAddOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { useBreakpoints } from '@/composables/useBreakpoints';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const userStore = useUserStore();
|
||||
const { isMobile } = useBreakpoints();
|
||||
|
||||
const collapsed = ref(false);
|
||||
const selectedKeys = ref(['dashboard']);
|
||||
const notifications = ref(0);
|
||||
const drawerVisible = ref(false);
|
||||
|
||||
const userName = computed(() => userStore.user?.name || '教师');
|
||||
const tenantName = computed(() => userStore.user?.tenantName || '');
|
||||
|
||||
watch(isMobile, (mobile) => {
|
||||
if (!mobile) drawerVisible.value = false;
|
||||
});
|
||||
|
||||
// 根据路由设置选中的菜单
|
||||
watch(
|
||||
() => route.path,
|
||||
@ -183,6 +245,13 @@ const handleMenuClick = ({ key }: { key: string | number }) => {
|
||||
router.push(`/teacher/${key}`);
|
||||
};
|
||||
|
||||
const handleDrawerMenuClick = ({ key }: { key: string | number }) => {
|
||||
const keyStr = String(key);
|
||||
if (!['dashboard', 'classes', 'courses', 'school-courses', 'lessons', 'schedule', 'tasks', 'feedback', 'growth'].includes(keyStr)) return;
|
||||
router.push(`/teacher/${keyStr}`);
|
||||
drawerVisible.value = false;
|
||||
};
|
||||
|
||||
const handleUserMenuClick = ({ key }: { key: string | number }) => {
|
||||
const keyStr = String(key);
|
||||
if (keyStr === 'logout') {
|
||||
@ -380,5 +449,49 @@ $bg-light: #FAFAFA;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.teacher-content-mobile {
|
||||
margin: 12px;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.teacher-drawer {
|
||||
.drawer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
.drawer-logo { width: 36px; height: 36px; object-fit: contain; }
|
||||
.drawer-title { font-size: 16px; font-weight: 600; color: $text-color; }
|
||||
}
|
||||
.drawer-menu {
|
||||
border-right: none !important;
|
||||
padding: 8px 0;
|
||||
:deep(.ant-menu-item) { margin: 4px 8px; border-radius: 8px; height: 48px; line-height: 48px; }
|
||||
}
|
||||
}
|
||||
|
||||
.teacher-mobile-header {
|
||||
flex-shrink: 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
background: white;
|
||||
padding: 0 16px;
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
border-bottom: 1px solid $border-color;
|
||||
.menu-trigger { font-size: 22px; color: $text-secondary; padding: 8px; cursor: pointer; }
|
||||
.mobile-title { font-size: 17px; font-weight: 600; color: $text-color; }
|
||||
.user-avatar { background: linear-gradient(135deg, $primary-color 0%, $primary-dark 100%); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.teacher-layout :deep(.ant-layout-sider) { display: none; }
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user