chore: 固定后台布局与全局滚动样式优化
Made-with: Cursor
This commit is contained in:
parent
30b9cd5e05
commit
cfaca4a2aa
@ -12,6 +12,12 @@ const AConfigProvider = ConfigProvider;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#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',
|
||||
@ -24,4 +30,24 @@ body {
|
||||
margin: 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>
|
||||
|
||||
18
reading-platform-frontend/src/components.d.ts
vendored
18
reading-platform-frontend/src/components.d.ts
vendored
@ -15,10 +15,6 @@ declare module 'vue' {
|
||||
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
|
||||
ACol: typeof import('ant-design-vue/es')['Col']
|
||||
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']
|
||||
@ -26,47 +22,33 @@ declare module 'vue' {
|
||||
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']
|
||||
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
|
||||
ARangePicker: typeof import('ant-design-vue/es')['RangePicker']
|
||||
ARate: typeof import('ant-design-vue/es')['Rate']
|
||||
ARow: typeof import('ant-design-vue/es')['Row']
|
||||
ASelect: typeof import('ant-design-vue/es')['Select']
|
||||
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']
|
||||
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']
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
<template>
|
||||
<a-layout class="admin-layout">
|
||||
<a-layout class="admin-layout" :class="{ 'is-collapsed': collapsed }">
|
||||
<a-layout-sider
|
||||
v-model:collapsed="collapsed"
|
||||
:trigger="null"
|
||||
collapsible
|
||||
class="admin-sider"
|
||||
:width="240"
|
||||
:collapsed-width="80"
|
||||
>
|
||||
<div class="logo">
|
||||
<img src="/logo.png" alt="Logo" class="logo-img" />
|
||||
@ -14,6 +16,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sider-scroll">
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
mode="inline"
|
||||
@ -71,9 +74,10 @@
|
||||
<span>系统设置</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</div>
|
||||
</a-layout-sider>
|
||||
|
||||
<a-layout>
|
||||
<a-layout class="admin-main">
|
||||
<a-layout-header class="admin-header">
|
||||
<div class="header-left">
|
||||
<MenuUnfoldOutlined
|
||||
@ -218,6 +222,10 @@ $border-color: #E5E7EB;
|
||||
$bg-light: #F9FAFB;
|
||||
$bg-dark: #111827;
|
||||
|
||||
$sider-width: 240px;
|
||||
$sider-collapsed-width: 80px;
|
||||
$header-height: 64px;
|
||||
|
||||
.admin-layout {
|
||||
min-height: 100vh;
|
||||
background: $bg-light;
|
||||
@ -227,6 +235,19 @@ $bg-dark: #111827;
|
||||
background: white !important;
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.04);
|
||||
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 {
|
||||
height: 72px;
|
||||
@ -264,6 +285,12 @@ $bg-dark: #111827;
|
||||
}
|
||||
}
|
||||
|
||||
.sider-scroll {
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.side-menu {
|
||||
border-right: none !important;
|
||||
padding: 12px 8px;
|
||||
@ -318,6 +345,11 @@ $bg-dark: #111827;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-main {
|
||||
margin-left: $sider-width;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.admin-header {
|
||||
background: white;
|
||||
padding: 0 24px;
|
||||
@ -326,6 +358,12 @@ $bg-dark: #111827;
|
||||
align-items: center;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
border-bottom: 1px solid $border-color;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: $sider-width;
|
||||
height: $header-height;
|
||||
z-index: 90;
|
||||
|
||||
.trigger {
|
||||
font-size: 18px;
|
||||
@ -354,10 +392,21 @@ $bg-dark: #111827;
|
||||
|
||||
.admin-content {
|
||||
margin: 20px;
|
||||
margin-top: calc(#{$header-height} + 20px);
|
||||
padding: 24px;
|
||||
background: white;
|
||||
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);
|
||||
}
|
||||
|
||||
.admin-layout.is-collapsed {
|
||||
.admin-main {
|
||||
margin-left: $sider-collapsed-width;
|
||||
}
|
||||
|
||||
.admin-header {
|
||||
left: $sider-collapsed-width;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -4,14 +4,24 @@
|
||||
<div class="welcome-banner">
|
||||
<div class="banner-content">
|
||||
<div class="banner-text">
|
||||
<h1><HomeOutlined /> 校园阅读管理中心</h1>
|
||||
<h1>
|
||||
<HomeOutlined /> 校园阅读管理中心
|
||||
</h1>
|
||||
<p>让每一个孩子都能享受阅读的快乐,智慧成长每一天!</p>
|
||||
</div>
|
||||
<div class="banner-decorations">
|
||||
<span class="decoration"><BookOutlined /></span>
|
||||
<span class="decoration"><StarOutlined /></span>
|
||||
<span class="decoration"><BgColorsOutlined /></span>
|
||||
<span class="decoration"><SmileOutlined /></span>
|
||||
<span class="decoration">
|
||||
<BookOutlined />
|
||||
</span>
|
||||
<span class="decoration">
|
||||
<StarOutlined />
|
||||
</span>
|
||||
<span class="decoration">
|
||||
<BgColorsOutlined />
|
||||
</span>
|
||||
<span class="decoration">
|
||||
<SmileOutlined />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -33,7 +43,9 @@
|
||||
<div class="charts-grid">
|
||||
<div class="content-card trend-card">
|
||||
<div class="card-header">
|
||||
<span class="card-icon"><LineChartOutlined /></span>
|
||||
<span class="card-icon">
|
||||
<LineChartOutlined />
|
||||
</span>
|
||||
<h3>授课趋势</h3>
|
||||
</div>
|
||||
<div class="card-body" :class="{ 'is-loading': trendLoading }">
|
||||
@ -43,7 +55,9 @@
|
||||
</div>
|
||||
<div class="content-card distribution-card">
|
||||
<div class="card-header">
|
||||
<span class="card-icon"><BarChartOutlined /></span>
|
||||
<span class="card-icon">
|
||||
<BarChartOutlined />
|
||||
</span>
|
||||
<h3>课程分布</h3>
|
||||
</div>
|
||||
<div class="card-body" :class="{ 'is-loading': distributionLoading }">
|
||||
@ -58,21 +72,21 @@
|
||||
<!-- 近期活动 -->
|
||||
<div class="content-card activities-card">
|
||||
<div class="card-header">
|
||||
<span class="card-icon"><CalendarOutlined /></span>
|
||||
<span class="card-icon">
|
||||
<CalendarOutlined />
|
||||
</span>
|
||||
<h3>近期课程活动</h3>
|
||||
</div>
|
||||
<div class="card-body" :class="{ 'is-loading': loading }">
|
||||
<a-spin v-if="loading" />
|
||||
<div v-else-if="recentActivities.length === 0" class="empty-state">
|
||||
<span class="empty-icon"><InboxOutlined /></span>
|
||||
<span class="empty-icon">
|
||||
<InboxOutlined />
|
||||
</span>
|
||||
<p>暂无近期活动</p>
|
||||
</div>
|
||||
<div v-else class="activity-list">
|
||||
<div
|
||||
v-for="item in recentActivities"
|
||||
:key="item.id"
|
||||
class="activity-item"
|
||||
>
|
||||
<div v-for="item in recentActivities" :key="item.id" class="activity-item">
|
||||
<div class="activity-avatar">
|
||||
<BookOutlined />
|
||||
</div>
|
||||
@ -88,28 +102,30 @@
|
||||
<!-- 教师活跃度排行 -->
|
||||
<div class="content-card teachers-card">
|
||||
<div class="card-header">
|
||||
<span class="card-icon"><TrophyOutlined /></span>
|
||||
<span class="card-icon">
|
||||
<TrophyOutlined />
|
||||
</span>
|
||||
<h3>教师活跃度排行</h3>
|
||||
</div>
|
||||
<div class="card-body" :class="{ 'is-loading': loading }">
|
||||
<a-spin v-if="loading" />
|
||||
<div v-else-if="activeTeachers.length === 0" class="empty-state">
|
||||
<span class="empty-icon"><TeamOutlined /></span>
|
||||
<span class="empty-icon">
|
||||
<TeamOutlined />
|
||||
</span>
|
||||
<p>暂无数据</p>
|
||||
</div>
|
||||
<div v-else class="teacher-list">
|
||||
<div
|
||||
v-for="(item, index) in activeTeachers"
|
||||
:key="item.id"
|
||||
class="teacher-item"
|
||||
>
|
||||
<div v-for="(item, index) in activeTeachers" :key="item.id" class="teacher-item">
|
||||
<div class="rank-badge" :class="'rank-' + (index + 1)">
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
<div class="teacher-info">
|
||||
<div class="teacher-name">{{ item.name }}</div>
|
||||
<div class="teacher-lessons">
|
||||
<span class="lesson-icon"><ReadOutlined /></span>
|
||||
<span class="lesson-icon">
|
||||
<ReadOutlined />
|
||||
</span>
|
||||
授课 {{ item.lessonCount }} 次
|
||||
</div>
|
||||
</div>
|
||||
@ -125,29 +141,25 @@
|
||||
<!-- 课程使用统计 -->
|
||||
<div class="course-stats-card">
|
||||
<div class="card-header">
|
||||
<span class="card-icon"><BarChartOutlined /></span>
|
||||
<span class="card-icon">
|
||||
<BarChartOutlined />
|
||||
</span>
|
||||
<h3>课程使用统计</h3>
|
||||
<div class="header-extra">
|
||||
<a-range-picker
|
||||
v-model:value="dateRange"
|
||||
@change="loadCourseStats"
|
||||
:placeholder="['开始日期', '结束日期']"
|
||||
style="width: 240px;"
|
||||
/>
|
||||
<a-range-picker v-model:value="dateRange" @change="loadCourseStats" :placeholder="['开始日期', '结束日期']"
|
||||
style="width: 240px;" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body" :class="{ 'is-loading': courseStatsLoading }">
|
||||
<a-spin v-if="courseStatsLoading" />
|
||||
<div v-else-if="courseStats.length === 0" class="empty-state">
|
||||
<span class="empty-icon"><LineChartOutlined /></span>
|
||||
<span class="empty-icon">
|
||||
<LineChartOutlined />
|
||||
</span>
|
||||
<p>暂无课程使用数据</p>
|
||||
</div>
|
||||
<div v-else class="course-list">
|
||||
<div
|
||||
v-for="(item, index) in courseStats"
|
||||
:key="item.courseId"
|
||||
class="course-item"
|
||||
>
|
||||
<div v-for="(item, index) in courseStats" :key="item.courseId" class="course-item">
|
||||
<div class="course-rank" :class="'top-' + (index + 1)">
|
||||
<TrophyFilled v-if="index < 3" class="rank-crown" :style="getTrophyColor(index)" />
|
||||
<span v-else>{{ index + 1 }}</span>
|
||||
@ -155,13 +167,10 @@
|
||||
<div class="course-name">{{ item.courseName }}</div>
|
||||
<div class="course-progress">
|
||||
<div class="progress-bar">
|
||||
<div
|
||||
class="progress-fill"
|
||||
:style="{
|
||||
<div class="progress-fill" :style="{
|
||||
width: getUsagePercent(item.usageCount) + '%',
|
||||
background: getProgressGradient(index)
|
||||
}"
|
||||
></div>
|
||||
}"></div>
|
||||
</div>
|
||||
<span class="progress-value">{{ item.usageCount }}次</span>
|
||||
</div>
|
||||
@ -174,7 +183,9 @@
|
||||
<div class="export-section">
|
||||
<div class="content-card export-card">
|
||||
<div class="card-header">
|
||||
<span class="card-icon"><DownloadOutlined /></span>
|
||||
<span class="card-icon">
|
||||
<DownloadOutlined />
|
||||
</span>
|
||||
<h3>数据导出</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@ -717,13 +728,28 @@ onUnmounted(() => {
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.decoration:nth-child(2) { animation-delay: 0.5s; }
|
||||
.decoration:nth-child(3) { animation-delay: 1s; }
|
||||
.decoration:nth-child(4) { animation-delay: 1.5s; }
|
||||
.decoration:nth-child(2) {
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
.decoration:nth-child(3) {
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
.decoration:nth-child(4) {
|
||||
animation-delay: 1.5s;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
.rank-badge.rank-1 { background: linear-gradient(135deg, #FFD700 0%, #FFA500 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%); }
|
||||
.rank-badge.rank-1 {
|
||||
background: linear-gradient(135deg, #FFD700 0%, #FFA500 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 {
|
||||
flex: 1;
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
<template>
|
||||
<a-layout class="school-layout">
|
||||
<a-layout class="school-layout" :class="{ 'is-collapsed': collapsed }">
|
||||
<a-layout-sider
|
||||
v-model:collapsed="collapsed"
|
||||
:trigger="null"
|
||||
collapsible
|
||||
class="school-sider"
|
||||
:width="240"
|
||||
:collapsed-width="80"
|
||||
>
|
||||
<div class="logo">
|
||||
<img src="/logo.png" alt="Logo" class="logo-img" />
|
||||
@ -15,6 +17,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sider-scroll">
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
v-model:openKeys="openKeys"
|
||||
@ -114,9 +117,10 @@
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
</a-menu>
|
||||
</div>
|
||||
</a-layout-sider>
|
||||
|
||||
<a-layout>
|
||||
<a-layout class="school-main">
|
||||
<a-layout-header class="school-header">
|
||||
<div class="header-left">
|
||||
<MenuUnfoldOutlined
|
||||
@ -287,6 +291,10 @@ $text-secondary: #666666;
|
||||
$border-color: #E8E8E8;
|
||||
$bg-light: #FAFAFA;
|
||||
|
||||
$sider-width: 240px;
|
||||
$sider-collapsed-width: 80px;
|
||||
$header-height: 64px;
|
||||
|
||||
.school-layout {
|
||||
min-height: 100vh;
|
||||
background: $bg-light;
|
||||
@ -296,6 +304,19 @@ $bg-light: #FAFAFA;
|
||||
background: white !important;
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.06);
|
||||
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 {
|
||||
height: 80px;
|
||||
@ -344,6 +365,12 @@ $bg-light: #FAFAFA;
|
||||
}
|
||||
}
|
||||
|
||||
.sider-scroll {
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.side-menu {
|
||||
border-right: none !important;
|
||||
padding: 8px 12px;
|
||||
@ -423,6 +450,11 @@ $bg-light: #FAFAFA;
|
||||
}
|
||||
}
|
||||
|
||||
.school-main {
|
||||
margin-left: $sider-width;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.school-header {
|
||||
background: white;
|
||||
padding: 0 24px;
|
||||
@ -431,6 +463,12 @@ $bg-light: #FAFAFA;
|
||||
align-items: center;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
border-bottom: 1px solid $border-color;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: $sider-width;
|
||||
height: $header-height;
|
||||
z-index: 90;
|
||||
|
||||
.trigger {
|
||||
font-size: 18px;
|
||||
@ -459,10 +497,21 @@ $bg-light: #FAFAFA;
|
||||
|
||||
.school-content {
|
||||
margin: 20px;
|
||||
margin-top: calc(#{$header-height} + 20px);
|
||||
padding: 24px;
|
||||
background: white;
|
||||
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);
|
||||
}
|
||||
|
||||
.school-layout.is-collapsed {
|
||||
.school-main {
|
||||
margin-left: $sider-collapsed-width;
|
||||
}
|
||||
|
||||
.school-header {
|
||||
left: $sider-collapsed-width;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
<template>
|
||||
<a-layout class="teacher-layout">
|
||||
<a-layout class="teacher-layout" :class="{ 'is-collapsed': collapsed }">
|
||||
<a-layout-sider
|
||||
v-model:collapsed="collapsed"
|
||||
:trigger="null"
|
||||
collapsible
|
||||
class="teacher-sider"
|
||||
:width="240"
|
||||
:collapsed-width="80"
|
||||
>
|
||||
<div class="logo">
|
||||
<img src="/logo.png" alt="Logo" class="logo-img" />
|
||||
@ -15,6 +17,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sider-scroll">
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
mode="inline"
|
||||
@ -60,9 +63,10 @@
|
||||
<span>成长档案</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</div>
|
||||
</a-layout-sider>
|
||||
|
||||
<a-layout>
|
||||
<a-layout class="teacher-main">
|
||||
<a-layout-header class="teacher-header">
|
||||
<div class="header-left">
|
||||
<MenuUnfoldOutlined
|
||||
@ -204,6 +208,10 @@ $text-secondary: #666666;
|
||||
$border-color: #E8E8E8;
|
||||
$bg-light: #FAFAFA;
|
||||
|
||||
$sider-width: 240px;
|
||||
$sider-collapsed-width: 80px;
|
||||
$header-height: 64px;
|
||||
|
||||
.teacher-layout {
|
||||
min-height: 100vh;
|
||||
background: $bg-light;
|
||||
@ -213,6 +221,19 @@ $bg-light: #FAFAFA;
|
||||
background: white !important;
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.06);
|
||||
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 {
|
||||
height: 80px;
|
||||
@ -261,6 +282,12 @@ $bg-light: #FAFAFA;
|
||||
}
|
||||
}
|
||||
|
||||
.sider-scroll {
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.side-menu {
|
||||
border-right: none !important;
|
||||
padding: 8px 12px;
|
||||
@ -303,6 +330,11 @@ $bg-light: #FAFAFA;
|
||||
}
|
||||
}
|
||||
|
||||
.teacher-main {
|
||||
margin-left: $sider-width;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.teacher-header {
|
||||
background: white;
|
||||
padding: 0 24px;
|
||||
@ -311,6 +343,12 @@ $bg-light: #FAFAFA;
|
||||
align-items: center;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
border-bottom: 1px solid $border-color;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: $sider-width;
|
||||
height: $header-height;
|
||||
z-index: 90;
|
||||
|
||||
.trigger {
|
||||
font-size: 18px;
|
||||
@ -339,10 +377,21 @@ $bg-light: #FAFAFA;
|
||||
|
||||
.teacher-content {
|
||||
margin: 20px;
|
||||
margin-top: calc(#{$header-height} + 20px);
|
||||
padding: 24px;
|
||||
background: white;
|
||||
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);
|
||||
}
|
||||
|
||||
.teacher-layout.is-collapsed {
|
||||
.teacher-main {
|
||||
margin-left: $sider-collapsed-width;
|
||||
}
|
||||
|
||||
.teacher-header {
|
||||
left: $sider-collapsed-width;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -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 + " 条记录");
|
||||
}
|
||||
}
|
||||
@ -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 迁移");
|
||||
}
|
||||
}
|
||||
@ -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"));
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user