菜单顶部栏优化
This commit is contained in:
parent
bb3db79758
commit
0d4d9f5768
@ -1,4 +1,4 @@
|
||||
DATABASE_URL="file:/Users/retirado/ccProgram/reading-platform-backend/dev.db"
|
||||
DATABASE_URL="file:./dev.db"
|
||||
NODE_ENV=development
|
||||
PORT=3000
|
||||
JWT_SECRET="your-super-secret-jwt-key"
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
"@nestjs/platform-express": "^10.4.22",
|
||||
"@nestjs/schedule": "^6.1.1",
|
||||
"@nestjs/throttler": "^5.2.0",
|
||||
"@prisma/client": "^5.8.0",
|
||||
"@prisma/client": "^5.22.0",
|
||||
"@types/multer": "^2.0.0",
|
||||
"ali-oss": "^6.18.1",
|
||||
"bcrypt": "^5.1.1",
|
||||
@ -54,7 +54,7 @@
|
||||
"@typescript-eslint/parser": "^6.18.0",
|
||||
"eslint": "^8.56.0",
|
||||
"jest": "^29.7.0",
|
||||
"prisma": "^5.8.0",
|
||||
"prisma": "^5.22.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-loader": "^9.5.1",
|
||||
|
||||
@ -1,21 +1,21 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { ValidationPipe, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import { join } from 'path';
|
||||
import { AppModule } from './app.module';
|
||||
import * as compression from 'compression';
|
||||
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
||||
import { NestFactory } from "@nestjs/core";
|
||||
import { ValidationPipe, Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { join } from "path";
|
||||
import { AppModule } from "./app.module";
|
||||
import * as compression from "compression";
|
||||
import { HttpExceptionFilter } from "./common/filters/http-exception.filter";
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
|
||||
logger: ['error', 'warn', 'log', 'debug', 'verbose'],
|
||||
logger: ["error", "warn", "log", "debug", "verbose"],
|
||||
});
|
||||
|
||||
console.log("bootstrap");
|
||||
// 增加请求体大小限制(支持上传大文件 base64)
|
||||
// 1GB 文件编码后约 1.33GB,加上其他字段,设置为 1500mb
|
||||
app.useBodyParser('json', { limit: '1500mb' });
|
||||
app.useBodyParser('urlencoded', { limit: '1500mb', extended: true });
|
||||
app.useBodyParser("json", { limit: "1500mb" });
|
||||
app.useBodyParser("urlencoded", { limit: "1500mb", extended: true });
|
||||
|
||||
const configService = app.get(ConfigService);
|
||||
|
||||
@ -28,7 +28,7 @@ async function bootstrap() {
|
||||
transformOptions: {
|
||||
enableImplicitConversion: true,
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
// 全局异常过滤器
|
||||
@ -39,23 +39,23 @@ async function bootstrap() {
|
||||
|
||||
// CORS
|
||||
app.enableCors({
|
||||
origin: configService.get('FRONTEND_URL') || 'http://localhost:5173',
|
||||
origin: configService.get("FRONTEND_URL") || "http://localhost:5173",
|
||||
credentials: true,
|
||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
|
||||
allowedHeaders: 'Content-Type, Accept, Authorization',
|
||||
methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS",
|
||||
allowedHeaders: "Content-Type, Accept, Authorization",
|
||||
});
|
||||
|
||||
// 配置静态文件服务(用于访问上传的文件)
|
||||
// 使用绝对路径确保在编译后也能正确找到 uploads 目录
|
||||
const uploadsPath = join(__dirname, '..', '..', 'uploads');
|
||||
const uploadsPath = join(__dirname, "..", "..", "uploads");
|
||||
app.useStaticAssets(uploadsPath, {
|
||||
prefix: '/uploads/',
|
||||
prefix: "/uploads/",
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
||||
console.log(`
|
||||
|
||||
@ -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
|
||||
@ -7,7 +7,8 @@
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build",
|
||||
"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": {
|
||||
"@ant-design/icons-vue": "^7.0.1",
|
||||
@ -27,10 +28,12 @@
|
||||
"vue-router": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^20.11.28",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"@playwright/test": "^1.50.0",
|
||||
"sass-embedded": "^1.97.3",
|
||||
"typescript": "~5.4.0",
|
||||
"unplugin-auto-import": "^0.17.5",
|
||||
|
||||
27
reading-platform-frontend/playwright.config.ts
Normal file
27
reading-platform-frontend/playwright.config.ts
Normal 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'] },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
30
reading-platform-frontend/src/components.d.ts
vendored
30
reading-platform-frontend/src/components.d.ts
vendored
@ -11,9 +11,14 @@ declare module 'vue' {
|
||||
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']
|
||||
@ -22,6 +27,8 @@ declare module 'vue' {
|
||||
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']
|
||||
@ -30,24 +37,47 @@ declare module 'vue' {
|
||||
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']
|
||||
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']
|
||||
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']
|
||||
NotificationBell: typeof import('./components/NotificationBell.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<a-layout class="admin-layout">
|
||||
<a-layout :class="['admin-layout', { 'is-collapsed': collapsed }]">
|
||||
<a-layout-sider
|
||||
v-model:collapsed="collapsed"
|
||||
:trigger="null"
|
||||
@ -14,14 +14,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
mode="inline"
|
||||
theme="light"
|
||||
:inline-collapsed="collapsed"
|
||||
@select="handleMenuSelect"
|
||||
class="side-menu"
|
||||
>
|
||||
<div class="sider-menu-wrapper">
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
mode="inline"
|
||||
theme="light"
|
||||
:inline-collapsed="collapsed"
|
||||
@select="handleMenuSelect"
|
||||
class="side-menu"
|
||||
>
|
||||
<a-menu-item key="dashboard">
|
||||
<template #icon>
|
||||
<LayoutDashboard :size="18" :stroke-width="1.5" />
|
||||
@ -57,6 +58,7 @@
|
||||
<span>系统设置</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</div>
|
||||
</a-layout-sider>
|
||||
|
||||
<a-layout>
|
||||
@ -200,9 +202,21 @@ $bg-dark: #111827;
|
||||
.admin-layout {
|
||||
min-height: 100vh;
|
||||
background: $bg-light;
|
||||
position: relative;
|
||||
padding-left: 200px; // 侧边栏默认宽度
|
||||
padding-top: 64px; // 预留顶部栏高度
|
||||
}
|
||||
|
||||
.admin-layout.is-collapsed {
|
||||
padding-left: 80px; // 菜单收起后的宽度
|
||||
}
|
||||
|
||||
.admin-sider {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
background: white !important;
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.04);
|
||||
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 {
|
||||
background: white;
|
||||
padding: 0 24px;
|
||||
@ -305,6 +338,11 @@ $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;
|
||||
left: 200px;
|
||||
right: 0;
|
||||
z-index: 90;
|
||||
|
||||
.trigger {
|
||||
font-size: 18px;
|
||||
@ -331,6 +369,10 @@ $bg-dark: #111827;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-layout.is-collapsed .admin-header {
|
||||
left: 80px;
|
||||
}
|
||||
|
||||
.admin-content {
|
||||
margin: 20px;
|
||||
padding: 24px;
|
||||
|
||||
@ -71,6 +71,17 @@
|
||||
登录
|
||||
</a-button>
|
||||
</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>
|
||||
|
||||
<!-- 测试账号 -->
|
||||
@ -132,6 +143,11 @@ const formState = reactive({
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const currentRoleLabel = computed(() => {
|
||||
const found = roles.find(r => r.value === formState.role);
|
||||
return found ? found.label : '';
|
||||
});
|
||||
|
||||
const selectRole = (role: string) => {
|
||||
formState.role = role;
|
||||
};
|
||||
@ -158,6 +174,17 @@ const handleLogin = async () => {
|
||||
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>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@ -380,6 +407,13 @@ $bg-cream: #FEFEFE;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.quick-test-btn {
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// 测试账号 - 暂时隐藏
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<a-layout class="parent-layout">
|
||||
<a-layout :class="['parent-layout', { 'is-collapsed': collapsed }]">
|
||||
<!-- 桌面端侧边栏 -->
|
||||
<a-layout-sider
|
||||
v-if="!isMobile"
|
||||
@ -17,14 +17,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
mode="inline"
|
||||
theme="light"
|
||||
:inline-collapsed="collapsed"
|
||||
@click="handleMenuClick"
|
||||
class="side-menu"
|
||||
>
|
||||
<div class="sider-menu-wrapper">
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
mode="inline"
|
||||
theme="light"
|
||||
:inline-collapsed="collapsed"
|
||||
@click="handleMenuClick"
|
||||
class="side-menu"
|
||||
>
|
||||
<a-menu-item key="dashboard">
|
||||
<template #icon><HomeOutlined /></template>
|
||||
<span>首页</span>
|
||||
@ -46,6 +47,7 @@
|
||||
<span>成长档案</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</div>
|
||||
</a-layout-sider>
|
||||
|
||||
<!-- 移动端抽屉菜单 -->
|
||||
@ -307,6 +309,7 @@ $bg-light: #FAFAFA;
|
||||
.parent-layout {
|
||||
min-height: 100vh;
|
||||
background: $bg-light;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.main-layout {
|
||||
@ -315,8 +318,24 @@ $bg-light: #FAFAFA;
|
||||
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 {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
background: white !important;
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.06);
|
||||
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 {
|
||||
background: white;
|
||||
@ -402,6 +440,12 @@ $bg-light: #FAFAFA;
|
||||
border-bottom: 1px solid $border-color;
|
||||
height: 64px;
|
||||
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 220px;
|
||||
right: 0;
|
||||
z-index: 90;
|
||||
|
||||
.trigger {
|
||||
font-size: 18px;
|
||||
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 {
|
||||
margin: 20px;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<a-layout class="school-layout">
|
||||
<a-layout :class="['school-layout', { 'is-collapsed': collapsed }]">
|
||||
<a-layout-sider
|
||||
v-model:collapsed="collapsed"
|
||||
:trigger="null"
|
||||
@ -15,15 +15,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
v-model:openKeys="openKeys"
|
||||
mode="inline"
|
||||
theme="light"
|
||||
:inline-collapsed="collapsed"
|
||||
@click="handleMenuClick"
|
||||
class="side-menu"
|
||||
>
|
||||
<div class="sider-menu-wrapper">
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
v-model:openKeys="openKeys"
|
||||
mode="inline"
|
||||
theme="light"
|
||||
:inline-collapsed="collapsed"
|
||||
@click="handleMenuClick"
|
||||
class="side-menu"
|
||||
>
|
||||
<!-- 数据概览 - 独立一级菜单 -->
|
||||
<a-menu-item key="dashboard">
|
||||
<template #icon><DashboardOutlined /></template>
|
||||
@ -110,6 +111,7 @@
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
</a-menu>
|
||||
</div>
|
||||
</a-layout-sider>
|
||||
|
||||
<a-layout>
|
||||
@ -284,9 +286,21 @@ $bg-light: #FAFAFA;
|
||||
.school-layout {
|
||||
min-height: 100vh;
|
||||
background: $bg-light;
|
||||
position: relative;
|
||||
padding-left: 200px; // 侧边栏默认宽度
|
||||
padding-top: 64px; // 预留顶部栏高度
|
||||
}
|
||||
|
||||
.school-layout.is-collapsed {
|
||||
padding-left: 80px; // 菜单收起后的宽度
|
||||
}
|
||||
|
||||
.school-sider {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
background: white !important;
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.06);
|
||||
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 {
|
||||
background: white;
|
||||
padding: 0 24px;
|
||||
@ -425,6 +458,11 @@ $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;
|
||||
left: 200px;
|
||||
right: 0;
|
||||
z-index: 90;
|
||||
|
||||
.trigger {
|
||||
font-size: 18px;
|
||||
@ -451,6 +489,10 @@ $bg-light: #FAFAFA;
|
||||
}
|
||||
}
|
||||
|
||||
.school-layout.is-collapsed .school-header {
|
||||
left: 80px;
|
||||
}
|
||||
|
||||
.school-content {
|
||||
margin: 20px;
|
||||
padding: 24px;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<a-layout class="teacher-layout">
|
||||
<a-layout :class="['teacher-layout', { 'is-collapsed': collapsed }]">
|
||||
<a-layout-sider
|
||||
v-model:collapsed="collapsed"
|
||||
:trigger="null"
|
||||
@ -15,14 +15,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
mode="inline"
|
||||
theme="light"
|
||||
:inline-collapsed="collapsed"
|
||||
@click="handleMenuClick"
|
||||
class="side-menu"
|
||||
>
|
||||
<div class="sider-menu-wrapper">
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
mode="inline"
|
||||
theme="light"
|
||||
:inline-collapsed="collapsed"
|
||||
@click="handleMenuClick"
|
||||
class="side-menu"
|
||||
>
|
||||
<a-menu-item key="dashboard">
|
||||
<template #icon><HomeOutlined /></template>
|
||||
<span>首页</span>
|
||||
@ -56,6 +57,7 @@
|
||||
<span>成长档案</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</div>
|
||||
</a-layout-sider>
|
||||
|
||||
<a-layout>
|
||||
@ -200,9 +202,21 @@ $bg-light: #FAFAFA;
|
||||
.teacher-layout {
|
||||
min-height: 100vh;
|
||||
background: $bg-light;
|
||||
position: relative;
|
||||
padding-left: 200px; // 侧边栏默认宽度
|
||||
padding-top: 64px; // 预留顶部栏高度
|
||||
}
|
||||
|
||||
.teacher-layout.is-collapsed {
|
||||
padding-left: 80px; // 菜单收起后的宽度
|
||||
}
|
||||
|
||||
.teacher-sider {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
background: white !important;
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.06);
|
||||
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 {
|
||||
background: white;
|
||||
padding: 0 24px;
|
||||
@ -304,6 +337,11 @@ $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;
|
||||
left: 200px;
|
||||
right: 0;
|
||||
z-index: 90;
|
||||
|
||||
.trigger {
|
||||
font-size: 18px;
|
||||
@ -330,6 +368,10 @@ $bg-light: #FAFAFA;
|
||||
}
|
||||
}
|
||||
|
||||
.teacher-layout.is-collapsed .teacher-header {
|
||||
left: 80px;
|
||||
}
|
||||
|
||||
.teacher-content {
|
||||
margin: 20px;
|
||||
padding: 24px;
|
||||
|
||||
@ -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
|
||||
69
reading-platform-frontend/tests/e2e-login-flows.spec.ts
Normal file
69
reading-platform-frontend/tests/e2e-login-flows.spec.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user