chore: 固定后台布局与全局滚动样式优化

Made-with: Cursor
This commit is contained in:
zhonghua 2026-03-12 11:08:41 +08:00
parent 30b9cd5e05
commit cfaca4a2aa
10 changed files with 459 additions and 455 deletions

View File

@ -12,6 +12,12 @@ const AConfigProvider = ConfigProvider;
</script> </script>
<style> <style>
html,
body,
#app {
height: 100%;
}
#app { #app {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue',
Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
@ -24,4 +30,24 @@ body {
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
/* 全局滚动条样式(适用于页面与内部可滚动容器) */
* {
scrollbar-width: thin;
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
}
*::-webkit-scrollbar {
width: 4px;
height: 4px;
}
*::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.2);
border-radius: 999px;
}
*::-webkit-scrollbar-track {
background: transparent;
}
</style> </style>

View File

@ -15,10 +15,6 @@ declare module 'vue' {
ACheckbox: typeof import('ant-design-vue/es')['Checkbox'] ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
ACol: typeof import('ant-design-vue/es')['Col'] ACol: typeof import('ant-design-vue/es')['Col']
ADatePicker: typeof import('ant-design-vue/es')['DatePicker'] 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'] ADropdown: typeof import('ant-design-vue/es')['Dropdown']
AEmpty: typeof import('ant-design-vue/es')['Empty'] AEmpty: typeof import('ant-design-vue/es')['Empty']
AForm: typeof import('ant-design-vue/es')['Form'] AForm: typeof import('ant-design-vue/es')['Form']
@ -26,47 +22,33 @@ declare module 'vue' {
AImage: typeof import('ant-design-vue/es')['Image'] AImage: typeof import('ant-design-vue/es')['Image']
AImagePreviewGroup: typeof import('ant-design-vue/es')['ImagePreviewGroup'] AImagePreviewGroup: typeof import('ant-design-vue/es')['ImagePreviewGroup']
AInput: typeof import('ant-design-vue/es')['Input'] AInput: typeof import('ant-design-vue/es')['Input']
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword'] AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
AInputSearch: typeof import('ant-design-vue/es')['InputSearch'] AInputSearch: typeof import('ant-design-vue/es')['InputSearch']
ALayout: typeof import('ant-design-vue/es')['Layout'] ALayout: typeof import('ant-design-vue/es')['Layout']
ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent'] ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader'] ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader']
ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider'] 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'] AMenu: typeof import('ant-design-vue/es')['Menu']
AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider'] AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider']
AMenuItem: typeof import('ant-design-vue/es')['MenuItem'] AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
AModal: typeof import('ant-design-vue/es')['Modal'] AModal: typeof import('ant-design-vue/es')['Modal']
APageHeader: typeof import('ant-design-vue/es')['PageHeader']
APagination: typeof import('ant-design-vue/es')['Pagination'] APagination: typeof import('ant-design-vue/es')['Pagination']
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm'] APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
AProgress: typeof import('ant-design-vue/es')['Progress']
ARadio: typeof import('ant-design-vue/es')['Radio'] ARadio: typeof import('ant-design-vue/es')['Radio']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup'] ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARangePicker: typeof import('ant-design-vue/es')['RangePicker'] ARangePicker: typeof import('ant-design-vue/es')['RangePicker']
ARate: typeof import('ant-design-vue/es')['Rate']
ARow: typeof import('ant-design-vue/es')['Row'] ARow: typeof import('ant-design-vue/es')['Row']
ASelect: typeof import('ant-design-vue/es')['Select'] ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption'] ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ASkeleton: typeof import('ant-design-vue/es')['Skeleton']
ASpace: typeof import('ant-design-vue/es')['Space'] ASpace: typeof import('ant-design-vue/es')['Space']
ASpin: typeof import('ant-design-vue/es')['Spin'] ASpin: typeof import('ant-design-vue/es')['Spin']
AStatistic: typeof import('ant-design-vue/es')['Statistic']
ASubMenu: typeof import('ant-design-vue/es')['SubMenu'] ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
ASwitch: typeof import('ant-design-vue/es')['Switch']
ATable: typeof import('ant-design-vue/es')['Table'] 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'] ATag: typeof import('ant-design-vue/es')['Tag']
ATextarea: typeof import('ant-design-vue/es')['Textarea'] ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATimeRangePicker: typeof import('ant-design-vue/es')['TimeRangePicker']
ATooltip: typeof import('ant-design-vue/es')['Tooltip'] ATooltip: typeof import('ant-design-vue/es')['Tooltip']
ATypographyText: typeof import('ant-design-vue/es')['TypographyText'] ATypographyText: typeof import('ant-design-vue/es')['TypographyText']
AUpload: typeof import('ant-design-vue/es')['Upload'] AUpload: typeof import('ant-design-vue/es')['Upload']
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
FilePreviewModal: typeof import('./components/FilePreviewModal.vue')['default'] FilePreviewModal: typeof import('./components/FilePreviewModal.vue')['default']
FileUploader: typeof import('./components/course/FileUploader.vue')['default'] FileUploader: typeof import('./components/course/FileUploader.vue')['default']
LessonConfigPanel: typeof import('./components/course/LessonConfigPanel.vue')['default'] LessonConfigPanel: typeof import('./components/course/LessonConfigPanel.vue')['default']

View File

@ -1,10 +1,12 @@
<template> <template>
<a-layout class="admin-layout"> <a-layout class="admin-layout" :class="{ 'is-collapsed': collapsed }">
<a-layout-sider <a-layout-sider
v-model:collapsed="collapsed" v-model:collapsed="collapsed"
:trigger="null" :trigger="null"
collapsible collapsible
class="admin-sider" class="admin-sider"
:width="240"
:collapsed-width="80"
> >
<div class="logo"> <div class="logo">
<img src="/logo.png" alt="Logo" class="logo-img" /> <img src="/logo.png" alt="Logo" class="logo-img" />
@ -14,66 +16,68 @@
</div> </div>
</div> </div>
<a-menu <div class="sider-scroll">
v-model:selectedKeys="selectedKeys" <a-menu
mode="inline" v-model:selectedKeys="selectedKeys"
theme="light" mode="inline"
:inline-collapsed="collapsed" theme="light"
@select="handleMenuSelect" :inline-collapsed="collapsed"
class="side-menu" @select="handleMenuSelect"
> class="side-menu"
<a-menu-item key="dashboard"> >
<template #icon> <a-menu-item key="dashboard">
<LayoutDashboard :size="18" :stroke-width="1.5" /> <template #icon>
</template> <LayoutDashboard :size="18" :stroke-width="1.5" />
<span>数据看板</span> </template>
</a-menu-item> <span>数据看板</span>
</a-menu-item>
<a-menu-item key="courses"> <a-menu-item key="courses">
<template #icon> <template #icon>
<BookOpen :size="18" :stroke-width="1.5" /> <BookOpen :size="18" :stroke-width="1.5" />
</template> </template>
<span>课程包管理</span> <span>课程包管理</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="bundles"> <a-menu-item key="bundles">
<template #icon> <template #icon>
<DatabaseOutlined :size="18" :stroke-width="1.5" /> <DatabaseOutlined :size="18" :stroke-width="1.5" />
</template> </template>
<span>套餐管理</span> <span>套餐管理</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="themes"> <a-menu-item key="themes">
<template #icon> <template #icon>
<FormatPainterOutlined :size="18" :stroke-width="1.5" /> <FormatPainterOutlined :size="18" :stroke-width="1.5" />
</template> </template>
<span>主题字典</span> <span>主题字典</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="tenants"> <a-menu-item key="tenants">
<template #icon> <template #icon>
<Building2 :size="18" :stroke-width="1.5" /> <Building2 :size="18" :stroke-width="1.5" />
</template> </template>
<span>租户管理</span> <span>租户管理</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="resources"> <a-menu-item key="resources">
<template #icon> <template #icon>
<FolderOpen :size="18" :stroke-width="1.5" /> <FolderOpen :size="18" :stroke-width="1.5" />
</template> </template>
<span>资源库</span> <span>资源库</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="settings"> <a-menu-item key="settings">
<template #icon> <template #icon>
<Settings :size="18" :stroke-width="1.5" /> <Settings :size="18" :stroke-width="1.5" />
</template> </template>
<span>系统设置</span> <span>系统设置</span>
</a-menu-item> </a-menu-item>
</a-menu> </a-menu>
</div>
</a-layout-sider> </a-layout-sider>
<a-layout> <a-layout class="admin-main">
<a-layout-header class="admin-header"> <a-layout-header class="admin-header">
<div class="header-left"> <div class="header-left">
<MenuUnfoldOutlined <MenuUnfoldOutlined
@ -218,6 +222,10 @@ $border-color: #E5E7EB;
$bg-light: #F9FAFB; $bg-light: #F9FAFB;
$bg-dark: #111827; $bg-dark: #111827;
$sider-width: 240px;
$sider-collapsed-width: 80px;
$header-height: 64px;
.admin-layout { .admin-layout {
min-height: 100vh; min-height: 100vh;
background: $bg-light; background: $bg-light;
@ -227,6 +235,19 @@ $bg-dark: #111827;
background: white !important; background: white !important;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.04); box-shadow: 2px 0 8px rgba(0, 0, 0, 0.04);
border-right: 1px solid $border-color; border-right: 1px solid $border-color;
position: fixed;
left: 0;
top: 0;
bottom: 0;
z-index: 100;
display: flex;
flex-direction: column;
:deep(.ant-layout-sider-children) {
display: flex;
flex-direction: column;
height: 100%;
}
.logo { .logo {
height: 72px; height: 72px;
@ -264,6 +285,12 @@ $bg-dark: #111827;
} }
} }
.sider-scroll {
flex: 1 1 auto;
overflow-y: auto;
overflow-x: hidden;
}
.side-menu { .side-menu {
border-right: none !important; border-right: none !important;
padding: 12px 8px; padding: 12px 8px;
@ -318,6 +345,11 @@ $bg-dark: #111827;
} }
} }
.admin-main {
margin-left: $sider-width;
min-height: 100vh;
}
.admin-header { .admin-header {
background: white; background: white;
padding: 0 24px; padding: 0 24px;
@ -326,6 +358,12 @@ $bg-dark: #111827;
align-items: center; align-items: center;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
position: fixed;
top: 0;
right: 0;
left: $sider-width;
height: $header-height;
z-index: 90;
.trigger { .trigger {
font-size: 18px; font-size: 18px;
@ -354,10 +392,21 @@ $bg-dark: #111827;
.admin-content { .admin-content {
margin: 20px; margin: 20px;
margin-top: calc(#{$header-height} + 20px);
padding: 24px; padding: 24px;
background: white; background: white;
border-radius: 16px; border-radius: 16px;
min-height: calc(100vh - 64px - 40px); min-height: calc(100vh - #{$header-height} - 40px);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
} }
.admin-layout.is-collapsed {
.admin-main {
margin-left: $sider-collapsed-width;
}
.admin-header {
left: $sider-collapsed-width;
}
}
</style> </style>

View File

@ -4,14 +4,24 @@
<div class="welcome-banner"> <div class="welcome-banner">
<div class="banner-content"> <div class="banner-content">
<div class="banner-text"> <div class="banner-text">
<h1><HomeOutlined /> 校园阅读管理中心</h1> <h1>
<HomeOutlined /> 校园阅读管理中心
</h1>
<p>让每一个孩子都能享受阅读的快乐智慧成长每一天</p> <p>让每一个孩子都能享受阅读的快乐智慧成长每一天</p>
</div> </div>
<div class="banner-decorations"> <div class="banner-decorations">
<span class="decoration"><BookOutlined /></span> <span class="decoration">
<span class="decoration"><StarOutlined /></span> <BookOutlined />
<span class="decoration"><BgColorsOutlined /></span> </span>
<span class="decoration"><SmileOutlined /></span> <span class="decoration">
<StarOutlined />
</span>
<span class="decoration">
<BgColorsOutlined />
</span>
<span class="decoration">
<SmileOutlined />
</span>
</div> </div>
</div> </div>
</div> </div>
@ -33,7 +43,9 @@
<div class="charts-grid"> <div class="charts-grid">
<div class="content-card trend-card"> <div class="content-card trend-card">
<div class="card-header"> <div class="card-header">
<span class="card-icon"><LineChartOutlined /></span> <span class="card-icon">
<LineChartOutlined />
</span>
<h3>授课趋势</h3> <h3>授课趋势</h3>
</div> </div>
<div class="card-body" :class="{ 'is-loading': trendLoading }"> <div class="card-body" :class="{ 'is-loading': trendLoading }">
@ -43,7 +55,9 @@
</div> </div>
<div class="content-card distribution-card"> <div class="content-card distribution-card">
<div class="card-header"> <div class="card-header">
<span class="card-icon"><BarChartOutlined /></span> <span class="card-icon">
<BarChartOutlined />
</span>
<h3>课程分布</h3> <h3>课程分布</h3>
</div> </div>
<div class="card-body" :class="{ 'is-loading': distributionLoading }"> <div class="card-body" :class="{ 'is-loading': distributionLoading }">
@ -58,21 +72,21 @@
<!-- 近期活动 --> <!-- 近期活动 -->
<div class="content-card activities-card"> <div class="content-card activities-card">
<div class="card-header"> <div class="card-header">
<span class="card-icon"><CalendarOutlined /></span> <span class="card-icon">
<CalendarOutlined />
</span>
<h3>近期课程活动</h3> <h3>近期课程活动</h3>
</div> </div>
<div class="card-body" :class="{ 'is-loading': loading }"> <div class="card-body" :class="{ 'is-loading': loading }">
<a-spin v-if="loading" /> <a-spin v-if="loading" />
<div v-else-if="recentActivities.length === 0" class="empty-state"> <div v-else-if="recentActivities.length === 0" class="empty-state">
<span class="empty-icon"><InboxOutlined /></span> <span class="empty-icon">
<InboxOutlined />
</span>
<p>暂无近期活动</p> <p>暂无近期活动</p>
</div> </div>
<div v-else class="activity-list"> <div v-else class="activity-list">
<div <div v-for="item in recentActivities" :key="item.id" class="activity-item">
v-for="item in recentActivities"
:key="item.id"
class="activity-item"
>
<div class="activity-avatar"> <div class="activity-avatar">
<BookOutlined /> <BookOutlined />
</div> </div>
@ -88,28 +102,30 @@
<!-- 教师活跃度排行 --> <!-- 教师活跃度排行 -->
<div class="content-card teachers-card"> <div class="content-card teachers-card">
<div class="card-header"> <div class="card-header">
<span class="card-icon"><TrophyOutlined /></span> <span class="card-icon">
<TrophyOutlined />
</span>
<h3>教师活跃度排行</h3> <h3>教师活跃度排行</h3>
</div> </div>
<div class="card-body" :class="{ 'is-loading': loading }"> <div class="card-body" :class="{ 'is-loading': loading }">
<a-spin v-if="loading" /> <a-spin v-if="loading" />
<div v-else-if="activeTeachers.length === 0" class="empty-state"> <div v-else-if="activeTeachers.length === 0" class="empty-state">
<span class="empty-icon"><TeamOutlined /></span> <span class="empty-icon">
<TeamOutlined />
</span>
<p>暂无数据</p> <p>暂无数据</p>
</div> </div>
<div v-else class="teacher-list"> <div v-else class="teacher-list">
<div <div v-for="(item, index) in activeTeachers" :key="item.id" class="teacher-item">
v-for="(item, index) in activeTeachers"
:key="item.id"
class="teacher-item"
>
<div class="rank-badge" :class="'rank-' + (index + 1)"> <div class="rank-badge" :class="'rank-' + (index + 1)">
{{ index + 1 }} {{ index + 1 }}
</div> </div>
<div class="teacher-info"> <div class="teacher-info">
<div class="teacher-name">{{ item.name }}</div> <div class="teacher-name">{{ item.name }}</div>
<div class="teacher-lessons"> <div class="teacher-lessons">
<span class="lesson-icon"><ReadOutlined /></span> <span class="lesson-icon">
<ReadOutlined />
</span>
授课 {{ item.lessonCount }} 授课 {{ item.lessonCount }}
</div> </div>
</div> </div>
@ -125,29 +141,25 @@
<!-- 课程使用统计 --> <!-- 课程使用统计 -->
<div class="course-stats-card"> <div class="course-stats-card">
<div class="card-header"> <div class="card-header">
<span class="card-icon"><BarChartOutlined /></span> <span class="card-icon">
<BarChartOutlined />
</span>
<h3>课程使用统计</h3> <h3>课程使用统计</h3>
<div class="header-extra"> <div class="header-extra">
<a-range-picker <a-range-picker v-model:value="dateRange" @change="loadCourseStats" :placeholder="['开始日期', '结束日期']"
v-model:value="dateRange" style="width: 240px;" />
@change="loadCourseStats"
:placeholder="['开始日期', '结束日期']"
style="width: 240px;"
/>
</div> </div>
</div> </div>
<div class="card-body" :class="{ 'is-loading': courseStatsLoading }"> <div class="card-body" :class="{ 'is-loading': courseStatsLoading }">
<a-spin v-if="courseStatsLoading" /> <a-spin v-if="courseStatsLoading" />
<div v-else-if="courseStats.length === 0" class="empty-state"> <div v-else-if="courseStats.length === 0" class="empty-state">
<span class="empty-icon"><LineChartOutlined /></span> <span class="empty-icon">
<LineChartOutlined />
</span>
<p>暂无课程使用数据</p> <p>暂无课程使用数据</p>
</div> </div>
<div v-else class="course-list"> <div v-else class="course-list">
<div <div v-for="(item, index) in courseStats" :key="item.courseId" class="course-item">
v-for="(item, index) in courseStats"
:key="item.courseId"
class="course-item"
>
<div class="course-rank" :class="'top-' + (index + 1)"> <div class="course-rank" :class="'top-' + (index + 1)">
<TrophyFilled v-if="index < 3" class="rank-crown" :style="getTrophyColor(index)" /> <TrophyFilled v-if="index < 3" class="rank-crown" :style="getTrophyColor(index)" />
<span v-else>{{ index + 1 }}</span> <span v-else>{{ index + 1 }}</span>
@ -155,13 +167,10 @@
<div class="course-name">{{ item.courseName }}</div> <div class="course-name">{{ item.courseName }}</div>
<div class="course-progress"> <div class="course-progress">
<div class="progress-bar"> <div class="progress-bar">
<div <div class="progress-fill" :style="{
class="progress-fill" width: getUsagePercent(item.usageCount) + '%',
:style="{ background: getProgressGradient(index)
width: getUsagePercent(item.usageCount) + '%', }"></div>
background: getProgressGradient(index)
}"
></div>
</div> </div>
<span class="progress-value">{{ item.usageCount }}</span> <span class="progress-value">{{ item.usageCount }}</span>
</div> </div>
@ -174,7 +183,9 @@
<div class="export-section"> <div class="export-section">
<div class="content-card export-card"> <div class="content-card export-card">
<div class="card-header"> <div class="card-header">
<span class="card-icon"><DownloadOutlined /></span> <span class="card-icon">
<DownloadOutlined />
</span>
<h3>数据导出</h3> <h3>数据导出</h3>
</div> </div>
<div class="card-body"> <div class="card-body">
@ -717,13 +728,28 @@ onUnmounted(() => {
animation: float 3s ease-in-out infinite; animation: float 3s ease-in-out infinite;
} }
.decoration:nth-child(2) { animation-delay: 0.5s; } .decoration:nth-child(2) {
.decoration:nth-child(3) { animation-delay: 1s; } animation-delay: 0.5s;
.decoration:nth-child(4) { animation-delay: 1.5s; } }
.decoration:nth-child(3) {
animation-delay: 1s;
}
.decoration:nth-child(4) {
animation-delay: 1.5s;
}
@keyframes float { @keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); } 0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
} }
/* 统计卡片 */ /* 统计卡片 */
@ -956,9 +982,17 @@ onUnmounted(() => {
background: #B2BEC3; background: #B2BEC3;
} }
.rank-badge.rank-1 { background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%); } .rank-badge.rank-1 {
.rank-badge.rank-2 { background: linear-gradient(135deg, #C0C0C0 0%, #A8A8A8 100%); } background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
.rank-badge.rank-3 { background: linear-gradient(135deg, #CD7F32 0%, #B8860B 100%); } }
.rank-badge.rank-2 {
background: linear-gradient(135deg, #C0C0C0 0%, #A8A8A8 100%);
}
.rank-badge.rank-3 {
background: linear-gradient(135deg, #CD7F32 0%, #B8860B 100%);
}
.teacher-info { .teacher-info {
flex: 1; flex: 1;

View File

@ -1,10 +1,12 @@
<template> <template>
<a-layout class="school-layout"> <a-layout class="school-layout" :class="{ 'is-collapsed': collapsed }">
<a-layout-sider <a-layout-sider
v-model:collapsed="collapsed" v-model:collapsed="collapsed"
:trigger="null" :trigger="null"
collapsible collapsible
class="school-sider" class="school-sider"
:width="240"
:collapsed-width="80"
> >
<div class="logo"> <div class="logo">
<img src="/logo.png" alt="Logo" class="logo-img" /> <img src="/logo.png" alt="Logo" class="logo-img" />
@ -15,108 +17,110 @@
</div> </div>
</div> </div>
<a-menu <div class="sider-scroll">
v-model:selectedKeys="selectedKeys" <a-menu
v-model:openKeys="openKeys" v-model:selectedKeys="selectedKeys"
mode="inline" v-model:openKeys="openKeys"
theme="light" mode="inline"
:inline-collapsed="collapsed" theme="light"
@click="handleMenuClick" :inline-collapsed="collapsed"
class="side-menu" @click="handleMenuClick"
> class="side-menu"
<!-- 数据概览 - 独立一级菜单 --> >
<a-menu-item key="dashboard"> <!-- 数据概览 - 独立一级菜单 -->
<template #icon><DashboardOutlined /></template> <a-menu-item key="dashboard">
<span>数据概览</span> <template #icon><DashboardOutlined /></template>
</a-menu-item> <span>数据概览</span>
</a-menu-item>
<!-- 人员管理 --> <!-- 人员管理 -->
<a-sub-menu key="staff"> <a-sub-menu key="staff">
<template #icon><TeamOutlined /></template> <template #icon><TeamOutlined /></template>
<template #title>人员管理</template> <template #title>人员管理</template>
<a-menu-item key="teachers"> <a-menu-item key="teachers">
<template #icon><SolutionOutlined /></template> <template #icon><SolutionOutlined /></template>
<span>教师管理</span> <span>教师管理</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="students"> <a-menu-item key="students">
<template #icon><UserOutlined /></template> <template #icon><UserOutlined /></template>
<span>学生管理</span> <span>学生管理</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="parents"> <a-menu-item key="parents">
<template #icon><IdcardOutlined /></template> <template #icon><IdcardOutlined /></template>
<span>家长管理</span> <span>家长管理</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="classes"> <a-menu-item key="classes">
<template #icon><HomeOutlined /></template> <template #icon><HomeOutlined /></template>
<span>班级管理</span> <span>班级管理</span>
</a-menu-item> </a-menu-item>
</a-sub-menu> </a-sub-menu>
<!-- 教学管理 --> <!-- 教学管理 -->
<a-sub-menu key="teaching"> <a-sub-menu key="teaching">
<template #icon><BookOutlined /></template> <template #icon><BookOutlined /></template>
<template #title>教学管理</template> <template #title>教学管理</template>
<a-menu-item key="courses"> <a-menu-item key="courses">
<template #icon><ReadOutlined /></template> <template #icon><ReadOutlined /></template>
<span>课程管理</span> <span>课程管理</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="school-courses"> <a-menu-item key="school-courses">
<template #icon><FolderAddOutlined /></template> <template #icon><FolderAddOutlined /></template>
<span>校本课程包</span> <span>校本课程包</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="schedule"> <a-menu-item key="schedule">
<template #icon><CalendarOutlined /></template> <template #icon><CalendarOutlined /></template>
<span>课程排期</span> <span>课程排期</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="tasks"> <a-menu-item key="tasks">
<template #icon><CheckSquareOutlined /></template> <template #icon><CheckSquareOutlined /></template>
<span>阅读任务</span> <span>阅读任务</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="task-templates"> <a-menu-item key="task-templates">
<template #icon><CopyOutlined /></template> <template #icon><CopyOutlined /></template>
<span>任务模板</span> <span>任务模板</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="feedback"> <a-menu-item key="feedback">
<template #icon><MessageOutlined /></template> <template #icon><MessageOutlined /></template>
<span>课程反馈</span> <span>课程反馈</span>
</a-menu-item> </a-menu-item>
</a-sub-menu> </a-sub-menu>
<!-- 数据中心 --> <!-- 数据中心 -->
<a-sub-menu key="data"> <a-sub-menu key="data">
<template #icon><BarChartOutlined /></template> <template #icon><BarChartOutlined /></template>
<template #title>数据中心</template> <template #title>数据中心</template>
<a-menu-item key="reports"> <a-menu-item key="reports">
<template #icon><FileTextOutlined /></template> <template #icon><FileTextOutlined /></template>
<span>数据报告</span> <span>数据报告</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="growth"> <a-menu-item key="growth">
<template #icon><FileImageOutlined /></template> <template #icon><FileImageOutlined /></template>
<span>成长档案</span> <span>成长档案</span>
</a-menu-item> </a-menu-item>
</a-sub-menu> </a-sub-menu>
<!-- 系统管理 --> <!-- 系统管理 -->
<a-sub-menu key="system"> <a-sub-menu key="system">
<template #icon><SettingOutlined /></template> <template #icon><SettingOutlined /></template>
<template #title>系统管理</template> <template #title>系统管理</template>
<a-menu-item key="packages"> <a-menu-item key="packages">
<template #icon><GiftOutlined /></template> <template #icon><GiftOutlined /></template>
<span>套餐管理</span> <span>套餐管理</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="operation-logs"> <a-menu-item key="operation-logs">
<template #icon><HistoryOutlined /></template> <template #icon><HistoryOutlined /></template>
<span>操作日志</span> <span>操作日志</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="settings"> <a-menu-item key="settings">
<template #icon><ToolOutlined /></template> <template #icon><ToolOutlined /></template>
<span>系统设置</span> <span>系统设置</span>
</a-menu-item> </a-menu-item>
</a-sub-menu> </a-sub-menu>
</a-menu> </a-menu>
</div>
</a-layout-sider> </a-layout-sider>
<a-layout> <a-layout class="school-main">
<a-layout-header class="school-header"> <a-layout-header class="school-header">
<div class="header-left"> <div class="header-left">
<MenuUnfoldOutlined <MenuUnfoldOutlined
@ -287,6 +291,10 @@ $text-secondary: #666666;
$border-color: #E8E8E8; $border-color: #E8E8E8;
$bg-light: #FAFAFA; $bg-light: #FAFAFA;
$sider-width: 240px;
$sider-collapsed-width: 80px;
$header-height: 64px;
.school-layout { .school-layout {
min-height: 100vh; min-height: 100vh;
background: $bg-light; background: $bg-light;
@ -296,6 +304,19 @@ $bg-light: #FAFAFA;
background: white !important; background: white !important;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.06); box-shadow: 2px 0 8px rgba(0, 0, 0, 0.06);
border-right: 1px solid $border-color; border-right: 1px solid $border-color;
position: fixed;
left: 0;
top: 0;
bottom: 0;
z-index: 100;
display: flex;
flex-direction: column;
:deep(.ant-layout-sider-children) {
display: flex;
flex-direction: column;
height: 100%;
}
.logo { .logo {
height: 80px; height: 80px;
@ -344,6 +365,12 @@ $bg-light: #FAFAFA;
} }
} }
.sider-scroll {
flex: 1 1 auto;
overflow-y: auto;
overflow-x: hidden;
}
.side-menu { .side-menu {
border-right: none !important; border-right: none !important;
padding: 8px 12px; padding: 8px 12px;
@ -423,6 +450,11 @@ $bg-light: #FAFAFA;
} }
} }
.school-main {
margin-left: $sider-width;
min-height: 100vh;
}
.school-header { .school-header {
background: white; background: white;
padding: 0 24px; padding: 0 24px;
@ -431,6 +463,12 @@ $bg-light: #FAFAFA;
align-items: center; align-items: center;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
position: fixed;
top: 0;
right: 0;
left: $sider-width;
height: $header-height;
z-index: 90;
.trigger { .trigger {
font-size: 18px; font-size: 18px;
@ -459,10 +497,21 @@ $bg-light: #FAFAFA;
.school-content { .school-content {
margin: 20px; margin: 20px;
margin-top: calc(#{$header-height} + 20px);
padding: 24px; padding: 24px;
background: white; background: white;
border-radius: 12px; border-radius: 12px;
min-height: calc(100vh - 64px - 40px); min-height: calc(100vh - #{$header-height} - 40px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
} }
.school-layout.is-collapsed {
.school-main {
margin-left: $sider-collapsed-width;
}
.school-header {
left: $sider-collapsed-width;
}
}
</style> </style>

View File

@ -1,10 +1,12 @@
<template> <template>
<a-layout class="teacher-layout"> <a-layout class="teacher-layout" :class="{ 'is-collapsed': collapsed }">
<a-layout-sider <a-layout-sider
v-model:collapsed="collapsed" v-model:collapsed="collapsed"
:trigger="null" :trigger="null"
collapsible collapsible
class="teacher-sider" class="teacher-sider"
:width="240"
:collapsed-width="80"
> >
<div class="logo"> <div class="logo">
<img src="/logo.png" alt="Logo" class="logo-img" /> <img src="/logo.png" alt="Logo" class="logo-img" />
@ -15,54 +17,56 @@
</div> </div>
</div> </div>
<a-menu <div class="sider-scroll">
v-model:selectedKeys="selectedKeys" <a-menu
mode="inline" v-model:selectedKeys="selectedKeys"
theme="light" mode="inline"
:inline-collapsed="collapsed" theme="light"
@click="handleMenuClick" :inline-collapsed="collapsed"
class="side-menu" @click="handleMenuClick"
> class="side-menu"
<a-menu-item key="dashboard"> >
<template #icon><HomeOutlined /></template> <a-menu-item key="dashboard">
<span>首页</span> <template #icon><HomeOutlined /></template>
</a-menu-item> <span>首页</span>
<a-menu-item key="classes"> </a-menu-item>
<template #icon><TeamOutlined /></template> <a-menu-item key="classes">
<span>我的班级</span> <template #icon><TeamOutlined /></template>
</a-menu-item> <span>我的班级</span>
<a-menu-item key="courses"> </a-menu-item>
<template #icon><BookOutlined /></template> <a-menu-item key="courses">
<span>课程中心</span> <template #icon><BookOutlined /></template>
</a-menu-item> <span>课程中心</span>
<a-menu-item key="school-courses"> </a-menu-item>
<template #icon><FolderAddOutlined /></template> <a-menu-item key="school-courses">
<span>校本课程包</span> <template #icon><FolderAddOutlined /></template>
</a-menu-item> <span>校本课程包</span>
<a-menu-item key="lessons"> </a-menu-item>
<template #icon><CalendarOutlined /></template> <a-menu-item key="lessons">
<span>上课记录</span> <template #icon><CalendarOutlined /></template>
</a-menu-item> <span>上课记录</span>
<a-menu-item key="schedule"> </a-menu-item>
<template #icon><ScheduleOutlined /></template> <a-menu-item key="schedule">
<span>我的课表</span> <template #icon><ScheduleOutlined /></template>
</a-menu-item> <span>我的课表</span>
<a-menu-item key="tasks"> </a-menu-item>
<template #icon><CheckSquareOutlined /></template> <a-menu-item key="tasks">
<span>阅读任务</span> <template #icon><CheckSquareOutlined /></template>
</a-menu-item> <span>阅读任务</span>
<a-menu-item key="feedback"> </a-menu-item>
<template #icon><FileTextOutlined /></template> <a-menu-item key="feedback">
<span>课程反馈</span> <template #icon><FileTextOutlined /></template>
</a-menu-item> <span>课程反馈</span>
<a-menu-item key="growth"> </a-menu-item>
<template #icon><CameraOutlined /></template> <a-menu-item key="growth">
<span>成长档案</span> <template #icon><CameraOutlined /></template>
</a-menu-item> <span>成长档案</span>
</a-menu> </a-menu-item>
</a-menu>
</div>
</a-layout-sider> </a-layout-sider>
<a-layout> <a-layout class="teacher-main">
<a-layout-header class="teacher-header"> <a-layout-header class="teacher-header">
<div class="header-left"> <div class="header-left">
<MenuUnfoldOutlined <MenuUnfoldOutlined
@ -204,6 +208,10 @@ $text-secondary: #666666;
$border-color: #E8E8E8; $border-color: #E8E8E8;
$bg-light: #FAFAFA; $bg-light: #FAFAFA;
$sider-width: 240px;
$sider-collapsed-width: 80px;
$header-height: 64px;
.teacher-layout { .teacher-layout {
min-height: 100vh; min-height: 100vh;
background: $bg-light; background: $bg-light;
@ -213,6 +221,19 @@ $bg-light: #FAFAFA;
background: white !important; background: white !important;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.06); box-shadow: 2px 0 8px rgba(0, 0, 0, 0.06);
border-right: 1px solid $border-color; border-right: 1px solid $border-color;
position: fixed;
left: 0;
top: 0;
bottom: 0;
z-index: 100;
display: flex;
flex-direction: column;
:deep(.ant-layout-sider-children) {
display: flex;
flex-direction: column;
height: 100%;
}
.logo { .logo {
height: 80px; height: 80px;
@ -261,6 +282,12 @@ $bg-light: #FAFAFA;
} }
} }
.sider-scroll {
flex: 1 1 auto;
overflow-y: auto;
overflow-x: hidden;
}
.side-menu { .side-menu {
border-right: none !important; border-right: none !important;
padding: 8px 12px; padding: 8px 12px;
@ -303,6 +330,11 @@ $bg-light: #FAFAFA;
} }
} }
.teacher-main {
margin-left: $sider-width;
min-height: 100vh;
}
.teacher-header { .teacher-header {
background: white; background: white;
padding: 0 24px; padding: 0 24px;
@ -311,6 +343,12 @@ $bg-light: #FAFAFA;
align-items: center; align-items: center;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
position: fixed;
top: 0;
right: 0;
left: $sider-width;
height: $header-height;
z-index: 90;
.trigger { .trigger {
font-size: 18px; font-size: 18px;
@ -339,10 +377,21 @@ $bg-light: #FAFAFA;
.teacher-content { .teacher-content {
margin: 20px; margin: 20px;
margin-top: calc(#{$header-height} + 20px);
padding: 24px; padding: 24px;
background: white; background: white;
border-radius: 12px; border-radius: 12px;
min-height: calc(100vh - 64px - 40px); min-height: calc(100vh - #{$header-height} - 40px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
} }
.teacher-layout.is-collapsed {
.teacher-main {
margin-left: $sider-collapsed-width;
}
.teacher-header {
left: $sider-collapsed-width;
}
}
</style> </style>

View File

@ -1,57 +0,0 @@
package com.reading.platform;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.junit.jupiter.api.Test;
import org.springframework.test.context.ActiveProfiles;
import java.util.List;
import java.util.Map;
/**
* 数据库表结构检查工具
*/
@SpringBootTest
@ActiveProfiles("dev")
public class DatabaseInspectTool {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 检查 V20 迁移涉及的表和列
*/
@Test
public void checkV20Tables() {
System.out.println("=== 检查 V20 迁移涉及的表和列 ===\n");
String[] tables = {"student_records", "lesson_feedbacks", "lessons", "student_class_history", "class_teacher", "students"};
for (String table : tables) {
System.out.println("表:" + table);
try {
List<Map<String, Object>> columns = jdbcTemplate.queryForList(
"SHOW COLUMNS FROM " + table
);
System.out.println(" 列名:");
for (Map<String, Object> col : columns) {
System.out.println(" - " + col.get("Field") + " (" + col.get("Type") + ")");
}
} catch (Exception e) {
System.out.println(" 表不存在:" + e.getMessage());
}
System.out.println();
}
}
/**
* 清理 flyway_schema_history 表中的 V20 记录
*/
@Test
public void cleanFlywayHistory() {
System.out.println("=== 清理 Flyway V20 记录 ===");
int deleted = jdbcTemplate.update("DELETE FROM flyway_schema_history WHERE version = '20'");
System.out.println("已删除 " + deleted + " 条记录");
}
}

View File

@ -1,73 +0,0 @@
package com.reading.platform;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.junit.jupiter.api.Test;
import org.springframework.test.context.ActiveProfiles;
import java.util.List;
import java.util.Map;
/**
* Flyway 迁移历史查看和清理工具
*/
@SpringBootTest
@ActiveProfiles("dev")
public class FlywayHistoryTool {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 查看迁移历史
*/
@Test
public void showHistory() {
System.out.println("=== Flyway 迁移历史 ===");
List<Map<String, Object>> history = jdbcTemplate.queryForList(
"SELECT version, description, type, script, checksum, installed_by, installed_on, execution_time, success " +
"FROM flyway_schema_history " +
"ORDER BY installed_rank DESC"
);
for (Map<String, Object> row : history) {
System.out.printf("Version: %-10s | Type: %-15s | Script: %-50s | Success: %s | Installed: %s%n",
row.get("version"),
row.get("type"),
row.get("script"),
row.get("success"),
row.get("installed_on")
);
}
}
/**
* 清理版本 20 的迁移记录
*/
@Test
public void cleanV20() {
System.out.println("=== 清理版本 20 的迁移记录 ===");
// 查看版本 20 的记录
List<Map<String, Object>> v20Records = jdbcTemplate.queryForList(
"SELECT * FROM flyway_schema_history WHERE version = '20'"
);
if (v20Records.isEmpty()) {
System.out.println("没有找到版本 20 的记录");
return;
}
System.out.println("找到 " + v20Records.size() + " 条版本 20 的记录:");
for (Map<String, Object> row : v20Records) {
System.out.printf(" - ID: %s, Script: %s, Success: %s%n",
row.get("installed_rank"), row.get("script"), row.get("success"));
}
// 删除版本 20 的所有记录
int deleted = jdbcTemplate.update("DELETE FROM flyway_schema_history WHERE version = '20'");
System.out.println("已删除 " + deleted + " 条记录");
System.out.println("请重启应用Flyway 会重新执行 V20 迁移");
}
}

View File

@ -1,55 +0,0 @@
package com.reading.platform;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.junit.jupiter.api.Test;
import org.springframework.test.context.ActiveProfiles;
/**
* Flyway 修复工具
* 用于修复失败的数据库迁移
*
* 使用方法
* 1. application-dev.yml 中添加spring.flyway.enabled=false
* 2. 运行此测试类mvn test -Dtest=FlywayRepairTool#repairV20
* 3. 恢复 Flyway 配置
* 4. 重启应用
*/
@SpringBootTest
@ActiveProfiles("dev")
public class FlywayRepairTool {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 修复 V20 失败的迁移
*/
@Test
public void repairV20() {
System.out.println("开始修复 Flyway V20 迁移...");
// 删除 V20 的失败记录
int rows = jdbcTemplate.update(
"DELETE FROM flyway_schema_history WHERE version = '20'"
);
System.out.println("已删除 " + rows + " 条失败记录");
System.out.println("修复完成!请重启应用。");
}
/**
* 查看当前的迁移历史
*/
@Test
public void showMigrationHistory() {
System.out.println("当前迁移历史:");
jdbcTemplate.queryForList("SELECT * FROM flyway_schema_history ORDER BY installed_on DESC")
.forEach(record -> {
System.out.println(" Version: " + record.get("version") +
", Status: " + record.get("success") +
", Installed: " + record.get("installed_on"));
});
}
}