feat(登录): 添加路由守卫和异步导航处理
fix(表单验证): 为用户、角色和菜单管理添加表单验证规则 test(e2e): 增加页面导航超时时间和网络空闲等待 refactor(数据库): 移除Flyway配置并更新数据源配置
This commit is contained in:
@@ -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
|
||||
|
||||
-30
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,8 +272,10 @@ const handleModalOk = async () => {
|
||||
ElMessage.success('操作成功')
|
||||
modalVisible.value = false
|
||||
fetchData()
|
||||
} catch {
|
||||
ElMessage.error('操作失败')
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,7 +381,9 @@ const handleModalOk = async () => {
|
||||
modalVisible.value = false
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
handleApiError(error)
|
||||
if (error !== 'cancel') {
|
||||
handleApiError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,7 +394,9 @@ const handleModalOk = async () => {
|
||||
modalVisible.value = false
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
handleApiError(error)
|
||||
if (error !== 'cancel') {
|
||||
handleApiError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user