菜单顶部栏优化

This commit is contained in:
zhonghua 2026-02-28 06:44:56 +08:00
parent bb3db79758
commit 0d4d9f5768
14 changed files with 400 additions and 97 deletions

View File

@ -1,4 +1,4 @@
DATABASE_URL="file:/Users/retirado/ccProgram/reading-platform-backend/dev.db" DATABASE_URL="file:./dev.db"
NODE_ENV=development NODE_ENV=development
PORT=3000 PORT=3000
JWT_SECRET="your-super-secret-jwt-key" JWT_SECRET="your-super-secret-jwt-key"

View File

@ -24,7 +24,7 @@
"@nestjs/platform-express": "^10.4.22", "@nestjs/platform-express": "^10.4.22",
"@nestjs/schedule": "^6.1.1", "@nestjs/schedule": "^6.1.1",
"@nestjs/throttler": "^5.2.0", "@nestjs/throttler": "^5.2.0",
"@prisma/client": "^5.8.0", "@prisma/client": "^5.22.0",
"@types/multer": "^2.0.0", "@types/multer": "^2.0.0",
"ali-oss": "^6.18.1", "ali-oss": "^6.18.1",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
@ -54,7 +54,7 @@
"@typescript-eslint/parser": "^6.18.0", "@typescript-eslint/parser": "^6.18.0",
"eslint": "^8.56.0", "eslint": "^8.56.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"prisma": "^5.8.0", "prisma": "^5.22.0",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"ts-jest": "^29.1.1", "ts-jest": "^29.1.1",
"ts-loader": "^9.5.1", "ts-loader": "^9.5.1",

View File

@ -1,21 +1,21 @@
import { NestFactory } from '@nestjs/core'; import { NestFactory } from "@nestjs/core";
import { ValidationPipe, Logger } from '@nestjs/common'; import { ValidationPipe, Logger } from "@nestjs/common";
import { ConfigService } from '@nestjs/config'; import { ConfigService } from "@nestjs/config";
import { NestExpressApplication } from '@nestjs/platform-express'; import { NestExpressApplication } from "@nestjs/platform-express";
import { join } from 'path'; import { join } from "path";
import { AppModule } from './app.module'; import { AppModule } from "./app.module";
import * as compression from 'compression'; import * as compression from "compression";
import { HttpExceptionFilter } from './common/filters/http-exception.filter'; import { HttpExceptionFilter } from "./common/filters/http-exception.filter";
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule, { const app = await NestFactory.create<NestExpressApplication>(AppModule, {
logger: ['error', 'warn', 'log', 'debug', 'verbose'], logger: ["error", "warn", "log", "debug", "verbose"],
}); });
console.log("bootstrap");
// 增加请求体大小限制(支持上传大文件 base64 // 增加请求体大小限制(支持上传大文件 base64
// 1GB 文件编码后约 1.33GB,加上其他字段,设置为 1500mb // 1GB 文件编码后约 1.33GB,加上其他字段,设置为 1500mb
app.useBodyParser('json', { limit: '1500mb' }); app.useBodyParser("json", { limit: "1500mb" });
app.useBodyParser('urlencoded', { limit: '1500mb', extended: true }); app.useBodyParser("urlencoded", { limit: "1500mb", extended: true });
const configService = app.get(ConfigService); const configService = app.get(ConfigService);
@ -28,7 +28,7 @@ async function bootstrap() {
transformOptions: { transformOptions: {
enableImplicitConversion: true, enableImplicitConversion: true,
}, },
}), })
); );
// 全局异常过滤器 // 全局异常过滤器
@ -39,23 +39,23 @@ async function bootstrap() {
// CORS // CORS
app.enableCors({ app.enableCors({
origin: configService.get('FRONTEND_URL') || 'http://localhost:5173', origin: configService.get("FRONTEND_URL") || "http://localhost:5173",
credentials: true, credentials: true,
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS', methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS",
allowedHeaders: 'Content-Type, Accept, Authorization', allowedHeaders: "Content-Type, Accept, Authorization",
}); });
// 配置静态文件服务(用于访问上传的文件) // 配置静态文件服务(用于访问上传的文件)
// 使用绝对路径确保在编译后也能正确找到 uploads 目录 // 使用绝对路径确保在编译后也能正确找到 uploads 目录
const uploadsPath = join(__dirname, '..', '..', 'uploads'); const uploadsPath = join(__dirname, "..", "..", "uploads");
app.useStaticAssets(uploadsPath, { app.useStaticAssets(uploadsPath, {
prefix: '/uploads/', prefix: "/uploads/",
}); });
// API前缀 // API前缀
app.setGlobalPrefix('api/v1'); app.setGlobalPrefix("api/v1");
const port = configService.get<number>('PORT') || 3000; const port = configService.get<number>("PORT") || 3000;
await app.listen(port); await app.listen(port);
console.log(` console.log(`

View File

@ -1,18 +0,0 @@
#!/bin/bash
# 后端启动脚本
# 确保从正确目录启动
cd "$(dirname "$0")"
echo "🚀 正在启动后端服务..."
echo "📂 工作目录: $(pwd)"
# 检查 node_modules 是否存在
if [ ! -d "node_modules" ]; then
echo "📦 正在安装依赖..."
npm install
fi
# 启动后端
npm run start:dev

View File

@ -7,7 +7,8 @@
"dev": "vite", "dev": "vite",
"build": "vue-tsc && vite build", "build": "vue-tsc && vite build",
"preview": "vite preview", "preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"test:e2e": "playwright test"
}, },
"dependencies": { "dependencies": {
"@ant-design/icons-vue": "^7.0.1", "@ant-design/icons-vue": "^7.0.1",
@ -27,10 +28,12 @@
"vue-router": "^4.3.0" "vue-router": "^4.3.0"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.58.2",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^20.11.28", "@types/node": "^20.11.28",
"@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue": "^5.0.4",
"@vue/tsconfig": "^0.5.1", "@vue/tsconfig": "^0.5.1",
"@playwright/test": "^1.50.0",
"sass-embedded": "^1.97.3", "sass-embedded": "^1.97.3",
"typescript": "~5.4.0", "typescript": "~5.4.0",
"unplugin-auto-import": "^0.17.5", "unplugin-auto-import": "^0.17.5",

View File

@ -0,0 +1,27 @@
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
timeout: 60 * 1000,
expect: {
timeout: 10 * 1000,
},
use: {
baseURL: 'http://localhost:5173',
headless: true,
trace: 'on-first-retry',
},
webServer: {
command: 'npm run dev',
port: 5173,
reuseExistingServer: true,
timeout: 120 * 1000,
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});

View File

@ -11,9 +11,14 @@ declare module 'vue' {
AAvatar: typeof import('ant-design-vue/es')['Avatar'] AAvatar: typeof import('ant-design-vue/es')['Avatar']
ABadge: typeof import('ant-design-vue/es')['Badge'] ABadge: typeof import('ant-design-vue/es')['Badge']
AButton: typeof import('ant-design-vue/es')['Button'] AButton: typeof import('ant-design-vue/es')['Button']
AButtonGroup: typeof import('ant-design-vue/es')['ButtonGroup']
ACard: typeof import('ant-design-vue/es')['Card'] ACard: typeof import('ant-design-vue/es')['Card']
ACheckbox: typeof import('ant-design-vue/es')['Checkbox'] ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup']
ACol: typeof import('ant-design-vue/es')['Col'] 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'] ADescriptions: typeof import('ant-design-vue/es')['Descriptions']
ADescriptionsItem: typeof import('ant-design-vue/es')['DescriptionsItem'] ADescriptionsItem: typeof import('ant-design-vue/es')['DescriptionsItem']
ADivider: typeof import('ant-design-vue/es')['Divider'] ADivider: typeof import('ant-design-vue/es')['Divider']
@ -22,6 +27,8 @@ declare module 'vue' {
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']
AFormItem: typeof import('ant-design-vue/es')['FormItem'] 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'] AInput: typeof import('ant-design-vue/es')['Input']
AInputNumber: typeof import('ant-design-vue/es')['InputNumber'] AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword'] AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
@ -30,24 +37,47 @@ declare module 'vue' {
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'] APageHeader: typeof import('ant-design-vue/es')['PageHeader']
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']
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
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']
AResult: typeof import('ant-design-vue/es')['Result']
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']
ASelectOptGroup: typeof import('ant-design-vue/es')['SelectOptGroup']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption'] ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ASkeleton: typeof import('ant-design-vue/es')['Skeleton'] 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']
AStatistic: typeof import('ant-design-vue/es')['Statistic'] 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'] 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']
ATimeline: typeof import('ant-design-vue/es')['Timeline']
ATimelineItem: typeof import('ant-design-vue/es')['TimelineItem']
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']
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']
NotificationBell: typeof import('./components/NotificationBell.vue')['default'] NotificationBell: typeof import('./components/NotificationBell.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']

View File

@ -1,5 +1,5 @@
<template> <template>
<a-layout class="admin-layout"> <a-layout :class="['admin-layout', { 'is-collapsed': collapsed }]">
<a-layout-sider <a-layout-sider
v-model:collapsed="collapsed" v-model:collapsed="collapsed"
:trigger="null" :trigger="null"
@ -14,6 +14,7 @@
</div> </div>
</div> </div>
<div class="sider-menu-wrapper">
<a-menu <a-menu
v-model:selectedKeys="selectedKeys" v-model:selectedKeys="selectedKeys"
mode="inline" mode="inline"
@ -57,6 +58,7 @@
<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>
@ -200,9 +202,21 @@ $bg-dark: #111827;
.admin-layout { .admin-layout {
min-height: 100vh; min-height: 100vh;
background: $bg-light; background: $bg-light;
position: relative;
padding-left: 200px; //
padding-top: 64px; //
}
.admin-layout.is-collapsed {
padding-left: 80px; //
} }
.admin-sider { .admin-sider {
position: fixed;
left: 0;
top: 0;
bottom: 0;
z-index: 100;
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;
@ -297,6 +311,25 @@ $bg-dark: #111827;
} }
} }
.sider-menu-wrapper {
height: calc(100vh - 72px);
overflow: auto;
//
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(148, 163, 184, 0.6);
border-radius: 999px;
}
}
.admin-header { .admin-header {
background: white; background: white;
padding: 0 24px; padding: 0 24px;
@ -305,6 +338,11 @@ $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;
left: 200px;
right: 0;
z-index: 90;
.trigger { .trigger {
font-size: 18px; font-size: 18px;
@ -331,6 +369,10 @@ $bg-dark: #111827;
} }
} }
.admin-layout.is-collapsed .admin-header {
left: 80px;
}
.admin-content { .admin-content {
margin: 20px; margin: 20px;
padding: 24px; padding: 24px;

View File

@ -71,6 +71,17 @@
登录 登录
</a-button> </a-button>
</a-form-item> </a-form-item>
<a-form-item>
<a-button
size="large"
block
class="quick-test-btn"
@click="handleQuickTest"
>
一键测试{{ currentRoleLabel }}账号
</a-button>
</a-form-item>
</a-form> </a-form>
<!-- 测试账号 --> <!-- 测试账号 -->
@ -132,6 +143,11 @@ const formState = reactive({
const loading = ref(false); const loading = ref(false);
const currentRoleLabel = computed(() => {
const found = roles.find(r => r.value === formState.role);
return found ? found.label : '';
});
const selectRole = (role: string) => { const selectRole = (role: string) => {
formState.role = role; formState.role = role;
}; };
@ -158,6 +174,17 @@ const handleLogin = async () => {
loading.value = false; loading.value = false;
} }
}; };
const handleQuickTest = async () => {
const found = testAccounts.find(t => t.role === formState.role);
if (!found) {
message.warning('当前角色暂不支持一键测试');
return;
}
fillAccount(found);
await handleLogin();
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@ -380,6 +407,13 @@ $bg-cream: #FEFEFE;
transform: translateY(0); transform: translateY(0);
} }
} }
.quick-test-btn {
height: 44px;
border-radius: 12px;
font-size: 14px;
margin-top: 4px;
}
} }
// - // -

View File

@ -1,5 +1,5 @@
<template> <template>
<a-layout class="parent-layout"> <a-layout :class="['parent-layout', { 'is-collapsed': collapsed }]">
<!-- 桌面端侧边栏 --> <!-- 桌面端侧边栏 -->
<a-layout-sider <a-layout-sider
v-if="!isMobile" v-if="!isMobile"
@ -17,6 +17,7 @@
</div> </div>
</div> </div>
<div class="sider-menu-wrapper">
<a-menu <a-menu
v-model:selectedKeys="selectedKeys" v-model:selectedKeys="selectedKeys"
mode="inline" mode="inline"
@ -46,6 +47,7 @@
<span>成长档案</span> <span>成长档案</span>
</a-menu-item> </a-menu-item>
</a-menu> </a-menu>
</div>
</a-layout-sider> </a-layout-sider>
<!-- 移动端抽屉菜单 --> <!-- 移动端抽屉菜单 -->
@ -307,6 +309,7 @@ $bg-light: #FAFAFA;
.parent-layout { .parent-layout {
min-height: 100vh; min-height: 100vh;
background: $bg-light; background: $bg-light;
position: relative;
} }
.main-layout { .main-layout {
@ -315,8 +318,24 @@ $bg-light: #FAFAFA;
min-height: 100vh; min-height: 100vh;
} }
@media screen and (min-width: 769px) {
.parent-layout {
padding-left: 220px; //
padding-top: 64px; //
}
.parent-layout.is-collapsed {
padding-left: 80px; //
}
}
// //
.parent-sider { .parent-sider {
position: fixed;
left: 0;
top: 0;
bottom: 0;
z-index: 100;
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;
@ -391,6 +410,25 @@ $bg-light: #FAFAFA;
} }
} }
.sider-menu-wrapper {
height: calc(100vh - 80px);
overflow: auto;
//
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(148, 163, 184, 0.6);
border-radius: 999px;
}
}
// //
.parent-header { .parent-header {
background: white; background: white;
@ -402,6 +440,12 @@ $bg-light: #FAFAFA;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
height: 64px; height: 64px;
position: fixed;
top: 0;
left: 220px;
right: 0;
z-index: 90;
.trigger { .trigger {
font-size: 18px; font-size: 18px;
cursor: pointer; cursor: pointer;
@ -427,6 +471,12 @@ $bg-light: #FAFAFA;
} }
} }
@media screen and (min-width: 769px) {
.parent-layout.is-collapsed .parent-header {
left: 80px;
}
}
// //
.parent-content { .parent-content {
margin: 20px; margin: 20px;

View File

@ -1,5 +1,5 @@
<template> <template>
<a-layout class="school-layout"> <a-layout :class="['school-layout', { 'is-collapsed': collapsed }]">
<a-layout-sider <a-layout-sider
v-model:collapsed="collapsed" v-model:collapsed="collapsed"
:trigger="null" :trigger="null"
@ -15,6 +15,7 @@
</div> </div>
</div> </div>
<div class="sider-menu-wrapper">
<a-menu <a-menu
v-model:selectedKeys="selectedKeys" v-model:selectedKeys="selectedKeys"
v-model:openKeys="openKeys" v-model:openKeys="openKeys"
@ -110,6 +111,7 @@
</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>
@ -284,9 +286,21 @@ $bg-light: #FAFAFA;
.school-layout { .school-layout {
min-height: 100vh; min-height: 100vh;
background: $bg-light; background: $bg-light;
position: relative;
padding-left: 200px; //
padding-top: 64px; //
}
.school-layout.is-collapsed {
padding-left: 80px; //
} }
.school-sider { .school-sider {
position: fixed;
left: 0;
top: 0;
bottom: 0;
z-index: 100;
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;
@ -417,6 +431,25 @@ $bg-light: #FAFAFA;
} }
} }
.sider-menu-wrapper {
height: calc(100vh - 80px);
overflow: auto;
//
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(148, 163, 184, 0.6);
border-radius: 999px;
}
}
.school-header { .school-header {
background: white; background: white;
padding: 0 24px; padding: 0 24px;
@ -425,6 +458,11 @@ $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;
left: 200px;
right: 0;
z-index: 90;
.trigger { .trigger {
font-size: 18px; font-size: 18px;
@ -451,6 +489,10 @@ $bg-light: #FAFAFA;
} }
} }
.school-layout.is-collapsed .school-header {
left: 80px;
}
.school-content { .school-content {
margin: 20px; margin: 20px;
padding: 24px; padding: 24px;

View File

@ -1,5 +1,5 @@
<template> <template>
<a-layout class="teacher-layout"> <a-layout :class="['teacher-layout', { 'is-collapsed': collapsed }]">
<a-layout-sider <a-layout-sider
v-model:collapsed="collapsed" v-model:collapsed="collapsed"
:trigger="null" :trigger="null"
@ -15,6 +15,7 @@
</div> </div>
</div> </div>
<div class="sider-menu-wrapper">
<a-menu <a-menu
v-model:selectedKeys="selectedKeys" v-model:selectedKeys="selectedKeys"
mode="inline" mode="inline"
@ -56,6 +57,7 @@
<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>
@ -200,9 +202,21 @@ $bg-light: #FAFAFA;
.teacher-layout { .teacher-layout {
min-height: 100vh; min-height: 100vh;
background: $bg-light; background: $bg-light;
position: relative;
padding-left: 200px; //
padding-top: 64px; //
}
.teacher-layout.is-collapsed {
padding-left: 80px; //
} }
.teacher-sider { .teacher-sider {
position: fixed;
left: 0;
top: 0;
bottom: 0;
z-index: 100;
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;
@ -296,6 +310,25 @@ $bg-light: #FAFAFA;
} }
} }
.sider-menu-wrapper {
height: calc(100vh - 80px);
overflow: auto;
//
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(148, 163, 184, 0.6);
border-radius: 999px;
}
}
.teacher-header { .teacher-header {
background: white; background: white;
padding: 0 24px; padding: 0 24px;
@ -304,6 +337,11 @@ $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;
left: 200px;
right: 0;
z-index: 90;
.trigger { .trigger {
font-size: 18px; font-size: 18px;
@ -330,6 +368,10 @@ $bg-light: #FAFAFA;
} }
} }
.teacher-layout.is-collapsed .teacher-header {
left: 80px;
}
.teacher-content { .teacher-content {
margin: 20px; margin: 20px;
padding: 24px; padding: 24px;

View File

@ -1,18 +0,0 @@
#!/bin/bash
# 前端启动脚本
# 确保从正确目录启动
cd "$(dirname "$0")"
echo "🚀 正在启动前端服务..."
echo "📂 工作目录: $(pwd)"
# 检查 node_modules 是否存在
if [ ! -d "node_modules" ]; then
echo "📦 正在安装依赖..."
npm install
fi
# 启动前端
npm run dev

View File

@ -0,0 +1,69 @@
import { test, expect, Page } from '@playwright/test';
type RoleKey = 'admin' | 'school' | 'teacher' | 'parent';
const ROLE_CONFIG: Record<
RoleKey,
{
label: string;
account: string;
password: string;
dashboardPath: string;
}
> = {
admin: {
label: '超管',
account: 'admin',
password: 'admin123',
dashboardPath: '/admin/dashboard',
},
school: {
label: '学校',
account: 'school',
password: '123456',
dashboardPath: '/school/dashboard',
},
teacher: {
label: '教师',
account: 'teacher1',
password: '123456',
dashboardPath: '/teacher/dashboard',
},
parent: {
label: '家长',
account: 'parent1',
password: '123456',
dashboardPath: '/parent/dashboard',
},
};
async function loginAsRole(page: Page, role: RoleKey) {
const cfg = ROLE_CONFIG[role];
await page.goto('/login');
await page.getByText(cfg.label).click();
await page.getByPlaceholder('请输入账号').fill(cfg.account);
await page.getByPlaceholder('请输入密码').fill(cfg.password);
await page.getByRole('button', { name: '登录' }).click();
await page.waitForURL(`**${cfg.dashboardPath}*`);
await expect(page).toHaveURL(new RegExp(`${cfg.dashboardPath}`));
await expect(page).toHaveTitle(/幼儿阅读教学服务平台/);
}
test.describe('从登录开始的一键全角色流程', () => {
for (const role of Object.keys(ROLE_CONFIG) as RoleKey[]) {
test(`角色:${ROLE_CONFIG[role].label} 登录并进入首页`, async ({ page }) => {
await loginAsRole(page, role);
await expect(page.getByText('数据看板').or(page.getByText('首页'))).toBeVisible({
timeout: 10_000,
});
});
}
});