feat(登录): 添加路由守卫和异步导航处理

fix(表单验证): 为用户、角色和菜单管理添加表单验证规则

test(e2e): 增加页面导航超时时间和网络空闲等待

refactor(数据库): 移除Flyway配置并更新数据源配置
This commit is contained in:
张翔
2026-03-27 14:40:55 +08:00
parent af44c23f21
commit a05368d306
9 changed files with 168 additions and 58 deletions
@@ -14,6 +14,13 @@ spring:
max-idle-time: 30m
max-life-time: 1h
acquire-timeout: 5s
datasource:
url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:55432}/${DB_NAME:manage_system}
username: ${DB_USERNAME:postgres}
password: ${DB_PASSWORD:postgres}
driver-class-name: org.postgresql.Driver
flyway:
enabled: false
security:
user:
name: disabled
@@ -1,30 +0,0 @@
package cn.novalon.manage.db.config;
import org.flywaydb.core.Flyway;
import org.springframework.boot.autoconfigure.flyway.FlywayProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import javax.sql.DataSource;
@Configuration
public class FlywayConfig {
@Bean
@Profile("!test")
public Flyway flyway(DataSource dataSource, FlywayProperties flywayProperties) {
Flyway flyway = Flyway.configure()
.dataSource(dataSource)
.locations(flywayProperties.getLocations().toArray(new String[0]))
.baselineOnMigrate(true)
.baselineVersion("0")
.table("flyway_schema_history")
.validateOnMigrate(true)
.outOfOrder(false)
.load();
flyway.migrate();
return flyway;
}
}
+29 -19
View File
@@ -35,83 +35,93 @@ export class DashboardPage {
async navigateToUserManagement() {
const systemMenu = this.page.locator('.el-sub-menu__title:has-text("系统管理")');
await systemMenu.click();
await this.page.waitForTimeout(500);
await this.page.waitForTimeout(1000);
await this.userManagementLink.click();
await this.page.waitForURL('**/users', { timeout: 10000 });
await this.page.waitForURL('**/users', { timeout: 30000 });
await this.page.waitForLoadState('networkidle');
}
async navigateToRoleManagement() {
const systemMenu = this.page.locator('.el-sub-menu__title:has-text("系统管理")');
await systemMenu.click();
await this.page.waitForTimeout(500);
await this.page.waitForTimeout(1000);
await this.roleManagementLink.click();
await this.page.waitForURL('**/roles', { timeout: 10000 });
await this.page.waitForURL('**/roles', { timeout: 30000 });
await this.page.waitForLoadState('networkidle');
}
async navigateToMenuManagement() {
const systemMenu = this.page.locator('.el-sub-menu__title:has-text("系统管理")');
await systemMenu.click();
await this.page.waitForTimeout(500);
await this.page.waitForTimeout(1000);
await this.menuManagementLink.click();
await this.page.waitForURL('**/menus', { timeout: 10000 });
await this.page.waitForURL('**/menus', { timeout: 30000 });
await this.page.waitForLoadState('networkidle');
}
async navigateToSystemConfig() {
const configMenu = this.page.locator('.el-sub-menu__title:has-text("系统配置")');
await configMenu.click();
await this.page.waitForTimeout(500);
await this.page.waitForTimeout(1000);
await this.systemConfigLink.click();
await this.page.waitForURL('**/sys/config', { timeout: 10000 });
await this.page.waitForURL('**/sys/config', { timeout: 30000 });
await this.page.waitForLoadState('networkidle');
}
async navigateToNoticeManagement() {
const notifyMenu = this.page.locator('.el-sub-menu__title:has-text("通知中心")');
await notifyMenu.click();
await this.page.waitForTimeout(500);
await this.page.waitForTimeout(1000);
await this.noticeManagementLink.click();
await this.page.waitForURL('**/notice', { timeout: 10000 });
await this.page.waitForURL('**/notice', { timeout: 30000 });
await this.page.waitForLoadState('networkidle');
}
async navigateToFileManagement() {
const fileMenu = this.page.locator('.el-sub-menu__title:has-text("文件管理")');
await fileMenu.click();
await this.page.waitForTimeout(500);
await this.page.waitForTimeout(1000);
await this.fileManagementLink.click();
await this.page.waitForURL('**/files', { timeout: 10000 });
await this.page.waitForURL('**/files', { timeout: 30000 });
await this.page.waitForLoadState('networkidle');
}
async navigateToAudit() {
const auditMenu = this.page.locator('.el-sub-menu__title:has-text("审计中心")');
await auditMenu.click();
await this.page.waitForTimeout(500);
await this.page.waitForTimeout(1000);
}
async navigateToOperationLog() {
await this.navigateToAudit();
await this.operationLogLink.click();
await this.page.waitForURL('**/oplog', { timeout: 10000 });
await this.page.waitForURL('**/oplog', { timeout: 30000 });
await this.page.waitForLoadState('networkidle');
}
async navigateToLoginLog() {
await this.navigateToAudit();
await this.loginLogLink.click();
await this.page.waitForURL('**/loginlog', { timeout: 10000 });
await this.page.waitForURL('**/loginlog', { timeout: 30000 });
await this.page.waitForLoadState('networkidle');
}
async navigateToNotification() {
const notifyMenu = this.page.locator('.el-sub-menu__title:has-text("通知中心")');
await notifyMenu.click();
await this.page.waitForTimeout(500);
await this.page.waitForTimeout(1000);
await this.noticeManagementLink.click();
await this.page.waitForURL('**/notification', { timeout: 10000 });
await this.page.waitForURL('**/notification', { timeout: 30000 });
await this.page.waitForLoadState('networkidle');
}
async navigateToDictionary() {
const configMenu = this.page.locator('.el-sub-menu__title:has-text("系统配置")');
await configMenu.click();
await this.page.waitForTimeout(500);
await this.page.waitForTimeout(1000);
await this.dictionaryLink.click();
await this.page.waitForURL('**/dict', { timeout: 10000 });
await this.page.waitForURL('**/dict', { timeout: 30000 });
await this.page.waitForLoadState('networkidle');
}
async getUsername(): Promise<string | null> {
+1 -1
View File
@@ -31,7 +31,7 @@ export class LoginPage {
console.log('Clicked login button');
try {
await this.page.waitForURL('**/dashboard', { timeout: 10000 });
await this.page.waitForURL('**/dashboard', { timeout: 30000 });
console.log('Successfully navigated to dashboard');
await this.page.waitForLoadState('networkidle');
console.log('Network idle achieved');
+23
View File
@@ -76,4 +76,27 @@ const router = createRouter({
routes
})
router.beforeEach((to, from, next) => {
try {
const token = localStorage.getItem('token')
if (to.path === '/login') {
if (token) {
next('/')
} else {
next()
}
} else {
if (token) {
next()
} else {
next('/login')
}
}
} catch (error) {
console.error('路由守卫错误:', error)
next('/login')
}
})
export default router
@@ -77,7 +77,8 @@ const onFinish = async () => {
localStorage.setItem('userId', res.userId)
localStorage.setItem('username', res.username)
ElMessage.success('登录成功')
router.push('/')
await router.push('/')
} catch (error: any) {
ElMessage.error(error.response?.data?.message || '登录失败')
} finally {
@@ -92,10 +92,15 @@
width="500px"
>
<el-form
ref="formRef"
:model="formState"
:rules="formRules"
label-width="100px"
>
<el-form-item label="菜单名称">
<el-form-item
label="菜单名称"
prop="menuName"
>
<el-input v-model="formState.menuName" />
</el-form-item>
<el-form-item label="父级菜单">
@@ -107,7 +112,10 @@
check-strictly
/>
</el-form-item>
<el-form-item label="菜单类型">
<el-form-item
label="菜单类型"
prop="menuType"
>
<el-select v-model="formState.menuType">
<el-option
value="M"
@@ -126,16 +134,21 @@
<el-form-item
v-if="formState.menuType !== 'F'"
label="路由地址"
prop="perms"
>
<el-input v-model="formState.perms" />
</el-form-item>
<el-form-item
v-if="formState.menuType === 'C'"
label="组件路径"
prop="component"
>
<el-input v-model="formState.component" />
</el-form-item>
<el-form-item label="排序">
<el-form-item
label="排序"
prop="orderNum"
>
<el-input-number v-model="formState.orderNum" />
</el-form-item>
<el-form-item label="状态">
@@ -176,10 +189,33 @@ const dataSource = ref([])
const menuTree = ref<any[]>([])
const modalVisible = ref(false)
const modalTitle = ref('')
const formRef = ref()
const formState = reactive({
id: null, menuName: '', parentId: 0, menuType: 'C', perms: '', component: '', orderNum: 0, status: '0'
})
const formRules = {
menuName: [
{ required: true, message: '请输入菜单名称', trigger: 'blur' },
{ min: 2, max: 50, message: '菜单名称长度在 2 到 50 个字符', trigger: 'blur' }
],
menuType: [
{ required: true, message: '请选择菜单类型', trigger: 'change' }
],
perms: [
{ required: true, message: '请输入路由地址', trigger: 'blur' },
{ pattern: /^\/[a-zA-Z0-9/_-]*$/, message: '路由地址格式不正确,应以/开头', trigger: 'blur' }
],
component: [
{ required: true, message: '请输入组件路径', trigger: 'blur' },
{ pattern: /^[a-zA-Z0-9/-]+$/, message: '组件路径格式不正确', trigger: 'blur' }
],
orderNum: [
{ required: true, message: '请输入排序', trigger: 'blur' },
{ type: 'number', min: 0, message: '排序必须大于等于0', trigger: 'blur' }
]
}
const fetchData = async () => {
loading.value = true
try {
@@ -223,7 +259,11 @@ const handleDelete = async (row: any) => {
}
const handleModalOk = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
if (formState.id) {
await request.put(`/menus/${formState.id}`, formState)
} else {
@@ -232,9 +272,11 @@ const handleModalOk = async () => {
ElMessage.success('操作成功')
modalVisible.value = false
fetchData()
} catch {
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('操作失败')
}
}
}
onMounted(() => fetchData())
@@ -128,17 +128,21 @@
width="500px"
>
<el-form
ref="formRef"
:model="formState"
:rules="formRules"
label-width="80px"
>
<el-form-item
label="角色名称"
prop="roleName"
required
>
<el-input v-model="formState.roleName" />
</el-form-item>
<el-form-item
label="角色标识"
prop="roleKey"
required
>
<el-input
@@ -148,6 +152,7 @@
</el-form-item>
<el-form-item
label="显示顺序"
prop="roleSort"
required
>
<el-input-number
@@ -235,6 +240,7 @@ const sortInfo = reactive({
const modalVisible = ref(false)
const modalTitle = ref('')
const formRef = ref()
const formState = reactive<CreateRoleRequest & { id?: number; status?: RoleStatus }>({
roleName: '',
roleKey: '',
@@ -243,6 +249,22 @@ const formState = reactive<CreateRoleRequest & { id?: number; status?: RoleStatu
status: RoleStatus.ACTIVE
})
const formRules = {
roleName: [
{ required: true, message: '请输入角色名称', trigger: 'blur' },
{ min: 2, max: 50, message: '角色名称长度在 2 到 50 个字符', trigger: 'blur' }
],
roleKey: [
{ required: true, message: '请输入角色标识', trigger: 'blur' },
{ min: 2, max: 50, message: '角色标识长度在 2 到 50 个字符', trigger: 'blur' },
{ pattern: /^[a-zA-Z0-9_-]+$/, message: '角色标识只能包含字母、数字、下划线和横线', trigger: 'blur' }
],
roleSort: [
{ required: true, message: '请输入显示顺序', trigger: 'blur' },
{ type: 'number', min: 1, message: '显示顺序必须大于0', trigger: 'blur' }
]
}
const permissionDialogVisible = ref(false)
const permissionTreeRef = ref()
const permissionTree = ref<any[]>([])
@@ -332,7 +354,11 @@ const handleDelete = async (row: Role) => {
}
const handleModalOk = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
if (formState.id) {
const updateData: UpdateRoleRequest = {
roleName: formState.roleName,
@@ -355,8 +381,10 @@ const handleModalOk = async () => {
modalVisible.value = false
fetchData()
} catch (error) {
if (error !== 'cancel') {
handleApiError(error)
}
}
}
const handleAssignPermissions = async (row: Role) => {
@@ -133,11 +133,14 @@
width="500px"
>
<el-form
ref="formRef"
:model="formState"
:rules="formRules"
label-width="80px"
>
<el-form-item
label="用户名"
prop="username"
required
>
<el-input
@@ -148,6 +151,7 @@
<el-form-item
v-if="!formState.id"
label="密码"
prop="password"
required
>
<el-input
@@ -241,6 +245,7 @@ const sortInfo = reactive({
const modalVisible = ref(false)
const modalTitle = ref('')
const formRef = ref()
const formState = reactive<CreateUserRequest & { id?: number; status?: UserStatus }>({
username: '',
password: '',
@@ -251,6 +256,24 @@ const formState = reactive<CreateUserRequest & { id?: number; status?: UserStatu
status: UserStatus.ACTIVE
})
const formRules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '用户名长度在 3 到 20 个字符', trigger: 'blur' },
{ pattern: /^[a-zA-Z0-9_]+$/, message: '用户名只能包含字母、数字和下划线', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
],
email: [
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
],
phone: [
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
]
}
const roleDialogVisible = ref(false)
const selectedRoles = ref<number[]>([])
const allRoles = ref<{ key: number; label: string }[]>([])
@@ -342,7 +365,11 @@ const handleDelete = async (row: User) => {
}
const handleModalOk = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
if (formState.id) {
const updateData: UpdateUserRequest = {
nickname: formState.nickname,
@@ -367,8 +394,10 @@ const handleModalOk = async () => {
modalVisible.value = false
fetchData()
} catch (error) {
if (error !== 'cancel') {
handleApiError(error)
}
}
}
const handleModalCancel = () => {