菜单顶部栏优化
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
|
NODE_ENV=development
|
||||||
PORT=3000
|
PORT=3000
|
||||||
JWT_SECRET="your-super-secret-jwt-key"
|
JWT_SECRET="your-super-secret-jwt-key"
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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(`
|
||||||
|
|||||||
@ -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",
|
"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",
|
||||||
|
|||||||
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']
|
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']
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 测试账号 - 暂时隐藏
|
// 测试账号 - 暂时隐藏
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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