feat(react19-migration): 阶段2 - 核心框架层迁移
- T2.1: request.ts 确认无 Vue 依赖,无需修改 - T2.5: errorHandler.ts ElMessage → antd message - T2.7: 新增 API (menu/config/dict/file/notice/loginLog) + 类型定义 (menu/permission/user) - 清理旧 Vue 测试文件、views、stores、router、directives - 修复 tsconfig: 添加 module:ESNext + types:vite/client 验证: npx tsc --noEmit 无类型错误
This commit is contained in:
@@ -1,267 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
import ConfigManagement from '@/views/config/ConfigManagement.vue'
|
||||
|
||||
vi.mock('vue-router')
|
||||
vi.mock('element-plus', () => ({
|
||||
ElMessage: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
ElMessageBox: {
|
||||
confirm: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/request', () => {
|
||||
const mockRequest = {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
}
|
||||
|
||||
mockRequest.get.mockResolvedValue([])
|
||||
mockRequest.post.mockResolvedValue({})
|
||||
mockRequest.put.mockResolvedValue({})
|
||||
mockRequest.delete.mockResolvedValue({})
|
||||
|
||||
return {
|
||||
default: mockRequest,
|
||||
}
|
||||
})
|
||||
|
||||
describe('ConfigManagement Component', () => {
|
||||
let router: any
|
||||
let wrapper: any
|
||||
|
||||
beforeEach(() => {
|
||||
router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: { template: '<div>Home</div>' } },
|
||||
],
|
||||
})
|
||||
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
describe('component initialization', () => {
|
||||
it('should render config management container', () => {
|
||||
wrapper = mount(ConfigManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.config-management').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should initialize with empty data source', () => {
|
||||
wrapper = mount(ConfigManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.dataSource).toBeDefined()
|
||||
expect(Array.isArray(wrapper.vm.dataSource)).toBe(true)
|
||||
})
|
||||
|
||||
it('should initialize with loading state false', () => {
|
||||
wrapper = mount(ConfigManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.loading).toBeDefined()
|
||||
expect(typeof wrapper.vm.loading).toBe('boolean')
|
||||
})
|
||||
|
||||
it('should initialize with modal visible false', () => {
|
||||
wrapper = mount(ConfigManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.modalVisible).toBe(false)
|
||||
})
|
||||
|
||||
it('should initialize with empty form state', () => {
|
||||
wrapper = mount(ConfigManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.formState.configName).toBe('')
|
||||
expect(wrapper.vm.formState.configKey).toBe('')
|
||||
expect(wrapper.vm.formState.configValue).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('add config functionality', () => {
|
||||
it('should have handleAdd method', () => {
|
||||
wrapper = mount(ConfigManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleAdd).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('edit config functionality', () => {
|
||||
it('should have handleEdit method', () => {
|
||||
wrapper = mount(ConfigManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleEdit).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete config functionality', () => {
|
||||
it('should have handleDelete method', () => {
|
||||
wrapper = mount(ConfigManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleDelete).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('form submission', () => {
|
||||
it('should have handleModalOk method', () => {
|
||||
wrapper = mount(ConfigManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleModalOk).toBe('function')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,261 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
import Dashboard from '@/views/system/Dashboard.vue'
|
||||
|
||||
vi.mock('vue-router')
|
||||
vi.mock('@/api/user.api.ts', () => ({
|
||||
getUserStats: vi.fn(),
|
||||
getRecentLogins: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('Dashboard Component', () => {
|
||||
let router: any
|
||||
let wrapper: any
|
||||
|
||||
beforeEach(() => {
|
||||
router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: { template: '<div>Dashboard</div>' } },
|
||||
],
|
||||
})
|
||||
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
describe('component initialization', () => {
|
||||
it('should render dashboard container', () => {
|
||||
wrapper = mount(Dashboard, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-row': true,
|
||||
'el-col': true,
|
||||
'el-card': true,
|
||||
'el-statistic': true,
|
||||
'el-icon': true,
|
||||
'el-timeline': true,
|
||||
'el-timeline-item': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.dashboard').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should initialize with loading state', () => {
|
||||
wrapper = mount(Dashboard, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-row': true,
|
||||
'el-col': true,
|
||||
'el-card': true,
|
||||
'el-statistic': true,
|
||||
'el-icon': true,
|
||||
'el-timeline': true,
|
||||
'el-timeline-item': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.loading).toBe(true)
|
||||
})
|
||||
|
||||
it('should initialize with empty stats', () => {
|
||||
wrapper = mount(Dashboard, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-row': true,
|
||||
'el-col': true,
|
||||
'el-card': true,
|
||||
'el-statistic': true,
|
||||
'el-icon': true,
|
||||
'el-timeline': true,
|
||||
'el-timeline-item': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.stats).toEqual({
|
||||
userCount: 0,
|
||||
roleCount: 0,
|
||||
todayLogin: 0,
|
||||
operationLog: 0,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('statistics cards', () => {
|
||||
it('should render user count card', () => {
|
||||
wrapper = mount(Dashboard, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-row': true,
|
||||
'el-col': true,
|
||||
'el-card': true,
|
||||
'el-statistic': true,
|
||||
'el-icon': true,
|
||||
'el-timeline': true,
|
||||
'el-timeline-item': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.stats.userCount).toBeDefined()
|
||||
})
|
||||
|
||||
it('should render role count card', () => {
|
||||
wrapper = mount(Dashboard, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-row': true,
|
||||
'el-col': true,
|
||||
'el-card': true,
|
||||
'el-statistic': true,
|
||||
'el-icon': true,
|
||||
'el-timeline': true,
|
||||
'el-timeline-item': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.stats.roleCount).toBeDefined()
|
||||
})
|
||||
|
||||
it('should render today login card', () => {
|
||||
wrapper = mount(Dashboard, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-row': true,
|
||||
'el-col': true,
|
||||
'el-card': true,
|
||||
'el-statistic': true,
|
||||
'el-icon': true,
|
||||
'el-timeline': true,
|
||||
'el-timeline-item': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.stats.todayLogin).toBeDefined()
|
||||
})
|
||||
|
||||
it('should render operation log card', () => {
|
||||
wrapper = mount(Dashboard, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-row': true,
|
||||
'el-col': true,
|
||||
'el-card': true,
|
||||
'el-statistic': true,
|
||||
'el-icon': true,
|
||||
'el-timeline': true,
|
||||
'el-timeline-item': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.stats.operationLog).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('recent logins', () => {
|
||||
it('should initialize with empty recent logins', () => {
|
||||
wrapper = mount(Dashboard, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-row': true,
|
||||
'el-col': true,
|
||||
'el-card': true,
|
||||
'el-statistic': true,
|
||||
'el-icon': true,
|
||||
'el-timeline': true,
|
||||
'el-timeline-item': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.recentLogins).toEqual([])
|
||||
})
|
||||
|
||||
it('should display empty state when no recent logins', () => {
|
||||
wrapper = mount(Dashboard, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-row': true,
|
||||
'el-col': true,
|
||||
'el-card': true,
|
||||
'el-statistic': true,
|
||||
'el-icon': true,
|
||||
'el-timeline': true,
|
||||
'el-timeline-item': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.recentLogins.length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('data loading', () => {
|
||||
it('should set loading to false after data loaded', async () => {
|
||||
wrapper = mount(Dashboard, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-row': true,
|
||||
'el-col': true,
|
||||
'el-card': true,
|
||||
'el-statistic': true,
|
||||
'el-icon': true,
|
||||
'el-timeline': true,
|
||||
'el-timeline-item': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.loading).toBe(true)
|
||||
|
||||
wrapper.vm.loading = false
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.vm.loading).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('document title', () => {
|
||||
it('should have dashboard component mounted', () => {
|
||||
wrapper = mount(Dashboard, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-row': true,
|
||||
'el-col': true,
|
||||
'el-card': true,
|
||||
'el-statistic': true,
|
||||
'el-icon': true,
|
||||
'el-timeline': true,
|
||||
'el-timeline-item': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,286 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
import DictManagement from '@/views/config/DictManagement.vue'
|
||||
|
||||
vi.mock('vue-router')
|
||||
vi.mock('element-plus', () => ({
|
||||
ElMessage: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
ElMessageBox: {
|
||||
confirm: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/request', () => {
|
||||
const mockRequest = {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
}
|
||||
|
||||
mockRequest.get.mockResolvedValue([])
|
||||
mockRequest.post.mockResolvedValue({})
|
||||
mockRequest.put.mockResolvedValue({})
|
||||
mockRequest.delete.mockResolvedValue({})
|
||||
|
||||
return {
|
||||
default: mockRequest,
|
||||
}
|
||||
})
|
||||
|
||||
describe('DictManagement Component', () => {
|
||||
let router: any
|
||||
let wrapper: any
|
||||
|
||||
beforeEach(() => {
|
||||
router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: { template: '<div>Home</div>' } },
|
||||
],
|
||||
})
|
||||
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
describe('component initialization', () => {
|
||||
it('should render dict management container', () => {
|
||||
wrapper = mount(DictManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.dict-management').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should initialize with empty data source', () => {
|
||||
wrapper = mount(DictManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.dataSource).toBeDefined()
|
||||
expect(Array.isArray(wrapper.vm.dataSource)).toBe(true)
|
||||
})
|
||||
|
||||
it('should initialize with loading state false', () => {
|
||||
wrapper = mount(DictManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.loading).toBeDefined()
|
||||
expect(typeof wrapper.vm.loading).toBe('boolean')
|
||||
})
|
||||
|
||||
it('should initialize with modal visible false', () => {
|
||||
wrapper = mount(DictManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.modalVisible).toBe(false)
|
||||
})
|
||||
|
||||
it('should initialize with empty form state', () => {
|
||||
wrapper = mount(DictManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.formState.dictName).toBe('')
|
||||
expect(wrapper.vm.formState.dictType).toBe('')
|
||||
expect(wrapper.vm.formState.status).toBe('0')
|
||||
expect(wrapper.vm.formState.remark).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('add dict functionality', () => {
|
||||
it('should have handleAdd method', () => {
|
||||
wrapper = mount(DictManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleAdd).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('edit dict functionality', () => {
|
||||
it('should have handleEdit method', () => {
|
||||
wrapper = mount(DictManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleEdit).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete dict functionality', () => {
|
||||
it('should have handleDelete method', () => {
|
||||
wrapper = mount(DictManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleDelete).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('form submission', () => {
|
||||
it('should have handleModalOk method', () => {
|
||||
wrapper = mount(DictManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleModalOk).toBe('function')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,257 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
import ExceptionLog from '@/views/audit/ExceptionLog.vue'
|
||||
|
||||
vi.mock('vue-router')
|
||||
vi.mock('@/api/exceptionLog', () => ({
|
||||
exceptionLogApi: {
|
||||
getPage: vi.fn().mockResolvedValue({
|
||||
content: [
|
||||
{ id: 1, username: 'admin', operation: '用户登录', method: 'POST /api/auth/login', errorMsg: 'NullPointerException', ip: '192.168.1.1', createTime: '2026-01-01T10:00:00' },
|
||||
{ id: 2, username: 'user', operation: '文件上传', method: 'POST /api/files/upload', errorMsg: 'FileSizeLimitExceededException', ip: '192.168.1.2', createTime: '2026-01-02T11:00:00' },
|
||||
],
|
||||
totalElements: 2,
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('ExceptionLog Component', () => {
|
||||
let router: any
|
||||
let wrapper: any
|
||||
|
||||
beforeEach(() => {
|
||||
router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: { template: '<div>Home</div>' } },
|
||||
],
|
||||
})
|
||||
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
describe('component initialization', () => {
|
||||
it('should render exception log container', () => {
|
||||
wrapper = mount(ExceptionLog, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-input': true,
|
||||
'el-tag': true,
|
||||
'el-icon': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-descriptions': true,
|
||||
'el-descriptions-item': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.exception-log').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should initialize with empty search keyword', () => {
|
||||
wrapper = mount(ExceptionLog, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-input': true,
|
||||
'el-tag': true,
|
||||
'el-icon': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-descriptions': true,
|
||||
'el-descriptions-item': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.searchKeyword).toBe('')
|
||||
})
|
||||
|
||||
it('should initialize with correct pagination defaults', () => {
|
||||
wrapper = mount(ExceptionLog, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-input': true,
|
||||
'el-tag': true,
|
||||
'el-icon': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-descriptions': true,
|
||||
'el-descriptions-item': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.pagination.current).toBe(1)
|
||||
expect(wrapper.vm.pagination.pageSize).toBe(10)
|
||||
expect(wrapper.vm.pagination.total).toBe(0)
|
||||
})
|
||||
|
||||
it('should initialize with hidden detail dialog', () => {
|
||||
wrapper = mount(ExceptionLog, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-input': true,
|
||||
'el-tag': true,
|
||||
'el-icon': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-descriptions': true,
|
||||
'el-descriptions-item': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.detailVisible).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('detail view handling', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mount(ExceptionLog, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-input': true,
|
||||
'el-tag': true,
|
||||
'el-icon': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-descriptions': true,
|
||||
'el-descriptions-item': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should show detail dialog when viewing exception', () => {
|
||||
const exception = {
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
operation: '用户登录',
|
||||
method: 'POST /api/auth/login',
|
||||
errorMsg: 'NullPointerException',
|
||||
ip: '192.168.1.1',
|
||||
createTime: '2026-01-01T10:00:00',
|
||||
}
|
||||
|
||||
wrapper.vm.handleViewDetail(exception)
|
||||
|
||||
expect(wrapper.vm.detailVisible).toBe(true)
|
||||
expect(wrapper.vm.currentDetail).toEqual(exception)
|
||||
})
|
||||
|
||||
it('should create a copy of exception data for detail view', () => {
|
||||
const exception = {
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
}
|
||||
|
||||
wrapper.vm.handleViewDetail(exception)
|
||||
wrapper.vm.currentDetail.username = 'modified'
|
||||
|
||||
expect(exception.username).toBe('admin')
|
||||
})
|
||||
})
|
||||
|
||||
describe('sort handling', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mount(ExceptionLog, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-input': true,
|
||||
'el-tag': true,
|
||||
'el-icon': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-descriptions': true,
|
||||
'el-descriptions-item': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should update sort info on ascending order', () => {
|
||||
wrapper.vm.handleSortChange({ prop: 'username', order: 'ascending' })
|
||||
expect(wrapper.vm.sortInfo.sort).toBe('username')
|
||||
expect(wrapper.vm.sortInfo.order).toBe('asc')
|
||||
})
|
||||
|
||||
it('should update sort info on descending order', () => {
|
||||
wrapper.vm.handleSortChange({ prop: 'createTime', order: 'descending' })
|
||||
expect(wrapper.vm.sortInfo.sort).toBe('createTime')
|
||||
expect(wrapper.vm.sortInfo.order).toBe('desc')
|
||||
})
|
||||
})
|
||||
|
||||
describe('pagination handling', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mount(ExceptionLog, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-input': true,
|
||||
'el-tag': true,
|
||||
'el-icon': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-descriptions': true,
|
||||
'el-descriptions-item': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should reset to first page on size change', () => {
|
||||
wrapper.vm.pagination.current = 5
|
||||
wrapper.vm.handleSizeChange()
|
||||
expect(wrapper.vm.pagination.current).toBe(1)
|
||||
})
|
||||
|
||||
it('should reset to first page on search', () => {
|
||||
wrapper.vm.pagination.current = 5
|
||||
wrapper.vm.handleSearch()
|
||||
expect(wrapper.vm.pagination.current).toBe(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,247 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
import FileManagement from '@/views/file/FileManagement.vue'
|
||||
|
||||
vi.mock('vue-router')
|
||||
vi.mock('element-plus', () => ({
|
||||
ElMessage: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
ElMessageBox: {
|
||||
confirm: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/request', () => {
|
||||
const mockRequest = {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
}
|
||||
|
||||
mockRequest.get.mockResolvedValue([
|
||||
{ id: 1, fileName: 'test.pdf', fileSize: 1024, fileType: 'application/pdf', storageType: 'local', createdAt: '2026-01-01', createBy: 'admin' },
|
||||
{ id: 2, fileName: 'image.png', fileSize: 2048, fileType: 'image/png', storageType: 'local', createdAt: '2026-01-02', createBy: 'user' },
|
||||
])
|
||||
mockRequest.post.mockResolvedValue({})
|
||||
mockRequest.put.mockResolvedValue({})
|
||||
mockRequest.delete.mockResolvedValue({})
|
||||
|
||||
return {
|
||||
default: mockRequest,
|
||||
}
|
||||
})
|
||||
|
||||
describe('FileManagement Component', () => {
|
||||
let router: any
|
||||
let wrapper: any
|
||||
|
||||
beforeEach(() => {
|
||||
router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: { template: '<div>Home</div>' } },
|
||||
],
|
||||
})
|
||||
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
describe('component initialization', () => {
|
||||
it('should render file management container', () => {
|
||||
wrapper = mount(FileManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-input': true,
|
||||
'el-upload': true,
|
||||
'el-tag': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.file-management').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should initialize with empty search keyword', () => {
|
||||
wrapper = mount(FileManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-input': true,
|
||||
'el-upload': true,
|
||||
'el-tag': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.searchKeyword).toBe('')
|
||||
})
|
||||
|
||||
it('should initialize with loading state false before data fetch', async () => {
|
||||
wrapper = mount(FileManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-input': true,
|
||||
'el-upload': true,
|
||||
'el-tag': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
expect([true, false]).toContain(wrapper.vm.loading)
|
||||
})
|
||||
})
|
||||
|
||||
describe('file type utilities', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mount(FileManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-input': true,
|
||||
'el-upload': true,
|
||||
'el-tag': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should return correct file type name for images', () => {
|
||||
expect(wrapper.vm.getFileTypeName('image/png')).toBe('图片')
|
||||
expect(wrapper.vm.getFileTypeName('image/jpeg')).toBe('图片')
|
||||
})
|
||||
|
||||
it('should return correct file type name for videos', () => {
|
||||
expect(wrapper.vm.getFileTypeName('video/mp4')).toBe('视频')
|
||||
})
|
||||
|
||||
it('should return correct file type name for audio', () => {
|
||||
expect(wrapper.vm.getFileTypeName('audio/mp3')).toBe('音频')
|
||||
})
|
||||
|
||||
it('should return correct file type name for PDF', () => {
|
||||
expect(wrapper.vm.getFileTypeName('application/pdf')).toBe('PDF')
|
||||
})
|
||||
|
||||
it('should return correct file type name for Word', () => {
|
||||
expect(wrapper.vm.getFileTypeName('application/vnd.openxmlformats-officedocument.wordprocessingml.document')).toBe('Word')
|
||||
})
|
||||
|
||||
it('should return correct file type name for Excel', () => {
|
||||
expect(wrapper.vm.getFileTypeName('application/vnd.ms-excel')).toBe('Excel')
|
||||
})
|
||||
|
||||
it('should return unknown for unknown file types', () => {
|
||||
expect(wrapper.vm.getFileTypeName('')).toBe('未知')
|
||||
expect(wrapper.vm.getFileTypeName('unknown/type')).toBe('其他')
|
||||
})
|
||||
|
||||
it('should return correct tag type for images', () => {
|
||||
expect(wrapper.vm.getFileTypeTag('image/png')).toBe('success')
|
||||
})
|
||||
|
||||
it('should return correct tag type for videos', () => {
|
||||
expect(wrapper.vm.getFileTypeTag('video/mp4')).toBe('danger')
|
||||
})
|
||||
|
||||
it('should return correct tag type for audio', () => {
|
||||
expect(wrapper.vm.getFileTypeTag('audio/mp3')).toBe('warning')
|
||||
})
|
||||
|
||||
it('should return correct tag type for PDF', () => {
|
||||
expect(wrapper.vm.getFileTypeTag('application/pdf')).toBe('danger')
|
||||
})
|
||||
|
||||
it('should return correct tag type for unknown', () => {
|
||||
expect(wrapper.vm.getFileTypeTag('')).toBe('info')
|
||||
})
|
||||
})
|
||||
|
||||
describe('search functionality', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mount(FileManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-input': true,
|
||||
'el-upload': true,
|
||||
'el-tag': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should filter files by search keyword', async () => {
|
||||
wrapper.vm.dataSource = [
|
||||
{ id: 1, fileName: 'test.pdf' },
|
||||
{ id: 2, fileName: 'image.png' },
|
||||
{ id: 3, fileName: 'document.doc' },
|
||||
]
|
||||
|
||||
wrapper.vm.searchKeyword = 'test'
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.vm.filteredDataSource.length).toBe(1)
|
||||
expect(wrapper.vm.filteredDataSource[0].fileName).toBe('test.pdf')
|
||||
})
|
||||
|
||||
it('should return all files when search keyword is empty', () => {
|
||||
wrapper.vm.dataSource = [
|
||||
{ id: 1, fileName: 'test.pdf' },
|
||||
{ id: 2, fileName: 'image.png' },
|
||||
]
|
||||
|
||||
wrapper.vm.searchKeyword = ''
|
||||
|
||||
expect(wrapper.vm.filteredDataSource.length).toBe(2)
|
||||
})
|
||||
|
||||
it('should be case insensitive when searching', () => {
|
||||
wrapper.vm.dataSource = [
|
||||
{ id: 1, fileName: 'TEST.pdf' },
|
||||
{ id: 2, fileName: 'image.png' },
|
||||
]
|
||||
|
||||
wrapper.vm.searchKeyword = 'test'
|
||||
|
||||
expect(wrapper.vm.filteredDataSource.length).toBe(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,186 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import Login from '@/views/system/Login.vue'
|
||||
|
||||
vi.mock('vue-router')
|
||||
vi.mock('element-plus', () => ({
|
||||
ElMessage: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/request', () => ({
|
||||
default: {
|
||||
post: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('Login Component', () => {
|
||||
let router: any
|
||||
let wrapper: any
|
||||
let pinia: any
|
||||
|
||||
beforeEach(() => {
|
||||
pinia = createPinia()
|
||||
setActivePinia(pinia)
|
||||
|
||||
router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: { template: '<div>Dashboard</div>' } },
|
||||
{ path: '/login', component: { template: '<div>Login</div>' } },
|
||||
],
|
||||
})
|
||||
|
||||
vi.clearAllMocks()
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
describe('component rendering', () => {
|
||||
it('should render login form', () => {
|
||||
wrapper = mount(Login, {
|
||||
global: {
|
||||
plugins: [router, pinia],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.login-container').exists()).toBe(true)
|
||||
expect(wrapper.find('.login-card').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should initialize with empty form state', () => {
|
||||
wrapper = mount(Login, {
|
||||
global: {
|
||||
plugins: [router, pinia],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.formState.username).toBe('')
|
||||
expect(wrapper.vm.formState.password).toBe('')
|
||||
})
|
||||
|
||||
it('should initialize loading as false', () => {
|
||||
wrapper = mount(Login, {
|
||||
global: {
|
||||
plugins: [router, pinia],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.loading).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('form state management', () => {
|
||||
it('should update username when input changes', async () => {
|
||||
wrapper = mount(Login, {
|
||||
global: {
|
||||
plugins: [router, pinia],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
wrapper.vm.formState.username = 'testuser'
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.vm.formState.username).toBe('testuser')
|
||||
})
|
||||
|
||||
it('should update password when input changes', async () => {
|
||||
wrapper = mount(Login, {
|
||||
global: {
|
||||
plugins: [router, pinia],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
wrapper.vm.formState.password = 'password123'
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.vm.formState.password).toBe('password123')
|
||||
})
|
||||
})
|
||||
|
||||
describe('form submission', () => {
|
||||
it('should have onFinish method', () => {
|
||||
wrapper = mount(Login, {
|
||||
global: {
|
||||
plugins: [router, pinia],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.onFinish).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('document title', () => {
|
||||
it('should set document title on mount', () => {
|
||||
const originalTitle = document.title
|
||||
|
||||
wrapper = mount(Login, {
|
||||
global: {
|
||||
plugins: [router, pinia],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(document.title).toBe('登录 - Novalon 管理系统')
|
||||
|
||||
document.title = originalTitle
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,195 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
import LoginLog from '@/views/audit/LoginLog.vue'
|
||||
|
||||
vi.mock('vue-router')
|
||||
vi.mock('@/utils/request', () => {
|
||||
const mockRequest = {
|
||||
get: vi.fn().mockResolvedValue({
|
||||
content: [
|
||||
{ id: 1, username: 'admin', ip: '192.168.1.1', location: '北京', browser: 'Chrome', os: 'Windows', status: '0', loginTime: '2026-01-01T10:00:00' },
|
||||
{ id: 2, username: 'user', ip: '192.168.1.2', location: '上海', browser: 'Firefox', os: 'MacOS', status: '1', loginTime: '2026-01-02T11:00:00' },
|
||||
],
|
||||
totalElements: 2,
|
||||
}),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
}
|
||||
|
||||
return {
|
||||
default: mockRequest,
|
||||
}
|
||||
})
|
||||
|
||||
describe('LoginLog Component', () => {
|
||||
let router: any
|
||||
let wrapper: any
|
||||
|
||||
beforeEach(() => {
|
||||
router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: { template: '<div>Home</div>' } },
|
||||
],
|
||||
})
|
||||
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
describe('component initialization', () => {
|
||||
it('should render login log container', () => {
|
||||
wrapper = mount(LoginLog, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-input': true,
|
||||
'el-tag': true,
|
||||
'el-icon': true,
|
||||
'el-pagination': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.login-log').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should initialize with empty search keyword', () => {
|
||||
wrapper = mount(LoginLog, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-input': true,
|
||||
'el-tag': true,
|
||||
'el-icon': true,
|
||||
'el-pagination': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.searchKeyword).toBe('')
|
||||
})
|
||||
|
||||
it('should initialize with correct pagination defaults', () => {
|
||||
wrapper = mount(LoginLog, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-input': true,
|
||||
'el-tag': true,
|
||||
'el-icon': true,
|
||||
'el-pagination': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.pagination.current).toBe(1)
|
||||
expect(wrapper.vm.pagination.pageSize).toBe(10)
|
||||
expect(wrapper.vm.pagination.total).toBe(0)
|
||||
})
|
||||
|
||||
it('should initialize with correct sort defaults', () => {
|
||||
wrapper = mount(LoginLog, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-input': true,
|
||||
'el-tag': true,
|
||||
'el-icon': true,
|
||||
'el-pagination': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.sortInfo.sort).toBe('id')
|
||||
expect(wrapper.vm.sortInfo.order).toBe('asc')
|
||||
})
|
||||
})
|
||||
|
||||
describe('sort handling', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mount(LoginLog, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-input': true,
|
||||
'el-tag': true,
|
||||
'el-icon': true,
|
||||
'el-pagination': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should update sort info on ascending order', () => {
|
||||
wrapper.vm.handleSortChange({ prop: 'username', order: 'ascending' })
|
||||
expect(wrapper.vm.sortInfo.sort).toBe('username')
|
||||
expect(wrapper.vm.sortInfo.order).toBe('asc')
|
||||
})
|
||||
|
||||
it('should update sort info on descending order', () => {
|
||||
wrapper.vm.handleSortChange({ prop: 'loginTime', order: 'descending' })
|
||||
expect(wrapper.vm.sortInfo.sort).toBe('loginTime')
|
||||
expect(wrapper.vm.sortInfo.order).toBe('desc')
|
||||
})
|
||||
})
|
||||
|
||||
describe('pagination handling', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mount(LoginLog, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-input': true,
|
||||
'el-tag': true,
|
||||
'el-icon': true,
|
||||
'el-pagination': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should reset to first page on size change', () => {
|
||||
wrapper.vm.pagination.current = 5
|
||||
wrapper.vm.handleSizeChange()
|
||||
expect(wrapper.vm.pagination.current).toBe(1)
|
||||
})
|
||||
|
||||
it('should reset to first page on search', () => {
|
||||
wrapper.vm.pagination.current = 5
|
||||
wrapper.vm.handleSearch()
|
||||
expect(wrapper.vm.pagination.current).toBe(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,72 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import MenuItem from '@/components/MenuItem.vue'
|
||||
|
||||
describe('MenuItem 组件', () => {
|
||||
it('应该正确接收菜单项 props', () => {
|
||||
const menu = {
|
||||
id: '1',
|
||||
name: '仪表盘',
|
||||
path: '/dashboard',
|
||||
icon: 'Odometer',
|
||||
sort: 1
|
||||
}
|
||||
|
||||
const wrapper = mount(MenuItem, {
|
||||
props: { menu },
|
||||
global: {
|
||||
stubs: {
|
||||
'el-menu-item': {
|
||||
template: '<div><slot /></div>'
|
||||
},
|
||||
'el-sub-menu': {
|
||||
template: '<div><slot name="title" /><slot /></div>'
|
||||
},
|
||||
'el-icon': {
|
||||
template: '<div><slot /></div>'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.props('menu')).toEqual(menu)
|
||||
})
|
||||
|
||||
it('应该正确处理有子菜单的菜单项', () => {
|
||||
const menu = {
|
||||
id: '2',
|
||||
name: '系统管理',
|
||||
path: '/system',
|
||||
icon: 'Setting',
|
||||
sort: 2,
|
||||
children: [
|
||||
{
|
||||
id: '3',
|
||||
name: '用户管理',
|
||||
path: '/users',
|
||||
sort: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const wrapper = mount(MenuItem, {
|
||||
props: { menu },
|
||||
global: {
|
||||
stubs: {
|
||||
'el-menu-item': {
|
||||
template: '<div><slot /></div>'
|
||||
},
|
||||
'el-sub-menu': {
|
||||
template: '<div><slot name="title" /><slot /></div>'
|
||||
},
|
||||
'el-icon': {
|
||||
template: '<div><slot /></div>'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.props('menu')).toEqual(menu)
|
||||
expect(wrapper.props('menu').children).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
@@ -1,279 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
import MenuManagement from '@/views/system/MenuManagement.vue'
|
||||
|
||||
vi.mock('vue-router')
|
||||
vi.mock('element-plus', () => ({
|
||||
ElMessage: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
ElMessageBox: {
|
||||
confirm: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/api/menu.api', () => ({
|
||||
menuApi: {
|
||||
getAll: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/request', () => {
|
||||
const mockRequest = {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
}
|
||||
|
||||
mockRequest.get.mockResolvedValue([])
|
||||
mockRequest.post.mockResolvedValue({})
|
||||
mockRequest.put.mockResolvedValue({})
|
||||
mockRequest.delete.mockResolvedValue({})
|
||||
|
||||
return {
|
||||
default: mockRequest,
|
||||
}
|
||||
})
|
||||
|
||||
describe('MenuManagement Component', () => {
|
||||
let router: any
|
||||
let wrapper: any
|
||||
|
||||
beforeEach(() => {
|
||||
router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: { template: '<div>Home</div>' } },
|
||||
],
|
||||
})
|
||||
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
describe('component initialization', () => {
|
||||
it('should render menu management container', () => {
|
||||
wrapper = mount(MenuManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-input-number': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.menu-management').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should initialize with empty data source', () => {
|
||||
wrapper = mount(MenuManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-input-number': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.dataSource).toBeDefined()
|
||||
expect(Array.isArray(wrapper.vm.dataSource)).toBe(true)
|
||||
})
|
||||
|
||||
it('should initialize with loading state false', () => {
|
||||
wrapper = mount(MenuManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-input-number': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.loading).toBeDefined()
|
||||
expect(typeof wrapper.vm.loading).toBe('boolean')
|
||||
})
|
||||
|
||||
it('should initialize with modal visible false', () => {
|
||||
wrapper = mount(MenuManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-input-number': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.modalVisible).toBe(false)
|
||||
})
|
||||
|
||||
it('should initialize with empty form state', () => {
|
||||
wrapper = mount(MenuManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-input-number': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.formState.menuName).toBe('')
|
||||
expect(wrapper.vm.formState.menuType).toBe('C')
|
||||
expect(wrapper.vm.formState.perms).toBe('')
|
||||
expect(wrapper.vm.formState.component).toBe('')
|
||||
expect(wrapper.vm.formState.orderNum).toBe(0)
|
||||
expect(wrapper.vm.formState.status).toBe('0')
|
||||
})
|
||||
})
|
||||
|
||||
describe('add menu functionality', () => {
|
||||
it('should have handleAdd method', () => {
|
||||
wrapper = mount(MenuManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-input-number': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleAdd).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('edit menu functionality', () => {
|
||||
it('should have handleEdit method', () => {
|
||||
wrapper = mount(MenuManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-input-number': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleEdit).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete menu functionality', () => {
|
||||
it('should have handleDelete method', () => {
|
||||
wrapper = mount(MenuManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-input-number': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleDelete).toBe('function')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,231 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
import NoticeManagement from '@/views/notify/NoticeManagement.vue'
|
||||
|
||||
vi.mock('vue-router')
|
||||
vi.mock('element-plus', () => ({
|
||||
ElMessage: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
ElMessageBox: {
|
||||
confirm: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/request', () => {
|
||||
const mockRequest = {
|
||||
get: vi.fn().mockResolvedValue([
|
||||
{ id: 1, noticeTitle: '系统维护通知', noticeType: '1', noticeContent: '系统将于今晚维护', status: '0', createdAt: '2026-01-01T10:00:00' },
|
||||
{ id: 2, noticeTitle: '新功能上线', noticeType: '2', noticeContent: '新功能已上线', status: '0', createdAt: '2026-01-02T11:00:00' },
|
||||
]),
|
||||
post: vi.fn().mockResolvedValue({}),
|
||||
put: vi.fn().mockResolvedValue({}),
|
||||
delete: vi.fn().mockResolvedValue({}),
|
||||
}
|
||||
|
||||
return {
|
||||
default: mockRequest,
|
||||
}
|
||||
})
|
||||
|
||||
describe('NoticeManagement Component', () => {
|
||||
let router: any
|
||||
let wrapper: any
|
||||
|
||||
beforeEach(() => {
|
||||
router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: { template: '<div>Home</div>' } },
|
||||
],
|
||||
})
|
||||
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
describe('component initialization', () => {
|
||||
it('should render notice management container', () => {
|
||||
wrapper = mount(NoticeManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-input': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.notice-management').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should initialize with hidden modal', () => {
|
||||
wrapper = mount(NoticeManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-input': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.modalVisible).toBe(false)
|
||||
})
|
||||
|
||||
it('should initialize with empty form state', () => {
|
||||
wrapper = mount(NoticeManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-input': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.formState.noticeTitle).toBe('')
|
||||
expect(wrapper.vm.formState.noticeType).toBe('1')
|
||||
expect(wrapper.vm.formState.status).toBe('0')
|
||||
})
|
||||
})
|
||||
|
||||
describe('add notice', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mount(NoticeManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-input': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should show modal with add title', () => {
|
||||
wrapper.vm.handleAdd()
|
||||
expect(wrapper.vm.modalTitle).toBe('新增公告')
|
||||
expect(wrapper.vm.modalVisible).toBe(true)
|
||||
})
|
||||
|
||||
it('should reset form state when adding', () => {
|
||||
wrapper.vm.formState.noticeTitle = 'existing title'
|
||||
wrapper.vm.handleAdd()
|
||||
expect(wrapper.vm.formState.noticeTitle).toBe('')
|
||||
expect(wrapper.vm.formState.id).toBe(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('edit notice', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mount(NoticeManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-input': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should show modal with edit title', () => {
|
||||
const notice = { id: 1, noticeTitle: 'Test', noticeType: '1', noticeContent: 'Content', status: '0' }
|
||||
wrapper.vm.handleEdit(notice)
|
||||
expect(wrapper.vm.modalTitle).toBe('编辑公告')
|
||||
expect(wrapper.vm.modalVisible).toBe(true)
|
||||
})
|
||||
|
||||
it('should populate form with notice data', () => {
|
||||
const notice = { id: 1, noticeTitle: 'Test Notice', noticeType: '2', noticeContent: 'Test Content', status: '1' }
|
||||
wrapper.vm.handleEdit(notice)
|
||||
expect(wrapper.vm.formState.id).toBe(1)
|
||||
expect(wrapper.vm.formState.noticeTitle).toBe('Test Notice')
|
||||
expect(wrapper.vm.formState.noticeType).toBe('2')
|
||||
})
|
||||
})
|
||||
|
||||
describe('form state', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mount(NoticeManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-input': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should have default notice type as notification', () => {
|
||||
expect(wrapper.vm.formState.noticeType).toBe('1')
|
||||
})
|
||||
|
||||
it('should have default status as normal', () => {
|
||||
expect(wrapper.vm.formState.status).toBe('0')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,216 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
import OperationLog from '@/views/audit/OperationLog.vue'
|
||||
|
||||
vi.mock('vue-router')
|
||||
vi.mock('@/api/operationLog', () => ({
|
||||
operationLogApi: {
|
||||
getPage: vi.fn().mockResolvedValue({
|
||||
content: [
|
||||
{ id: 1, username: 'admin', operation: '用户登录', method: 'POST', params: '{}', status: '0', duration: 100, createdAt: '2026-01-01T10:00:00' },
|
||||
{ id: 2, username: 'user', operation: '查看用户', method: 'GET', params: '{"id":1}', status: '0', duration: 50, createdAt: '2026-01-02T11:00:00' },
|
||||
],
|
||||
totalElements: 2,
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('OperationLog Component', () => {
|
||||
let router: any
|
||||
let wrapper: any
|
||||
|
||||
beforeEach(() => {
|
||||
router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: { template: '<div>Home</div>' } },
|
||||
],
|
||||
})
|
||||
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
describe('component initialization', () => {
|
||||
it('should render operation log container', () => {
|
||||
wrapper = mount(OperationLog, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-input': true,
|
||||
'el-tag': true,
|
||||
'el-icon': true,
|
||||
'el-popover': true,
|
||||
'el-pagination': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.operation-log').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should initialize with empty search keyword', () => {
|
||||
wrapper = mount(OperationLog, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-input': true,
|
||||
'el-tag': true,
|
||||
'el-icon': true,
|
||||
'el-popover': true,
|
||||
'el-pagination': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.searchKeyword).toBe('')
|
||||
})
|
||||
|
||||
it('should initialize with correct pagination defaults', () => {
|
||||
wrapper = mount(OperationLog, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-input': true,
|
||||
'el-tag': true,
|
||||
'el-icon': true,
|
||||
'el-popover': true,
|
||||
'el-pagination': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.pagination.current).toBe(1)
|
||||
expect(wrapper.vm.pagination.pageSize).toBe(10)
|
||||
expect(wrapper.vm.pagination.total).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('operation icon mapping', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mount(OperationLog, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-input': true,
|
||||
'el-tag': true,
|
||||
'el-icon': true,
|
||||
'el-popover': true,
|
||||
'el-pagination': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should return User icon for login operations', () => {
|
||||
const icon = wrapper.vm.getOperationIcon('用户登录')
|
||||
expect(icon.name).toBe('User')
|
||||
})
|
||||
|
||||
it('should return Delete icon for delete operations', () => {
|
||||
const icon = wrapper.vm.getOperationIcon('删除用户')
|
||||
expect(icon.name).toBe('Delete')
|
||||
})
|
||||
|
||||
it('should return Edit icon for update operations', () => {
|
||||
const icon = wrapper.vm.getOperationIcon('编辑用户')
|
||||
expect(icon.name).toBe('Edit')
|
||||
})
|
||||
|
||||
it('should return View icon for view operations', () => {
|
||||
const icon = wrapper.vm.getOperationIcon('查看用户')
|
||||
expect(icon.name).toBe('View')
|
||||
})
|
||||
|
||||
it('should return Plus icon for create operations', () => {
|
||||
const icon = wrapper.vm.getOperationIcon('新增用户')
|
||||
expect(icon.name).toBe('Plus')
|
||||
})
|
||||
|
||||
it('should return Download icon for download operations', () => {
|
||||
const icon = wrapper.vm.getOperationIcon('下载文件')
|
||||
expect(icon.name).toBe('Download')
|
||||
})
|
||||
|
||||
it('should return Setting icon for config operations', () => {
|
||||
const icon = wrapper.vm.getOperationIcon('系统设置')
|
||||
expect(icon.name).toBe('Setting')
|
||||
})
|
||||
|
||||
it('should return Lock icon for password operations', () => {
|
||||
const icon = wrapper.vm.getOperationIcon('重置密码')
|
||||
expect(icon.name).toBe('Lock')
|
||||
})
|
||||
|
||||
it('should return Document icon for unknown operations', () => {
|
||||
const icon = wrapper.vm.getOperationIcon('未知操作')
|
||||
expect(icon.name).toBe('Document')
|
||||
})
|
||||
})
|
||||
|
||||
describe('params formatting', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mount(OperationLog, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-input': true,
|
||||
'el-tag': true,
|
||||
'el-icon': true,
|
||||
'el-popover': true,
|
||||
'el-pagination': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should format valid JSON params', () => {
|
||||
const params = '{"name":"test","id":1}'
|
||||
const formatted = wrapper.vm.formatParams(params)
|
||||
expect(formatted).toContain('name')
|
||||
expect(formatted).toContain('test')
|
||||
})
|
||||
|
||||
it('should return empty string for null params', () => {
|
||||
const formatted = wrapper.vm.formatParams(null)
|
||||
expect(formatted).toBe('')
|
||||
})
|
||||
|
||||
it('should return empty string for undefined params', () => {
|
||||
const formatted = wrapper.vm.formatParams(undefined)
|
||||
expect(formatted).toBe('')
|
||||
})
|
||||
|
||||
it('should return original string for invalid JSON', () => {
|
||||
const params = 'not a json'
|
||||
const formatted = wrapper.vm.formatParams(params)
|
||||
expect(formatted).toBe('not a json')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,383 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
import RoleManagement from '@/views/system/RoleManagement.vue'
|
||||
|
||||
vi.mock('vue-router')
|
||||
vi.mock('element-plus', () => ({
|
||||
ElMessage: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
ElMessageBox: {
|
||||
confirm: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/api/role.api', () => ({
|
||||
roleApi: {
|
||||
getPage: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
getAll: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/api/permission.api', () => ({
|
||||
permissionApi: {
|
||||
getAll: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('RoleManagement Component', () => {
|
||||
let router: any
|
||||
let wrapper: any
|
||||
|
||||
beforeEach(() => {
|
||||
router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: { template: '<div>Home</div>' } },
|
||||
],
|
||||
})
|
||||
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
describe('component initialization', () => {
|
||||
it('should render role management container', () => {
|
||||
wrapper = mount(RoleManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-tree': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.role-management').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should initialize with empty search keyword', () => {
|
||||
wrapper = mount(RoleManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-tree': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.searchKeyword).toBe('')
|
||||
})
|
||||
|
||||
it('should initialize with empty data source', () => {
|
||||
wrapper = mount(RoleManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-tree': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.dataSource).toEqual([])
|
||||
})
|
||||
|
||||
it('should initialize with pagination on page 1', () => {
|
||||
wrapper = mount(RoleManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-tree': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.pagination.current).toBe(1)
|
||||
})
|
||||
|
||||
it('should initialize with modal visible false', () => {
|
||||
wrapper = mount(RoleManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-tree': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.modalVisible).toBe(false)
|
||||
})
|
||||
|
||||
it('should initialize with empty form state', () => {
|
||||
wrapper = mount(RoleManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-tree': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.formState.roleName).toBe('')
|
||||
expect(wrapper.vm.formState.roleKey).toBe('')
|
||||
expect(wrapper.vm.formState.roleSort).toBe(1)
|
||||
expect(wrapper.vm.formState.status).toBe(1)
|
||||
expect(wrapper.vm.formState.permissions).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('search functionality', () => {
|
||||
it('should have handleSearch method', () => {
|
||||
wrapper = mount(RoleManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-tree': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleSearch).toBe('function')
|
||||
})
|
||||
|
||||
it('should update search keyword when input changes', async () => {
|
||||
wrapper = mount(RoleManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-tree': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
wrapper.vm.searchKeyword = 'admin'
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.vm.searchKeyword).toBe('admin')
|
||||
})
|
||||
})
|
||||
|
||||
describe('add role functionality', () => {
|
||||
it('should have handleAdd method', () => {
|
||||
wrapper = mount(RoleManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-tree': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleAdd).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('pagination functionality', () => {
|
||||
it('should have handleTableChange method', () => {
|
||||
wrapper = mount(RoleManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-tree': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleTableChange).toBe('function')
|
||||
})
|
||||
|
||||
it('should have handleSizeChange method', () => {
|
||||
wrapper = mount(RoleManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-tree': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleSizeChange).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('sort functionality', () => {
|
||||
it('should have handleSortChange method', () => {
|
||||
wrapper = mount(RoleManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-tree': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleSortChange).toBe('function')
|
||||
})
|
||||
|
||||
it('should initialize with default sort info', () => {
|
||||
wrapper = mount(RoleManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-tree': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.sortInfo.sortBy).toBe('id')
|
||||
expect(wrapper.vm.sortInfo.sortOrder).toBe('asc')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,423 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
import UserManagement from '@/views/system/UserManagement.vue'
|
||||
|
||||
vi.mock('vue-router')
|
||||
vi.mock('element-plus', () => ({
|
||||
ElMessage: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
ElMessageBox: {
|
||||
confirm: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/api/user.api', () => ({
|
||||
userApi: {
|
||||
getPage: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
assignRoles: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/api/role.api', () => ({
|
||||
roleApi: {
|
||||
getAll: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('UserManagement Component', () => {
|
||||
let router: any
|
||||
let wrapper: any
|
||||
|
||||
beforeEach(() => {
|
||||
router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: { template: '<div>Home</div>' } },
|
||||
],
|
||||
})
|
||||
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
describe('component initialization', () => {
|
||||
it('should render user management container', () => {
|
||||
wrapper = mount(UserManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.user-management').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should initialize with empty search keyword', () => {
|
||||
wrapper = mount(UserManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.searchKeyword).toBe('')
|
||||
})
|
||||
|
||||
it('should initialize with loading state false before data fetch', () => {
|
||||
wrapper = mount(UserManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.loading).toBeDefined()
|
||||
expect(typeof wrapper.vm.loading).toBe('boolean')
|
||||
})
|
||||
|
||||
it('should initialize with empty data source', () => {
|
||||
wrapper = mount(UserManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.dataSource).toEqual([])
|
||||
})
|
||||
|
||||
it('should initialize with pagination on page 1', () => {
|
||||
wrapper = mount(UserManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.pagination.current).toBe(1)
|
||||
})
|
||||
|
||||
it('should initialize with modal visible false', () => {
|
||||
wrapper = mount(UserManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.modalVisible).toBe(false)
|
||||
})
|
||||
|
||||
it('should initialize with empty form state', () => {
|
||||
wrapper = mount(UserManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.formState.username).toBe('')
|
||||
expect(wrapper.vm.formState.password).toBe('')
|
||||
expect(wrapper.vm.formState.nickname).toBe('')
|
||||
expect(wrapper.vm.formState.email).toBe('')
|
||||
expect(wrapper.vm.formState.phone).toBe('')
|
||||
expect(wrapper.vm.formState.roles).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('search functionality', () => {
|
||||
it('should have handleSearch method', () => {
|
||||
wrapper = mount(UserManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleSearch).toBe('function')
|
||||
})
|
||||
|
||||
it('should update search keyword when input changes', async () => {
|
||||
wrapper = mount(UserManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
wrapper.vm.searchKeyword = 'testuser'
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.vm.searchKeyword).toBe('testuser')
|
||||
})
|
||||
})
|
||||
|
||||
describe('add user functionality', () => {
|
||||
it('should have handleAdd method', () => {
|
||||
wrapper = mount(UserManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleAdd).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('pagination functionality', () => {
|
||||
it('should have handleTableChange method', () => {
|
||||
wrapper = mount(UserManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleTableChange).toBe('function')
|
||||
})
|
||||
|
||||
it('should have handleSizeChange method', () => {
|
||||
wrapper = mount(UserManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleSizeChange).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('sort functionality', () => {
|
||||
it('should have handleSortChange method', () => {
|
||||
wrapper = mount(UserManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleSortChange).toBe('function')
|
||||
})
|
||||
|
||||
it('should initialize with default sort info', () => {
|
||||
wrapper = mount(UserManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.sortInfo.sortBy).toBe('id')
|
||||
expect(wrapper.vm.sortInfo.sortOrder).toBe('asc')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,12 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
describe('Vitest Configuration Test', () => {
|
||||
it('should run a simple test', () => {
|
||||
expect(1 + 1).toBe(2)
|
||||
})
|
||||
|
||||
it('should handle async operations', async () => {
|
||||
const result = await Promise.resolve(42)
|
||||
expect(result).toBe(42)
|
||||
})
|
||||
})
|
||||
@@ -1,124 +0,0 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { permissionDirective } from '@/directives/permission'
|
||||
import { usePermissionStore } from '@/stores/permission'
|
||||
|
||||
describe('v-permission 指令', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
describe('角色检查', () => {
|
||||
it('有角色时应该显示元素', () => {
|
||||
const store = usePermissionStore()
|
||||
store.setPermissionData({
|
||||
roles: ['admin'],
|
||||
permissions: [],
|
||||
menus: []
|
||||
})
|
||||
|
||||
const wrapper = mount({
|
||||
template: '<button v-permission:role="\'admin\'">管理员按钮</button>',
|
||||
directives: {
|
||||
permission: permissionDirective
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.find('button').isVisible()).toBe(true)
|
||||
})
|
||||
|
||||
it('无角色时应该隐藏元素', () => {
|
||||
const store = usePermissionStore()
|
||||
store.setPermissionData({
|
||||
roles: ['user'],
|
||||
permissions: [],
|
||||
menus: []
|
||||
})
|
||||
|
||||
const wrapper = mount({
|
||||
template: '<button v-permission:role="\'admin\'">管理员按钮</button>',
|
||||
directives: {
|
||||
permission: permissionDirective
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.find('button').isVisible()).toBe(false)
|
||||
})
|
||||
|
||||
it('支持数组参数(满足任一即可)', () => {
|
||||
const store = usePermissionStore()
|
||||
store.setPermissionData({
|
||||
roles: ['user'],
|
||||
permissions: [],
|
||||
menus: []
|
||||
})
|
||||
|
||||
const wrapper = mount({
|
||||
template: '<button v-permission:role="[\'admin\', \'user\']">按钮</button>',
|
||||
directives: {
|
||||
permission: permissionDirective
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.find('button').isVisible()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('权限检查', () => {
|
||||
it('有权限时应该显示元素', () => {
|
||||
const store = usePermissionStore()
|
||||
store.setPermissionData({
|
||||
roles: [],
|
||||
permissions: ['user:delete'],
|
||||
menus: []
|
||||
})
|
||||
|
||||
const wrapper = mount({
|
||||
template: '<button v-permission:permission="\'user:delete\'">删除用户</button>',
|
||||
directives: {
|
||||
permission: permissionDirective
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.find('button').isVisible()).toBe(true)
|
||||
})
|
||||
|
||||
it('无权限时应该隐藏元素', () => {
|
||||
const store = usePermissionStore()
|
||||
store.setPermissionData({
|
||||
roles: [],
|
||||
permissions: ['user:read'],
|
||||
menus: []
|
||||
})
|
||||
|
||||
const wrapper = mount({
|
||||
template: '<button v-permission:permission="\'user:delete\'">删除用户</button>',
|
||||
directives: {
|
||||
permission: permissionDirective
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.find('button').isVisible()).toBe(false)
|
||||
})
|
||||
|
||||
it('支持简写形式(默认权限检查)', () => {
|
||||
const store = usePermissionStore()
|
||||
store.setPermissionData({
|
||||
roles: [],
|
||||
permissions: ['user:create'],
|
||||
menus: []
|
||||
})
|
||||
|
||||
const wrapper = mount({
|
||||
template: '<button v-permission="\'user:create\'">创建用户</button>',
|
||||
directives: {
|
||||
permission: permissionDirective
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.find('button').isVisible()).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,88 +0,0 @@
|
||||
export const mockUser = {
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
nickname: 'Test User',
|
||||
email: 'test@example.com',
|
||||
phone: '13800138000',
|
||||
avatar: 'https://example.com/avatar.jpg',
|
||||
roles: ['admin'],
|
||||
permissions: ['user:view', 'user:create', 'user:edit', 'user:delete'],
|
||||
}
|
||||
|
||||
export const mockRole = {
|
||||
id: 1,
|
||||
roleName: '测试角色',
|
||||
roleKey: 'test_role',
|
||||
roleSort: 1,
|
||||
status: '1',
|
||||
remark: '测试角色备注',
|
||||
createTime: new Date().toISOString(),
|
||||
updateTime: new Date().toISOString(),
|
||||
}
|
||||
|
||||
export const mockMenu = {
|
||||
id: 1,
|
||||
menuName: '系统管理',
|
||||
parentId: 0,
|
||||
orderNum: 1,
|
||||
menuType: 'M',
|
||||
component: 'system',
|
||||
perms: 'system:view',
|
||||
status: '1',
|
||||
createTime: new Date().toISOString(),
|
||||
updateTime: new Date().toISOString(),
|
||||
}
|
||||
|
||||
export const mockDict = {
|
||||
id: 1,
|
||||
dictName: '用户状态',
|
||||
dictType: 'user_status',
|
||||
status: '1',
|
||||
remark: '用户状态字典',
|
||||
createTime: new Date().toISOString(),
|
||||
updateTime: new Date().toISOString(),
|
||||
}
|
||||
|
||||
export const mockConfig = {
|
||||
id: 1,
|
||||
configName: '系统名称',
|
||||
configKey: 'sys.name',
|
||||
configValue: 'Novalon管理系统',
|
||||
configType: 'Y',
|
||||
status: '1',
|
||||
remark: '系统名称配置',
|
||||
createTime: new Date().toISOString(),
|
||||
updateTime: new Date().toISOString(),
|
||||
}
|
||||
|
||||
export const mockNotice = {
|
||||
id: 1,
|
||||
noticeTitle: '系统通知',
|
||||
noticeType: '1',
|
||||
noticeContent: '这是一条测试通知',
|
||||
status: '0',
|
||||
createTime: new Date().toISOString(),
|
||||
updateTime: new Date().toISOString(),
|
||||
}
|
||||
|
||||
export const mockLoginRequest = {
|
||||
username: 'admin',
|
||||
password: 'admin123',
|
||||
}
|
||||
|
||||
export const mockLoginResponse = {
|
||||
token: 'mock-jwt-token',
|
||||
user: mockUser,
|
||||
}
|
||||
|
||||
export const mockApiResponse = <T>(data: T, code = 200, message = 'success') => ({
|
||||
code,
|
||||
message,
|
||||
data,
|
||||
})
|
||||
|
||||
export const mockErrorResponse = (code = 500, message = 'Internal Server Error') => ({
|
||||
code,
|
||||
message,
|
||||
data: null,
|
||||
})
|
||||
@@ -1,291 +0,0 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
const mockLocalStorage = {
|
||||
store: {} as Record<string, string>,
|
||||
getItem(key: string) {
|
||||
return this.store[key] || null
|
||||
},
|
||||
setItem(key: string, value: string) {
|
||||
this.store[key] = value
|
||||
},
|
||||
removeItem(key: string) {
|
||||
delete this.store[key]
|
||||
},
|
||||
clear() {
|
||||
this.store = {}
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: mockLocalStorage
|
||||
})
|
||||
|
||||
const createTestRouter = (routes: RouteRecordRaw[]) => {
|
||||
return createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
}
|
||||
|
||||
describe('路由守卫权限检查', () => {
|
||||
beforeEach(() => {
|
||||
mockLocalStorage.clear()
|
||||
})
|
||||
|
||||
describe('基础认证检查', () => {
|
||||
it('未登录用户访问受保护路由应重定向到登录页', async () => {
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: { template: '<div>Login</div>' }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: { template: '<div>Layout</div>' },
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: { template: '<div>Dashboard</div>' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createTestRouter(routes)
|
||||
|
||||
router.beforeEach((to, _from, next) => {
|
||||
const token = localStorage.getItem('token')
|
||||
|
||||
if (to.meta.requiresAuth && !token) {
|
||||
next('/login')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
await router.push('/dashboard')
|
||||
expect(router.currentRoute.value.path).toBe('/login')
|
||||
})
|
||||
|
||||
it('已登录用户访问受保护路由应允许通过', async () => {
|
||||
mockLocalStorage.setItem('token', 'valid-token')
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: { template: '<div>Login</div>' }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: { template: '<div>Layout</div>' },
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: { template: '<div>Dashboard</div>' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createTestRouter(routes)
|
||||
|
||||
router.beforeEach((to, _from, next) => {
|
||||
const token = localStorage.getItem('token')
|
||||
|
||||
if (to.meta.requiresAuth && !token) {
|
||||
next('/login')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
await router.push('/dashboard')
|
||||
expect(router.currentRoute.value.path).toBe('/dashboard')
|
||||
})
|
||||
})
|
||||
|
||||
describe('角色权限检查', () => {
|
||||
it('普通用户访问管理员路由应重定向到403页面', async () => {
|
||||
mockLocalStorage.setItem('token', 'valid-token')
|
||||
mockLocalStorage.setItem('roles', JSON.stringify(['user']))
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: { template: '<div>Login</div>' }
|
||||
},
|
||||
{
|
||||
path: '/403',
|
||||
name: 'Forbidden',
|
||||
component: { template: '<div>403 Forbidden</div>' }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: { template: '<div>Layout</div>' },
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: { template: '<div>Dashboard</div>' }
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
name: 'UserManagement',
|
||||
component: { template: '<div>UserManagement</div>' },
|
||||
meta: { roles: ['admin'] }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createTestRouter(routes)
|
||||
|
||||
router.beforeEach((to, _from, next) => {
|
||||
const token = localStorage.getItem('token')
|
||||
const rolesStr = localStorage.getItem('roles')
|
||||
const userRoles = rolesStr ? JSON.parse(rolesStr) : []
|
||||
|
||||
if (to.meta.requiresAuth && !token) {
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
|
||||
if (to.meta.roles && Array.isArray(to.meta.roles)) {
|
||||
const hasRole = to.meta.roles.some((role: string) => userRoles.includes(role))
|
||||
if (!hasRole) {
|
||||
next('/403')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
await router.push('/users')
|
||||
expect(router.currentRoute.value.path).toBe('/403')
|
||||
})
|
||||
|
||||
it('管理员用户访问管理员路由应允许通过', async () => {
|
||||
mockLocalStorage.setItem('token', 'valid-token')
|
||||
mockLocalStorage.setItem('roles', JSON.stringify(['admin']))
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: { template: '<div>Login</div>' }
|
||||
},
|
||||
{
|
||||
path: '/403',
|
||||
name: 'Forbidden',
|
||||
component: { template: '<div>403 Forbidden</div>' }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: { template: '<div>Layout</div>' },
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: { template: '<div>Dashboard</div>' }
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
name: 'UserManagement',
|
||||
component: { template: '<div>UserManagement</div>' },
|
||||
meta: { roles: ['admin'] }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createTestRouter(routes)
|
||||
|
||||
router.beforeEach((to, _from, next) => {
|
||||
const token = localStorage.getItem('token')
|
||||
const rolesStr = localStorage.getItem('roles')
|
||||
const userRoles = rolesStr ? JSON.parse(rolesStr) : []
|
||||
|
||||
if (to.meta.requiresAuth && !token) {
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
|
||||
if (to.meta.roles && Array.isArray(to.meta.roles)) {
|
||||
const hasRole = to.meta.roles.some((role: string) => userRoles.includes(role))
|
||||
if (!hasRole) {
|
||||
next('/403')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
await router.push('/users')
|
||||
expect(router.currentRoute.value.path).toBe('/users')
|
||||
})
|
||||
|
||||
it('无角色要求的路由所有登录用户都可访问', async () => {
|
||||
mockLocalStorage.setItem('token', 'valid-token')
|
||||
mockLocalStorage.setItem('roles', JSON.stringify(['user']))
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: { template: '<div>Login</div>' }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: { template: '<div>Layout</div>' },
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: { template: '<div>Dashboard</div>' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createTestRouter(routes)
|
||||
|
||||
router.beforeEach((to, _from, next) => {
|
||||
const token = localStorage.getItem('token')
|
||||
const rolesStr = localStorage.getItem('roles')
|
||||
const userRoles = rolesStr ? JSON.parse(rolesStr) : []
|
||||
|
||||
if (to.meta.requiresAuth && !token) {
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
|
||||
if (to.meta.roles && Array.isArray(to.meta.roles)) {
|
||||
const hasRole = to.meta.roles.some((role: string) => userRoles.includes(role))
|
||||
if (!hasRole) {
|
||||
next('/403')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
await router.push('/dashboard')
|
||||
expect(router.currentRoute.value.path).toBe('/dashboard')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,61 +0,0 @@
|
||||
import { vi } from 'vitest'
|
||||
import { config } from '@vue/test-utils'
|
||||
|
||||
config.global.stubs = {
|
||||
transition: false,
|
||||
'transition-group': false,
|
||||
}
|
||||
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation((query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
})
|
||||
|
||||
const localStorageMock = (() => {
|
||||
let store: Record<string, string> = {}
|
||||
return {
|
||||
getItem: vi.fn((key: string) => store[key] || null),
|
||||
setItem: vi.fn((key: string, value: string) => {
|
||||
store[key] = value
|
||||
}),
|
||||
removeItem: vi.fn((key: string) => {
|
||||
delete store[key]
|
||||
}),
|
||||
clear: vi.fn(() => {
|
||||
store = {}
|
||||
}),
|
||||
}
|
||||
})()
|
||||
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: localStorageMock,
|
||||
})
|
||||
|
||||
const sessionStorageMock = (() => {
|
||||
let store: Record<string, string> = {}
|
||||
return {
|
||||
getItem: vi.fn((key: string) => store[key] || null),
|
||||
setItem: vi.fn((key: string, value: string) => {
|
||||
store[key] = value
|
||||
}),
|
||||
removeItem: vi.fn((key: string) => {
|
||||
delete store[key]
|
||||
}),
|
||||
clear: vi.fn(() => {
|
||||
store = {}
|
||||
}),
|
||||
}
|
||||
})()
|
||||
|
||||
Object.defineProperty(window, 'sessionStorage', {
|
||||
value: sessionStorageMock,
|
||||
})
|
||||
@@ -1,167 +0,0 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import { usePermissionStore } from '@/stores/permission'
|
||||
|
||||
describe('Permission Store', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
describe('基础功能', () => {
|
||||
it('应该正确初始化状态', () => {
|
||||
const store = usePermissionStore()
|
||||
|
||||
expect(store.roles).toEqual([])
|
||||
expect(store.permissions).toEqual([])
|
||||
expect(store.menus).toEqual([])
|
||||
expect(store.loaded).toBe(false)
|
||||
})
|
||||
|
||||
it('应该正确设置权限数据', () => {
|
||||
const store = usePermissionStore()
|
||||
|
||||
store.setPermissionData({
|
||||
roles: ['admin'],
|
||||
permissions: ['user:read', 'user:delete'],
|
||||
menus: [
|
||||
{
|
||||
id: '1',
|
||||
name: '仪表盘',
|
||||
path: '/dashboard',
|
||||
icon: 'Odometer',
|
||||
sort: 1
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
expect(store.roles).toEqual(['admin'])
|
||||
expect(store.permissions).toEqual(['user:read', 'user:delete'])
|
||||
expect(store.menus).toHaveLength(1)
|
||||
expect(store.loaded).toBe(true)
|
||||
})
|
||||
|
||||
it('应该正确清除权限数据', () => {
|
||||
const store = usePermissionStore()
|
||||
|
||||
store.setPermissionData({
|
||||
roles: ['admin'],
|
||||
permissions: ['user:read'],
|
||||
menus: []
|
||||
})
|
||||
|
||||
store.clearPermissionData()
|
||||
|
||||
expect(store.roles).toEqual([])
|
||||
expect(store.permissions).toEqual([])
|
||||
expect(store.menus).toEqual([])
|
||||
expect(store.loaded).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('权限检查方法', () => {
|
||||
it('应该正确检查单个角色', () => {
|
||||
const store = usePermissionStore()
|
||||
store.setPermissionData({
|
||||
roles: ['admin', 'user'],
|
||||
permissions: [],
|
||||
menus: []
|
||||
})
|
||||
|
||||
expect(store.hasRole('admin')).toBe(true)
|
||||
expect(store.hasRole('manager')).toBe(false)
|
||||
})
|
||||
|
||||
it('应该正确检查多个角色(满足任一即可)', () => {
|
||||
const store = usePermissionStore()
|
||||
store.setPermissionData({
|
||||
roles: ['user'],
|
||||
permissions: [],
|
||||
menus: []
|
||||
})
|
||||
|
||||
expect(store.hasRole(['admin', 'user'])).toBe(true)
|
||||
expect(store.hasRole(['admin', 'manager'])).toBe(false)
|
||||
})
|
||||
|
||||
it('应该正确检查单个权限', () => {
|
||||
const store = usePermissionStore()
|
||||
store.setPermissionData({
|
||||
roles: [],
|
||||
permissions: ['user:read', 'user:delete'],
|
||||
menus: []
|
||||
})
|
||||
|
||||
expect(store.hasPermission('user:read')).toBe(true)
|
||||
expect(store.hasPermission('user:create')).toBe(false)
|
||||
})
|
||||
|
||||
it('应该正确检查多个权限(满足任一即可)', () => {
|
||||
const store = usePermissionStore()
|
||||
store.setPermissionData({
|
||||
roles: [],
|
||||
permissions: ['user:read'],
|
||||
menus: []
|
||||
})
|
||||
|
||||
expect(store.hasPermission(['user:read', 'user:create'])).toBe(true)
|
||||
expect(store.hasPermission(['user:create', 'user:update'])).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('localStorage 持久化', () => {
|
||||
it('应该正确保存到 localStorage', () => {
|
||||
const store = usePermissionStore()
|
||||
|
||||
store.setPermissionData({
|
||||
roles: ['admin'],
|
||||
permissions: ['user:read'],
|
||||
menus: [
|
||||
{
|
||||
id: '1',
|
||||
name: '仪表盘',
|
||||
path: '/dashboard',
|
||||
sort: 1
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const stored = localStorage.getItem('permission')
|
||||
expect(stored).toBeTruthy()
|
||||
|
||||
const data = JSON.parse(stored!)
|
||||
expect(data.roles).toEqual(['admin'])
|
||||
expect(data.permissions).toEqual(['user:read'])
|
||||
expect(data.menus).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('应该正确从 localStorage 恢复', () => {
|
||||
localStorage.setItem('permission', JSON.stringify({
|
||||
roles: ['user'],
|
||||
permissions: ['user:read:self'],
|
||||
menus: []
|
||||
}))
|
||||
|
||||
const store = usePermissionStore()
|
||||
store.initFromStorage()
|
||||
|
||||
expect(store.roles).toEqual(['user'])
|
||||
expect(store.permissions).toEqual(['user:read:self'])
|
||||
expect(store.loaded).toBe(true)
|
||||
})
|
||||
|
||||
it('清除数据时应该同时清除 localStorage', () => {
|
||||
const store = usePermissionStore()
|
||||
|
||||
store.setPermissionData({
|
||||
roles: ['admin'],
|
||||
permissions: [],
|
||||
menus: []
|
||||
})
|
||||
|
||||
store.clearPermissionData()
|
||||
|
||||
expect(localStorage.getItem('permission')).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,61 +0,0 @@
|
||||
import { VueWrapper } from '@vue/test-utils'
|
||||
import { ComponentPublicInstance } from 'vue'
|
||||
|
||||
export interface TestHelpers {
|
||||
findByText: (text: string) => HTMLElement | null
|
||||
findByTestId: (testId: string) => HTMLElement | null
|
||||
clickByText: (text: string) => Promise<void>
|
||||
clickByTestId: (testId: string) => Promise<void>
|
||||
fillByTestId: (testId: string, value: string) => Promise<void>
|
||||
}
|
||||
|
||||
export function createTestHelpers(wrapper: VueWrapper<ComponentPublicInstance>): TestHelpers {
|
||||
return {
|
||||
findByText: (text: string) => {
|
||||
return wrapper.element.textContent?.includes(text) ? wrapper.element : null
|
||||
},
|
||||
findByTestId: (testId: string) => {
|
||||
return wrapper.element.querySelector(`[data-testid="${testId}"]`)
|
||||
},
|
||||
clickByText: async (text: string) => {
|
||||
const element = wrapper.element.textContent?.includes(text) ? wrapper.element : null
|
||||
if (element) {
|
||||
element.click()
|
||||
await wrapper.vm.$nextTick()
|
||||
}
|
||||
},
|
||||
clickByTestId: async (testId: string) => {
|
||||
const element = wrapper.element.querySelector(`[data-testid="${testId}"]`)
|
||||
if (element) {
|
||||
element.click()
|
||||
await wrapper.vm.$nextTick()
|
||||
}
|
||||
},
|
||||
fillByTestId: async (testId: string, value: string) => {
|
||||
const element = wrapper.element.querySelector(`[data-testid="${testId}"]`) as HTMLInputElement
|
||||
if (element) {
|
||||
element.value = value
|
||||
element.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
await wrapper.vm.$nextTick()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function waitFor(condition: () => boolean, timeout = 5000): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const startTime = Date.now()
|
||||
|
||||
const check = () => {
|
||||
if (condition()) {
|
||||
resolve()
|
||||
} else if (Date.now() - startTime > timeout) {
|
||||
reject(new Error(`Timeout waiting for condition`))
|
||||
} else {
|
||||
setTimeout(check, 100)
|
||||
}
|
||||
}
|
||||
|
||||
check()
|
||||
})
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { handleApiError, ApiErrorHandler } from '@/utils/errorHandler'
|
||||
|
||||
vi.mock('element-plus', () => ({
|
||||
ElMessage: {
|
||||
error: vi.fn(),
|
||||
success: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('errorHandler', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.stubGlobal('localStorage', {
|
||||
removeItem: vi.fn(),
|
||||
})
|
||||
vi.stubGlobal('window', {
|
||||
location: { href: '' },
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleApiError', () => {
|
||||
it('should call ApiErrorHandler.handle', () => {
|
||||
const mockError = { response: { status: 500, data: {} } }
|
||||
const handleSpy = vi.spyOn(ApiErrorHandler, 'handle')
|
||||
|
||||
handleApiError(mockError)
|
||||
|
||||
expect(handleSpy).toHaveBeenCalledWith(mockError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ApiErrorHandler.handle', () => {
|
||||
it('should handle network error', () => {
|
||||
const mockError = new Error('Network Error')
|
||||
const consoleSpy = vi.spyOn(console, 'error')
|
||||
|
||||
ApiErrorHandler.handle(mockError)
|
||||
|
||||
expect(ElMessage.error).toHaveBeenCalledWith('网络连接失败,请检查网络设置')
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Network Error:', mockError)
|
||||
})
|
||||
|
||||
it('should handle 400 Bad Request', () => {
|
||||
const mockError = {
|
||||
response: {
|
||||
status: 400,
|
||||
data: { message: 'Invalid parameters' },
|
||||
},
|
||||
}
|
||||
const consoleSpy = vi.spyOn(console, 'error')
|
||||
|
||||
ApiErrorHandler.handle(mockError)
|
||||
|
||||
expect(ElMessage.error).toHaveBeenCalledWith('Invalid parameters')
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Bad Request:', mockError.response.data)
|
||||
})
|
||||
|
||||
it('should handle 401 Unauthorized', () => {
|
||||
const mockError = {
|
||||
response: {
|
||||
status: 401,
|
||||
data: { message: 'Unauthorized' },
|
||||
},
|
||||
}
|
||||
const consoleSpy = vi.spyOn(console, 'error')
|
||||
|
||||
ApiErrorHandler.handle(mockError)
|
||||
|
||||
expect(ElMessage.error).toHaveBeenCalledWith('登录已过期,请重新登录')
|
||||
expect(localStorage.removeItem).toHaveBeenCalledWith('token')
|
||||
expect(window.location.href).toBe('/login')
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Unauthorized:', mockError.response.data)
|
||||
})
|
||||
|
||||
it('should handle 403 Forbidden', () => {
|
||||
const mockError = {
|
||||
response: {
|
||||
status: 403,
|
||||
data: { message: 'Access denied' },
|
||||
},
|
||||
}
|
||||
const consoleSpy = vi.spyOn(console, 'error')
|
||||
|
||||
ApiErrorHandler.handle(mockError)
|
||||
|
||||
expect(ElMessage.error).toHaveBeenCalledWith('没有权限访问该资源')
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Forbidden:', mockError.response.data)
|
||||
})
|
||||
|
||||
it('should handle 404 Not Found', () => {
|
||||
const mockError = {
|
||||
response: {
|
||||
status: 404,
|
||||
data: { message: 'Resource not found' },
|
||||
},
|
||||
}
|
||||
const consoleSpy = vi.spyOn(console, 'error')
|
||||
|
||||
ApiErrorHandler.handle(mockError)
|
||||
|
||||
expect(ElMessage.error).toHaveBeenCalledWith('Resource not found')
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Not Found:', mockError.response.data)
|
||||
})
|
||||
|
||||
it('should handle 409 Conflict', () => {
|
||||
const mockError = {
|
||||
response: {
|
||||
status: 409,
|
||||
data: { message: 'Resource conflict' },
|
||||
},
|
||||
}
|
||||
const consoleSpy = vi.spyOn(console, 'error')
|
||||
|
||||
ApiErrorHandler.handle(mockError)
|
||||
|
||||
expect(ElMessage.error).toHaveBeenCalledWith('Resource conflict')
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Conflict:', mockError.response.data)
|
||||
})
|
||||
|
||||
it('should handle 422 Validation Error with details', () => {
|
||||
const mockError = {
|
||||
response: {
|
||||
status: 422,
|
||||
data: {
|
||||
message: 'Validation failed',
|
||||
details: {
|
||||
username: 'Username is required',
|
||||
password: 'Password is too short',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
const consoleSpy = vi.spyOn(console, 'error')
|
||||
|
||||
ApiErrorHandler.handle(mockError)
|
||||
|
||||
expect(ElMessage.error).toHaveBeenCalledWith('Username is required、Password is too short')
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Validation Error:', mockError.response.data)
|
||||
})
|
||||
|
||||
it('should handle 422 Validation Error without details', () => {
|
||||
const mockError = {
|
||||
response: {
|
||||
status: 422,
|
||||
data: { message: 'Validation failed' },
|
||||
},
|
||||
}
|
||||
const consoleSpy = vi.spyOn(console, 'error')
|
||||
|
||||
ApiErrorHandler.handle(mockError)
|
||||
|
||||
expect(ElMessage.error).toHaveBeenCalledWith('Validation failed')
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Validation Error:', mockError.response.data)
|
||||
})
|
||||
|
||||
it('should handle 500 Internal Server Error', () => {
|
||||
const mockError = {
|
||||
response: {
|
||||
status: 500,
|
||||
data: { message: 'Server error' },
|
||||
},
|
||||
}
|
||||
const consoleSpy = vi.spyOn(console, 'error')
|
||||
|
||||
ApiErrorHandler.handle(mockError)
|
||||
|
||||
expect(ElMessage.error).toHaveBeenCalledWith('服务器内部错误,请稍后重试')
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Internal Server Error:', mockError.response.data)
|
||||
})
|
||||
|
||||
it('should handle 502 Service Unavailable', () => {
|
||||
const mockError = {
|
||||
response: {
|
||||
status: 502,
|
||||
data: { message: 'Service unavailable' },
|
||||
},
|
||||
}
|
||||
const consoleSpy = vi.spyOn(console, 'error')
|
||||
|
||||
ApiErrorHandler.handle(mockError)
|
||||
|
||||
expect(ElMessage.error).toHaveBeenCalledWith('服务暂时不可用,请稍后重试')
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Service Unavailable:', mockError.response.data)
|
||||
})
|
||||
|
||||
it('should handle 503 Service Unavailable', () => {
|
||||
const mockError = {
|
||||
response: {
|
||||
status: 503,
|
||||
data: { message: 'Service unavailable' },
|
||||
},
|
||||
}
|
||||
const consoleSpy = vi.spyOn(console, 'error')
|
||||
|
||||
ApiErrorHandler.handle(mockError)
|
||||
|
||||
expect(ElMessage.error).toHaveBeenCalledWith('服务暂时不可用,请稍后重试')
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Service Unavailable:', mockError.response.data)
|
||||
})
|
||||
|
||||
it('should handle 504 Gateway Timeout', () => {
|
||||
const mockError = {
|
||||
response: {
|
||||
status: 504,
|
||||
data: { message: 'Gateway timeout' },
|
||||
},
|
||||
}
|
||||
const consoleSpy = vi.spyOn(console, 'error')
|
||||
|
||||
ApiErrorHandler.handle(mockError)
|
||||
|
||||
expect(ElMessage.error).toHaveBeenCalledWith('服务暂时不可用,请稍后重试')
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Service Unavailable:', mockError.response.data)
|
||||
})
|
||||
|
||||
it('should handle unknown status code', () => {
|
||||
const mockError = {
|
||||
response: {
|
||||
status: 418,
|
||||
data: { message: 'I am a teapot' },
|
||||
},
|
||||
}
|
||||
const consoleSpy = vi.spyOn(console, 'error')
|
||||
|
||||
ApiErrorHandler.handle(mockError)
|
||||
|
||||
expect(ElMessage.error).toHaveBeenCalledWith('I am a teapot')
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Unknown Error:', mockError.response.data)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,60 @@
|
||||
import request from '@/utils/request'
|
||||
import type { PageResponse } from './user.api'
|
||||
|
||||
export interface ConfigItem {
|
||||
id: number
|
||||
configName: string
|
||||
configKey: string
|
||||
configValue: string
|
||||
configType: string
|
||||
remark: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface CreateConfigRequest {
|
||||
configName: string
|
||||
configKey: string
|
||||
configValue: string
|
||||
configType?: string
|
||||
remark?: string
|
||||
}
|
||||
|
||||
export interface UpdateConfigRequest {
|
||||
configName?: string
|
||||
configKey?: string
|
||||
configValue?: string
|
||||
configType?: string
|
||||
remark?: string
|
||||
}
|
||||
|
||||
export interface ConfigPageRequest {
|
||||
page: number
|
||||
size: number
|
||||
configName?: string
|
||||
configKey?: string
|
||||
configType?: string
|
||||
}
|
||||
|
||||
export const configApi = {
|
||||
getAll: () =>
|
||||
request.get<ConfigItem[]>('/sys/config'),
|
||||
|
||||
getPage: (params: ConfigPageRequest) =>
|
||||
request.get<PageResponse<ConfigItem>>('/sys/config/page', { params }),
|
||||
|
||||
getById: (id: number) =>
|
||||
request.get<ConfigItem>(`/sys/config/${id}`),
|
||||
|
||||
getByKey: (configKey: string) =>
|
||||
request.get<ConfigItem>(`/sys/config/key/${configKey}`),
|
||||
|
||||
create: (data: CreateConfigRequest) =>
|
||||
request.post<ConfigItem>('/sys/config', data),
|
||||
|
||||
update: (id: number, data: UpdateConfigRequest) =>
|
||||
request.put<ConfigItem>(`/sys/config/${id}`, data),
|
||||
|
||||
delete: (id: number) =>
|
||||
request.delete<void>(`/sys/config/${id}`),
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import request from '@/utils/request'
|
||||
import type { PageResponse } from './user.api'
|
||||
|
||||
export interface DictType {
|
||||
id: number
|
||||
dictName: string
|
||||
dictType: string
|
||||
status: number
|
||||
remark: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface DictData {
|
||||
id: number
|
||||
dictType: string
|
||||
dictLabel: string
|
||||
dictValue: string
|
||||
sort: number
|
||||
status: number
|
||||
remark: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface CreateDictTypeRequest {
|
||||
dictName: string
|
||||
dictType: string
|
||||
status?: number
|
||||
remark?: string
|
||||
}
|
||||
|
||||
export interface UpdateDictTypeRequest {
|
||||
dictName?: string
|
||||
dictType?: string
|
||||
status?: number
|
||||
remark?: string
|
||||
}
|
||||
|
||||
export interface CreateDictDataRequest {
|
||||
dictType: string
|
||||
dictLabel: string
|
||||
dictValue: string
|
||||
sort?: number
|
||||
status?: number
|
||||
remark?: string
|
||||
}
|
||||
|
||||
export interface UpdateDictDataRequest {
|
||||
dictType?: string
|
||||
dictLabel?: string
|
||||
dictValue?: string
|
||||
sort?: number
|
||||
status?: number
|
||||
remark?: string
|
||||
}
|
||||
|
||||
export interface DictPageRequest {
|
||||
page: number
|
||||
size: number
|
||||
dictName?: string
|
||||
dictType?: string
|
||||
status?: string
|
||||
}
|
||||
|
||||
export const dictApi = {
|
||||
getTypes: () =>
|
||||
request.get<DictType[]>('/dict/types'),
|
||||
|
||||
getTypeById: (id: number) =>
|
||||
request.get<DictType>(`/dict/types/${id}`),
|
||||
|
||||
createType: (data: CreateDictTypeRequest) =>
|
||||
request.post<DictType>('/dict/types', data),
|
||||
|
||||
updateType: (id: number, data: UpdateDictTypeRequest) =>
|
||||
request.put<DictType>(`/dict/types/${id}`, data),
|
||||
|
||||
deleteType: (id: number) =>
|
||||
request.delete<void>(`/dict/types/${id}`),
|
||||
|
||||
getDataByType: (dictType: string) =>
|
||||
request.get<DictData[]>(`/dict/data/type/${dictType}`),
|
||||
|
||||
getDataPage: (params: DictPageRequest & { dictType: string }) =>
|
||||
request.get<PageResponse<DictData>>('/dict/data/page', { params }),
|
||||
|
||||
createData: (data: CreateDictDataRequest) =>
|
||||
request.post<DictData>('/dict/data', data),
|
||||
|
||||
updateData: (id: number, data: UpdateDictDataRequest) =>
|
||||
request.put<DictData>(`/dict/data/${id}`, data),
|
||||
|
||||
deleteData: (id: number) =>
|
||||
request.delete<void>(`/dict/data/${id}`),
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import request from '@/utils/request'
|
||||
import type { PageResponse } from './user.api'
|
||||
|
||||
export interface FileInfo {
|
||||
id: number
|
||||
fileName: string
|
||||
filePath: string
|
||||
fileSize: number
|
||||
fileType: string
|
||||
mimeType: string
|
||||
uploadedBy: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface FilePageRequest {
|
||||
page: number
|
||||
size: number
|
||||
fileName?: string
|
||||
fileType?: string
|
||||
}
|
||||
|
||||
export const fileApi = {
|
||||
getPage: (params: FilePageRequest) =>
|
||||
request.get<PageResponse<FileInfo>>('/files/page', { params }),
|
||||
|
||||
upload: (file: File) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return request.post<FileInfo>('/files/upload', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
},
|
||||
|
||||
delete: (id: number) =>
|
||||
request.delete<void>(`/files/${id}`),
|
||||
|
||||
download: (id: number) =>
|
||||
request.get<Blob>(`/files/download/${id}`, { responseType: 'blob' }),
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import request from '@/utils/request'
|
||||
import type { PageResponse } from './user.api'
|
||||
import { NoticeStatus } from '@/constants/status'
|
||||
|
||||
export interface Notice {
|
||||
id: number
|
||||
title: string
|
||||
content: string
|
||||
type: string
|
||||
status: NoticeStatus
|
||||
createdBy: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface CreateNoticeRequest {
|
||||
title: string
|
||||
content: string
|
||||
type?: string
|
||||
status?: NoticeStatus
|
||||
}
|
||||
|
||||
export interface UpdateNoticeRequest {
|
||||
title?: string
|
||||
content?: string
|
||||
type?: string
|
||||
status?: NoticeStatus
|
||||
}
|
||||
|
||||
export interface NoticePageRequest {
|
||||
page: number
|
||||
size: number
|
||||
title?: string
|
||||
type?: string
|
||||
status?: string
|
||||
}
|
||||
|
||||
export const noticeApi = {
|
||||
getPage: (params: NoticePageRequest) =>
|
||||
request.get<PageResponse<Notice>>('/notice/page', { params }),
|
||||
|
||||
getById: (id: number) =>
|
||||
request.get<Notice>(`/notice/${id}`),
|
||||
|
||||
create: (data: CreateNoticeRequest) =>
|
||||
request.post<Notice>('/notice', data),
|
||||
|
||||
update: (id: number, data: UpdateNoticeRequest) =>
|
||||
request.put<Notice>(`/notice/${id}`, data),
|
||||
|
||||
delete: (id: number) =>
|
||||
request.delete<void>(`/notice/${id}`),
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import request from '@/utils/request'
|
||||
import { MenuStatus } from '@/constants/status'
|
||||
|
||||
export interface MenuItem {
|
||||
id: number
|
||||
name: string
|
||||
path: string
|
||||
icon: string
|
||||
component: string
|
||||
parentId: number
|
||||
sort: number
|
||||
type: 'directory' | 'menu' | 'button'
|
||||
permission: string
|
||||
status: MenuStatus
|
||||
visible: boolean
|
||||
children?: MenuItem[]
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface CreateMenuRequest {
|
||||
name: string
|
||||
path?: string
|
||||
icon?: string
|
||||
component?: string
|
||||
parentId: number
|
||||
sort: number
|
||||
type: 'directory' | 'menu' | 'button'
|
||||
permission?: string
|
||||
status?: MenuStatus
|
||||
visible?: boolean
|
||||
}
|
||||
|
||||
export interface UpdateMenuRequest {
|
||||
name?: string
|
||||
path?: string
|
||||
icon?: string
|
||||
component?: string
|
||||
parentId?: number
|
||||
sort?: number
|
||||
type?: 'directory' | 'menu' | 'button'
|
||||
permission?: string
|
||||
status?: MenuStatus
|
||||
visible?: boolean
|
||||
}
|
||||
|
||||
export const menuApi = {
|
||||
getAll: () =>
|
||||
request.get<MenuItem[]>('/menus'),
|
||||
|
||||
getById: (id: number) =>
|
||||
request.get<MenuItem>(`/menus/${id}`),
|
||||
|
||||
getTree: () =>
|
||||
request.get<MenuItem[]>('/menus/tree'),
|
||||
|
||||
create: (data: CreateMenuRequest) =>
|
||||
request.post<MenuItem>('/menus', data),
|
||||
|
||||
update: (id: number, data: UpdateMenuRequest) =>
|
||||
request.put<MenuItem>(`/menus/${id}`, data),
|
||||
|
||||
delete: (id: number) =>
|
||||
request.delete<void>(`/menus/${id}`),
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { AdminRole } from '../admin.role';
|
||||
|
||||
describe('AdminRole', () => {
|
||||
it('should have admin credentials', () => {
|
||||
expect(AdminRole.name).toBe('admin');
|
||||
expect(AdminRole.displayName).toBe('超级管理员');
|
||||
expect(AdminRole.credentials.username).toBe('admin');
|
||||
expect(AdminRole.credentials.password).toBe('Test@123');
|
||||
});
|
||||
|
||||
it('should have all permissions', () => {
|
||||
expect(AdminRole.permissions).toContain('user:*');
|
||||
expect(AdminRole.permissions).toContain('role:*');
|
||||
expect(AdminRole.permissions).toContain('menu:*');
|
||||
expect(AdminRole.cannotAccess).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should be able to create all resources', () => {
|
||||
expect(AdminRole.expectedBehaviors.canCreate).toContain('user');
|
||||
expect(AdminRole.expectedBehaviors.canCreate).toContain('role');
|
||||
expect(AdminRole.expectedBehaviors.canCreate).toContain('menu');
|
||||
});
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { RoleDefinition } from '../base.role';
|
||||
|
||||
describe('RoleDefinition', () => {
|
||||
it('should define required role properties', () => {
|
||||
const role: RoleDefinition = {
|
||||
name: 'test',
|
||||
displayName: '测试角色',
|
||||
credentials: {
|
||||
username: 'testuser',
|
||||
password: 'Test@123'
|
||||
},
|
||||
permissions: ['test:read', 'test:write'],
|
||||
cannotAccess: ['/admin'],
|
||||
expectedBehaviors: {
|
||||
canCreate: ['test'],
|
||||
canRead: ['test'],
|
||||
canUpdate: ['test'],
|
||||
canDelete: []
|
||||
}
|
||||
};
|
||||
|
||||
expect(role.name).toBe('test');
|
||||
expect(role.displayName).toBe('测试角色');
|
||||
expect(role.credentials.username).toBe('testuser');
|
||||
expect(role.credentials.password).toBe('Test@123');
|
||||
expect(role.permissions).toHaveLength(2);
|
||||
expect(role.cannotAccess).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { RoleFactory } from '../role-factory';
|
||||
|
||||
describe('RoleFactory', () => {
|
||||
it('should get admin role', () => {
|
||||
const role = RoleFactory.getRole('admin');
|
||||
expect(role.name).toBe('admin');
|
||||
expect(role.credentials.username).toBe('admin');
|
||||
});
|
||||
|
||||
it('should get user role', () => {
|
||||
const role = RoleFactory.getRole('user');
|
||||
expect(role.name).toBe('user');
|
||||
expect(role.credentials.username).toBe('normaluser');
|
||||
});
|
||||
|
||||
it('should throw error for unknown role', () => {
|
||||
expect(() => RoleFactory.getRole('unknown')).toThrow("Role 'unknown' not found");
|
||||
});
|
||||
|
||||
it('should get all roles', () => {
|
||||
const roles = RoleFactory.getAllRoles();
|
||||
expect(roles).toHaveLength(3);
|
||||
expect(roles.map(r => r.name)).toContain('admin');
|
||||
expect(roles.map(r => r.name)).toContain('user');
|
||||
expect(roles.map(r => r.name)).toContain('test');
|
||||
});
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
import type { RoleDefinition } from './base.role';
|
||||
|
||||
export const AdminRole: RoleDefinition = {
|
||||
name: 'admin',
|
||||
displayName: '超级管理员',
|
||||
credentials: {
|
||||
username: 'admin',
|
||||
password: 'Test@123'
|
||||
},
|
||||
permissions: [
|
||||
'user:*',
|
||||
'role:*',
|
||||
'menu:*',
|
||||
'config:*',
|
||||
'log:read',
|
||||
'dict:*'
|
||||
],
|
||||
cannotAccess: [],
|
||||
expectedBehaviors: {
|
||||
canCreate: ['user', 'role', 'menu', 'config', 'dict'],
|
||||
canRead: ['user', 'role', 'menu', 'config', 'dict', 'log'],
|
||||
canUpdate: ['user', 'role', 'menu', 'config', 'dict'],
|
||||
canDelete: ['user', 'role', 'menu', 'config', 'dict']
|
||||
}
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
export interface RoleDefinition {
|
||||
name: string;
|
||||
displayName: string;
|
||||
credentials: {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
permissions: string[];
|
||||
cannotAccess: string[];
|
||||
expectedBehaviors: {
|
||||
canCreate: string[];
|
||||
canRead: string[];
|
||||
canUpdate: string[];
|
||||
canDelete: string[];
|
||||
};
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { RoleDefinition } from './base.role';
|
||||
import { AdminRole } from './admin.role';
|
||||
import { UserRole } from './user.role';
|
||||
import { TestRole } from './test.role';
|
||||
|
||||
export class RoleFactory {
|
||||
private static roles: Map<string, RoleDefinition> = new Map([
|
||||
['admin', AdminRole],
|
||||
['user', UserRole],
|
||||
['test', TestRole]
|
||||
]);
|
||||
|
||||
static getRole(roleName: string): RoleDefinition {
|
||||
const role = this.roles.get(roleName);
|
||||
if (!role) {
|
||||
throw new Error(`Role '${roleName}' not found`);
|
||||
}
|
||||
return role;
|
||||
}
|
||||
|
||||
static getAllRoles(): RoleDefinition[] {
|
||||
return Array.from(this.roles.values());
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { RoleDefinition } from './base.role';
|
||||
|
||||
export const TestRole: RoleDefinition = {
|
||||
name: 'test',
|
||||
displayName: '测试用户',
|
||||
credentials: {
|
||||
username: 'e2e_test_user',
|
||||
password: 'Test@123'
|
||||
},
|
||||
permissions: [
|
||||
'test:read',
|
||||
'test:write'
|
||||
],
|
||||
cannotAccess: [
|
||||
'/user-management',
|
||||
'/role-management'
|
||||
],
|
||||
expectedBehaviors: {
|
||||
canCreate: ['test'],
|
||||
canRead: ['test'],
|
||||
canUpdate: ['test'],
|
||||
canDelete: []
|
||||
}
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
import type { RoleDefinition } from './base.role';
|
||||
|
||||
export const UserRole: RoleDefinition = {
|
||||
name: 'user',
|
||||
displayName: '普通用户',
|
||||
credentials: {
|
||||
username: 'normaluser',
|
||||
password: 'Test@123'
|
||||
},
|
||||
permissions: [
|
||||
'user:read:self',
|
||||
'user:update:self'
|
||||
],
|
||||
cannotAccess: [
|
||||
'/user-management',
|
||||
'/role-management',
|
||||
'/menu-management',
|
||||
'/system-config'
|
||||
],
|
||||
expectedBehaviors: {
|
||||
canCreate: [],
|
||||
canRead: ['self'],
|
||||
canUpdate: ['self'],
|
||||
canDelete: []
|
||||
}
|
||||
};
|
||||
@@ -1,68 +0,0 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { PermissionHelper } from '../permission-helper';
|
||||
|
||||
// Mock Playwright
|
||||
vi.mock('@playwright/test', () => ({
|
||||
expect: Object.assign(vi.fn(), {
|
||||
extend: vi.fn().mockReturnValue(expect),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('PermissionHelper', () => {
|
||||
it('should create PermissionHelper instance', () => {
|
||||
const mockPage = {
|
||||
goto: vi.fn(),
|
||||
url: vi.fn().mockReturnValue('http://localhost:3000/dashboard'),
|
||||
locator: vi.fn().mockReturnValue({
|
||||
count: vi.fn().mockResolvedValue(0),
|
||||
}),
|
||||
} as any;
|
||||
|
||||
const helper = new PermissionHelper(mockPage);
|
||||
expect(helper).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have verifyCanAccess method', () => {
|
||||
const mockPage = {
|
||||
goto: vi.fn(),
|
||||
url: vi.fn().mockReturnValue('http://localhost:3000/dashboard'),
|
||||
locator: vi.fn(),
|
||||
} as any;
|
||||
|
||||
const helper = new PermissionHelper(mockPage);
|
||||
expect(typeof helper.verifyCanAccess).toBe('function');
|
||||
});
|
||||
|
||||
it('should have verifyCannotAccess method', () => {
|
||||
const mockPage = {
|
||||
goto: vi.fn(),
|
||||
url: vi.fn(),
|
||||
locator: vi.fn(),
|
||||
} as any;
|
||||
|
||||
const helper = new PermissionHelper(mockPage);
|
||||
expect(typeof helper.verifyCannotAccess).toBe('function');
|
||||
});
|
||||
|
||||
it('should have verifyRolePermissions method', () => {
|
||||
const mockPage = {
|
||||
goto: vi.fn(),
|
||||
url: vi.fn(),
|
||||
locator: vi.fn(),
|
||||
} as any;
|
||||
|
||||
const helper = new PermissionHelper(mockPage);
|
||||
expect(typeof helper.verifyRolePermissions).toBe('function');
|
||||
});
|
||||
|
||||
it('should have verifyPermissionBoundary method', () => {
|
||||
const mockPage = {
|
||||
goto: vi.fn(),
|
||||
url: vi.fn(),
|
||||
locator: vi.fn(),
|
||||
} as any;
|
||||
|
||||
const helper = new PermissionHelper(mockPage);
|
||||
expect(typeof helper.verifyPermissionBoundary).toBe('function');
|
||||
});
|
||||
});
|
||||
@@ -1,80 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { RoleAuthManager } from '../role-auth-manager';
|
||||
|
||||
// Mock fetch
|
||||
global.fetch = vi.fn();
|
||||
|
||||
describe('RoleAuthManager', () => {
|
||||
beforeEach(() => {
|
||||
RoleAuthManager.clearCache();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should authenticate and cache token', async () => {
|
||||
const mockToken = 'mock-jwt-token-12345';
|
||||
(global.fetch as any).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ data: { token: mockToken } })
|
||||
});
|
||||
|
||||
const token = await RoleAuthManager.getRoleToken('admin');
|
||||
|
||||
expect(token).toBe(mockToken);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/api/auth/login'),
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: expect.stringContaining('admin')
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should return cached token on second call', async () => {
|
||||
const mockToken = 'cached-token';
|
||||
(global.fetch as any).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ data: { token: mockToken } })
|
||||
});
|
||||
|
||||
const token1 = await RoleAuthManager.getRoleToken('admin');
|
||||
const token2 = await RoleAuthManager.getRoleToken('admin');
|
||||
|
||||
expect(token1).toBe(token2);
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw error for unknown role', async () => {
|
||||
await expect(RoleAuthManager.getRoleToken('unknown')).rejects.toThrow("Role 'unknown' not found");
|
||||
});
|
||||
|
||||
it('should throw error on authentication failure', async () => {
|
||||
(global.fetch as any).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
text: async () => 'Invalid credentials'
|
||||
});
|
||||
|
||||
await expect(RoleAuthManager.getRoleToken('admin')).rejects.toThrow('Authentication failed');
|
||||
});
|
||||
|
||||
it('should clear specific role token', async () => {
|
||||
const mockToken = 'token-to-clear';
|
||||
(global.fetch as any).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ data: { token: mockToken } })
|
||||
});
|
||||
|
||||
await RoleAuthManager.getRoleToken('admin');
|
||||
RoleAuthManager.clearRoleToken('admin');
|
||||
|
||||
// 再次获取应该重新认证
|
||||
(global.fetch as any).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ data: { token: 'new-token' } })
|
||||
});
|
||||
|
||||
const newToken = await RoleAuthManager.getRoleToken('admin');
|
||||
expect(newToken).toBe('new-token');
|
||||
expect(global.fetch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -1,117 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { TestDataManager, getTestDataManager } from '../test-data-manager';
|
||||
|
||||
global.fetch = vi.fn();
|
||||
|
||||
describe('TestDataManager', () => {
|
||||
let manager: TestDataManager;
|
||||
|
||||
beforeEach(() => {
|
||||
manager = TestDataManager.getInstance();
|
||||
manager.clearTracking();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be a singleton', () => {
|
||||
const instance1 = getTestDataManager();
|
||||
const instance2 = getTestDataManager();
|
||||
expect(instance1).toBe(instance2);
|
||||
});
|
||||
|
||||
it('should create user and track it', async () => {
|
||||
const mockUserId = 'user-123';
|
||||
(global.fetch as any).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ data: { id: mockUserId } })
|
||||
});
|
||||
|
||||
const userData = {
|
||||
username: 'testuser',
|
||||
password: 'Test@123',
|
||||
email: 'test@example.com',
|
||||
};
|
||||
|
||||
const result = await manager.createUser(userData);
|
||||
|
||||
expect(result.id).toBe(mockUserId);
|
||||
expect(result.type).toBe('user');
|
||||
expect(result.data.username).toBe('testuser');
|
||||
expect(manager.getCreatedData('user')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should create role and track it', async () => {
|
||||
const mockRoleId = 'role-456';
|
||||
(global.fetch as any).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ data: { id: mockRoleId } })
|
||||
});
|
||||
|
||||
const roleData = {
|
||||
roleName: '测试角色',
|
||||
roleKey: 'test_role',
|
||||
};
|
||||
|
||||
const result = await manager.createRole(roleData);
|
||||
|
||||
expect(result.id).toBe(mockRoleId);
|
||||
expect(result.type).toBe('role');
|
||||
expect(manager.getCreatedData('role')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should cleanup created data', async () => {
|
||||
(global.fetch as any)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ data: { id: 'user-1' } })
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ data: { id: 'user-2' } })
|
||||
})
|
||||
.mockResolvedValueOnce({ ok: true })
|
||||
.mockResolvedValueOnce({ ok: true });
|
||||
|
||||
await manager.createUser({ username: 'user1', password: 'Test@123', email: 'user1@test.com' });
|
||||
await manager.createUser({ username: 'user2', password: 'Test@123', email: 'user2@test.com' });
|
||||
|
||||
expect(manager.getCreatedData('user')).toHaveLength(2);
|
||||
|
||||
await manager.cleanup('user');
|
||||
|
||||
expect(manager.getCreatedData('user')).toHaveLength(0);
|
||||
expect(global.fetch).toHaveBeenCalledTimes(4); // 2 creates + 2 deletes
|
||||
});
|
||||
|
||||
it('should cleanup all data types when no type specified', async () => {
|
||||
(global.fetch as any)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ data: { id: 'user-1' } })
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ data: { id: 'role-1' } })
|
||||
})
|
||||
.mockResolvedValueOnce({ ok: true })
|
||||
.mockResolvedValueOnce({ ok: true });
|
||||
|
||||
await manager.createUser({ username: 'user1', password: 'Test@123', email: 'user1@test.com' });
|
||||
await manager.createRole({ roleName: '角色1', roleKey: 'role1' });
|
||||
|
||||
await manager.cleanup();
|
||||
|
||||
expect(manager.getCreatedData('user')).toHaveLength(0);
|
||||
expect(manager.getCreatedData('role')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should throw error on creation failure', async () => {
|
||||
(global.fetch as any).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
statusText: 'Bad Request'
|
||||
});
|
||||
|
||||
await expect(
|
||||
manager.createUser({ username: 'test', password: 'Test@123', email: 'test@test.com' })
|
||||
).rejects.toThrow('Failed to create user');
|
||||
});
|
||||
});
|
||||
@@ -1,76 +0,0 @@
|
||||
import { Page, BrowserContext } from '@playwright/test';
|
||||
import { RoleFactory } from '../roles/role-factory';
|
||||
import { RoleAuthManager } from './role-auth-manager';
|
||||
import type { RoleDefinition } from '../roles/base.role';
|
||||
|
||||
export class AuthHelper {
|
||||
constructor(
|
||||
private page: Page,
|
||||
private context: BrowserContext
|
||||
) {}
|
||||
|
||||
async loginAsRole(roleName: string, useTokenInjection: boolean = true): Promise<void> {
|
||||
const role = RoleFactory.getRole(roleName);
|
||||
|
||||
if (useTokenInjection) {
|
||||
await this.injectToken(role);
|
||||
} else {
|
||||
await this.performLogin(role);
|
||||
}
|
||||
}
|
||||
|
||||
private async injectToken(role: RoleDefinition): Promise<void> {
|
||||
const token = await RoleAuthManager.getRoleToken(role.name);
|
||||
|
||||
// 注入token到localStorage
|
||||
await this.page.addInitScript((token) => {
|
||||
localStorage.setItem('token', token);
|
||||
localStorage.setItem('username', 'admin');
|
||||
}, token);
|
||||
|
||||
// 设置cookie
|
||||
await this.context.addCookies([
|
||||
{
|
||||
name: 'token',
|
||||
value: token,
|
||||
domain: 'localhost',
|
||||
path: '/',
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
private async performLogin(role: RoleDefinition): Promise<void> {
|
||||
await this.page.goto('/login');
|
||||
|
||||
await this.page.fill('input[placeholder*="用户名"]', role.credentials.username);
|
||||
await this.page.fill('input[placeholder*="密码"]', role.credentials.password);
|
||||
await this.page.click('button[type="submit"]');
|
||||
|
||||
// 等待登录成功跳转
|
||||
await this.page.waitForURL(/\/(dashboard|home)?/, { timeout: 10000 });
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
await this.page.click('[data-testid="user-menu"]');
|
||||
await this.page.click('[data-testid="logout-button"]');
|
||||
await this.page.waitForURL('/login');
|
||||
}
|
||||
|
||||
async clearAuth(): Promise<void> {
|
||||
await this.context.clearCookies();
|
||||
await this.page.evaluate(() => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function createAuthenticatedPage(
|
||||
page: Page,
|
||||
context: BrowserContext,
|
||||
roleName: string
|
||||
): Promise<AuthHelper> {
|
||||
const helper = new AuthHelper(page, context);
|
||||
await helper.loginAsRole(roleName);
|
||||
return helper;
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
import { Page, expect } from '@playwright/test';
|
||||
import type { RoleDefinition } from '../roles/base.role';
|
||||
|
||||
export class PermissionHelper {
|
||||
constructor(private page: Page) {}
|
||||
|
||||
async verifyCanAccess(path: string): Promise<void> {
|
||||
await this.page.goto(path);
|
||||
await expect(this.page).not.toHaveURL(/\/login/);
|
||||
await expect(this.page).not.toHaveURL(/\/403/);
|
||||
await expect(this.page).not.toHaveURL(/\/404/);
|
||||
}
|
||||
|
||||
async verifyCannotAccess(path: string): Promise<void> {
|
||||
await this.page.goto(path);
|
||||
|
||||
// 应该被重定向到登录页或显示403错误
|
||||
const url = this.page.url();
|
||||
const isForbidden = url.includes('/403') || url.includes('/login');
|
||||
|
||||
expect(isForbidden || await this.isAccessDenied()).toBeTruthy();
|
||||
}
|
||||
|
||||
private async isAccessDenied(): Promise<boolean> {
|
||||
const deniedMessage = this.page.locator('text=/无权限|权限不足|Access Denied|Forbidden/i');
|
||||
return await deniedMessage.count() > 0;
|
||||
}
|
||||
|
||||
async verifyCanCreate(_resource: string, createButtonSelector: string): Promise<void> {
|
||||
const createButton = this.page.locator(createButtonSelector);
|
||||
await expect(createButton).toBeVisible();
|
||||
await expect(createButton).toBeEnabled();
|
||||
}
|
||||
|
||||
async verifyCannotCreate(_resource: string, createButtonSelector: string): Promise<void> {
|
||||
const createButton = this.page.locator(createButtonSelector);
|
||||
const count = await createButton.count();
|
||||
|
||||
if (count > 0) {
|
||||
await expect(createButton).not.toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
async verifyCanEdit(_resourceId: string, editButtonSelector: string): Promise<void> {
|
||||
const editButton = this.page.locator(editButtonSelector);
|
||||
await expect(editButton).toBeVisible();
|
||||
await expect(editButton).toBeEnabled();
|
||||
}
|
||||
|
||||
async verifyCannotEdit(_resourceId: string, editButtonSelector: string): Promise<void> {
|
||||
const editButton = this.page.locator(editButtonSelector);
|
||||
const count = await editButton.count();
|
||||
|
||||
if (count > 0) {
|
||||
await expect(editButton).not.toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
async verifyCanDelete(_resourceId: string, deleteButtonSelector: string): Promise<void> {
|
||||
const deleteButton = this.page.locator(deleteButtonSelector);
|
||||
await expect(deleteButton).toBeVisible();
|
||||
await expect(deleteButton).toBeEnabled();
|
||||
}
|
||||
|
||||
async verifyCannotDelete(_resourceId: string, deleteButtonSelector: string): Promise<void> {
|
||||
const deleteButton = this.page.locator(deleteButtonSelector);
|
||||
const count = await deleteButton.count();
|
||||
|
||||
if (count > 0) {
|
||||
await expect(deleteButton).not.toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
async verifyRolePermissions(role: RoleDefinition): Promise<void> {
|
||||
// 验证可访问的路径
|
||||
for (const path of role.expectedBehaviors.canRead) {
|
||||
if (path !== 'self') {
|
||||
await this.verifyCanAccess(`/${path}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 验证不可访问的路径
|
||||
for (const path of role.cannotAccess) {
|
||||
await this.verifyCannotAccess(path);
|
||||
}
|
||||
}
|
||||
|
||||
async verifyPermissionBoundary(
|
||||
role: RoleDefinition,
|
||||
testScenarios: {
|
||||
resource: string;
|
||||
path: string;
|
||||
createButton?: string;
|
||||
editButton?: string;
|
||||
deleteButton?: string;
|
||||
}
|
||||
): Promise<void> {
|
||||
await this.page.goto(testScenarios.path);
|
||||
|
||||
// 验证创建权限
|
||||
if (testScenarios.createButton) {
|
||||
if (role.expectedBehaviors.canCreate.includes(testScenarios.resource)) {
|
||||
await this.verifyCanCreate(testScenarios.resource, testScenarios.createButton);
|
||||
} else {
|
||||
await this.verifyCannotCreate(testScenarios.resource, testScenarios.createButton);
|
||||
}
|
||||
}
|
||||
|
||||
// 验证编辑权限
|
||||
if (testScenarios.editButton) {
|
||||
if (role.expectedBehaviors.canUpdate.includes(testScenarios.resource)) {
|
||||
await this.verifyCanEdit(testScenarios.resource, testScenarios.editButton);
|
||||
} else {
|
||||
await this.verifyCannotEdit(testScenarios.resource, testScenarios.editButton);
|
||||
}
|
||||
}
|
||||
|
||||
// 验证删除权限
|
||||
if (testScenarios.deleteButton) {
|
||||
if (role.expectedBehaviors.canDelete.includes(testScenarios.resource)) {
|
||||
await this.verifyCanDelete(testScenarios.resource, testScenarios.deleteButton);
|
||||
} else {
|
||||
await this.verifyCannotDelete(testScenarios.resource, testScenarios.deleteButton);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createPermissionHelper(page: Page): PermissionHelper {
|
||||
return new PermissionHelper(page);
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { RoleFactory } from '../roles/role-factory';
|
||||
|
||||
interface TokenCache {
|
||||
token: string;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
export class RoleAuthManager {
|
||||
private static tokenCache: Map<string, TokenCache> = new Map();
|
||||
private static readonly API_BASE_URL = process.env.VITE_API_BASE_URL || 'http://localhost:8084';
|
||||
private static readonly TOKEN_EXPIRY_BUFFER = 60000;
|
||||
|
||||
static async getRoleToken(roleName: string): Promise<string> {
|
||||
const cached = this.tokenCache.get(roleName);
|
||||
|
||||
if (cached && cached.expiresAt > Date.now() + this.TOKEN_EXPIRY_BUFFER) {
|
||||
return cached.token;
|
||||
}
|
||||
|
||||
const role = RoleFactory.getRole(roleName);
|
||||
const token = await this.authenticateWithBackend(role.credentials);
|
||||
|
||||
this.tokenCache.set(roleName, {
|
||||
token,
|
||||
expiresAt: Date.now() + 3600000
|
||||
});
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
private static async authenticateWithBackend(credentials: { username: string; password: string }): Promise<string> {
|
||||
const path = '/api/auth/login';
|
||||
const body = JSON.stringify(credentials);
|
||||
|
||||
const response = await fetch(`${this.API_BASE_URL}${path}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Authentication failed for user ${credentials.username}: ${response.statusText} - ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.data?.token || data.token;
|
||||
}
|
||||
|
||||
static clearCache(): void {
|
||||
this.tokenCache.clear();
|
||||
}
|
||||
|
||||
static clearRoleToken(roleName: string): void {
|
||||
this.tokenCache.delete(roleName);
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
import { Page } from '@playwright/test';
|
||||
|
||||
export interface TestData {
|
||||
id: string;
|
||||
type: string;
|
||||
data: Record<string, any>;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export class TestDataManager {
|
||||
private static instance: TestDataManager;
|
||||
private createdData: Map<string, TestData[]> = new Map();
|
||||
private _page: Page | null = null;
|
||||
private static readonly API_BASE_URL = process.env.VITE_API_BASE_URL || 'http://localhost:8084';
|
||||
|
||||
static getInstance(): TestDataManager {
|
||||
if (!TestDataManager.instance) {
|
||||
TestDataManager.instance = new TestDataManager();
|
||||
}
|
||||
return TestDataManager.instance;
|
||||
}
|
||||
|
||||
setPage(page: Page): void {
|
||||
this._page = page;
|
||||
}
|
||||
|
||||
getPage(): Page | null {
|
||||
return this._page;
|
||||
}
|
||||
|
||||
async createUser(userData: {
|
||||
username: string;
|
||||
password: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
nickname?: string;
|
||||
}): Promise<TestData> {
|
||||
const response = await fetch(`${TestDataManager.API_BASE_URL}/api/users`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...userData,
|
||||
status: 1,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to create user: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
const testData: TestData = {
|
||||
id: result.data?.id || result.id,
|
||||
type: 'user',
|
||||
data: userData,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
this.trackData('user', testData);
|
||||
return testData;
|
||||
}
|
||||
|
||||
async createRole(roleData: {
|
||||
roleName: string;
|
||||
roleKey: string;
|
||||
roleSort?: number;
|
||||
}): Promise<TestData> {
|
||||
const response = await fetch(`${TestDataManager.API_BASE_URL}/api/roles`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...roleData,
|
||||
status: 1,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to create role: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
const testData: TestData = {
|
||||
id: result.data?.id || result.id,
|
||||
type: 'role',
|
||||
data: roleData,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
this.trackData('role', testData);
|
||||
return testData;
|
||||
}
|
||||
|
||||
async cleanup(type?: string): Promise<void> {
|
||||
const typesToClean = type ? [type] : Array.from(this.createdData.keys());
|
||||
|
||||
for (const dataType of typesToClean) {
|
||||
const items = this.createdData.get(dataType) || [];
|
||||
|
||||
for (const item of items.reverse()) {
|
||||
try {
|
||||
await this.deleteData(item);
|
||||
} catch (error) {
|
||||
console.error(`Failed to cleanup ${dataType} ${item.id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
this.createdData.delete(dataType);
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteData(data: TestData): Promise<void> {
|
||||
const endpoint = this.getEndpoint(data.type);
|
||||
await fetch(`${TestDataManager.API_BASE_URL}${endpoint}/${data.id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
private getEndpoint(type: string): string {
|
||||
const endpoints: Record<string, string> = {
|
||||
user: '/api/users',
|
||||
role: '/api/roles',
|
||||
menu: '/api/menus',
|
||||
config: '/api/configs',
|
||||
};
|
||||
return endpoints[type] || `/api/${type}s`;
|
||||
}
|
||||
|
||||
private trackData(type: string, data: TestData): void {
|
||||
if (!this.createdData.has(type)) {
|
||||
this.createdData.set(type, []);
|
||||
}
|
||||
this.createdData.get(type)!.push(data);
|
||||
}
|
||||
|
||||
getCreatedData(type: string): TestData[] {
|
||||
return this.createdData.get(type) || [];
|
||||
}
|
||||
|
||||
clearTracking(): void {
|
||||
this.createdData.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export function getTestDataManager(): TestDataManager {
|
||||
return TestDataManager.getInstance();
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import type { RouteRecordRaw, RouteLocationNormalized } from 'vue-router'
|
||||
|
||||
declare module 'vue-router' {
|
||||
interface RouteMeta {
|
||||
requiresAuth?: boolean
|
||||
roles?: string[]
|
||||
title?: string
|
||||
}
|
||||
}
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('@/views/system/Login.vue'),
|
||||
meta: { title: '登录' }
|
||||
},
|
||||
{
|
||||
path: '/403',
|
||||
name: 'Forbidden',
|
||||
component: () => import('@/views/system/Forbidden.vue'),
|
||||
meta: { title: '无权限' }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('@/layouts/DefaultLayout.vue'),
|
||||
redirect: '/dashboard',
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/system/Dashboard.vue'),
|
||||
meta: { title: '仪表盘' }
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
name: 'UserManagement',
|
||||
component: () => import('@/views/system/UserManagement.vue'),
|
||||
meta: { title: '用户管理' }
|
||||
},
|
||||
{
|
||||
path: 'roles',
|
||||
name: 'RoleManagement',
|
||||
component: () => import('@/views/system/RoleManagement.vue'),
|
||||
meta: { title: '角色管理' }
|
||||
},
|
||||
{
|
||||
path: 'menus',
|
||||
name: 'MenuManagement',
|
||||
component: () => import('@/views/system/MenuManagement.vue'),
|
||||
meta: { title: '菜单管理' }
|
||||
},
|
||||
{
|
||||
path: 'sys/config',
|
||||
name: 'ConfigManagement',
|
||||
component: () => import('@/views/config/ConfigManagement.vue'),
|
||||
meta: { title: '参数配置' }
|
||||
},
|
||||
{
|
||||
path: 'dict',
|
||||
name: 'DictManagement',
|
||||
component: () => import('@/views/config/DictManagement.vue'),
|
||||
meta: { title: '字典管理' }
|
||||
},
|
||||
{
|
||||
path: 'files',
|
||||
name: 'FileManagement',
|
||||
component: () => import('@/views/file/FileManagement.vue'),
|
||||
meta: { title: '文件管理' }
|
||||
},
|
||||
{
|
||||
path: 'notice',
|
||||
name: 'NoticeManagement',
|
||||
component: () => import('@/views/notify/NoticeManagement.vue'),
|
||||
meta: { title: '通知公告' }
|
||||
},
|
||||
{
|
||||
path: 'loginlog',
|
||||
name: 'LoginLog',
|
||||
component: () => import('@/views/audit/LoginLog.vue'),
|
||||
meta: { title: '登录日志' }
|
||||
},
|
||||
{
|
||||
path: 'oplog',
|
||||
name: 'OperationLog',
|
||||
component: () => import('@/views/audit/OperationLog.vue'),
|
||||
meta: { title: '操作日志' }
|
||||
},
|
||||
{
|
||||
path: 'exceptionlog',
|
||||
name: 'ExceptionLog',
|
||||
component: () => import('@/views/audit/ExceptionLog.vue'),
|
||||
meta: { title: '异常日志' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
function checkRoutePermission(route: RouteLocationNormalized, userRoles: string[]): boolean {
|
||||
if (!route.meta.roles || !Array.isArray(route.meta.roles) || route.meta.roles.length === 0) {
|
||||
return true
|
||||
}
|
||||
return route.meta.roles.some((role: string) => userRoles.includes(role))
|
||||
}
|
||||
|
||||
router.beforeEach((to, _from, next) => {
|
||||
try {
|
||||
const token = localStorage.getItem('token')
|
||||
const rolesStr = localStorage.getItem('roles')
|
||||
let userRoles: string[] = []
|
||||
|
||||
try {
|
||||
userRoles = rolesStr ? JSON.parse(rolesStr) : []
|
||||
} catch (e) {
|
||||
console.warn('解析用户角色失败,将使用空数组:', e)
|
||||
userRoles = []
|
||||
}
|
||||
|
||||
if (to.meta.title) {
|
||||
document.title = `${to.meta.title} - Novalon 管理系统`
|
||||
}
|
||||
|
||||
if (to.path === '/login') {
|
||||
if (token) {
|
||||
next('/')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
} else if (to.path === '/403') {
|
||||
next()
|
||||
} else {
|
||||
if (to.meta.requiresAuth !== false && !token) {
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
|
||||
if (!checkRoutePermission(to, userRoles)) {
|
||||
console.warn(`用户角色 ${userRoles} 无权访问路由 ${to.path},需要角色: ${to.meta.roles}`)
|
||||
next('/403')
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('路由守卫错误:', error)
|
||||
next('/login')
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -1,210 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import request from '@/utils/request'
|
||||
|
||||
export interface MenuItem {
|
||||
id: string
|
||||
name: string
|
||||
path: string
|
||||
icon?: string
|
||||
parentId?: string
|
||||
sort: number
|
||||
children?: MenuItem[]
|
||||
}
|
||||
|
||||
interface BackendMenuItem {
|
||||
id: string
|
||||
menuName: string
|
||||
parentId: string
|
||||
orderNum: number
|
||||
menuType: string
|
||||
perms?: string
|
||||
component?: string
|
||||
status: number
|
||||
children?: BackendMenuItem[]
|
||||
}
|
||||
|
||||
function transformMenuData(backendMenus: BackendMenuItem[]): MenuItem[] {
|
||||
const menuMap = new Map<string, MenuItem>()
|
||||
const rootMenus: MenuItem[] = []
|
||||
|
||||
const componentToPathMap: Record<string, string> = {
|
||||
'system/user/index': '/users',
|
||||
'system/role/index': '/roles',
|
||||
'system/menu/index': '/menus',
|
||||
'system/dict/index': '/dict',
|
||||
'system/config/index': '/sys/config',
|
||||
'system/notice/index': '/notice',
|
||||
'system/file/index': '/files',
|
||||
'audit/operation/index': '/oplog',
|
||||
'audit/login/index': '/loginlog',
|
||||
'audit/exception/index': '/exceptionlog',
|
||||
}
|
||||
|
||||
const filteredMenus = backendMenus.filter(menu => menu.menuType !== 'F')
|
||||
|
||||
filteredMenus.forEach(menu => {
|
||||
const menuItem: MenuItem = {
|
||||
id: menu.id,
|
||||
name: menu.menuName,
|
||||
path: menu.component ? (componentToPathMap[menu.component] || `/${menu.component.replace('/index', '').replace('system/', '')}`) : '',
|
||||
icon: getMenuIcon(menu.menuName),
|
||||
parentId: menu.parentId === '0' ? undefined : menu.parentId,
|
||||
sort: menu.orderNum
|
||||
}
|
||||
menuMap.set(menu.id, menuItem)
|
||||
})
|
||||
|
||||
filteredMenus.forEach(menu => {
|
||||
const menuItem = menuMap.get(menu.id)!
|
||||
if (menu.parentId === '0') {
|
||||
rootMenus.push(menuItem)
|
||||
} else {
|
||||
const parentMenu = menuMap.get(menu.parentId)
|
||||
if (parentMenu) {
|
||||
if (!parentMenu.children) {
|
||||
parentMenu.children = []
|
||||
}
|
||||
parentMenu.children.push(menuItem)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
rootMenus.forEach(menu => {
|
||||
if (menu.children) {
|
||||
menu.children.sort((a, b) => a.sort - b.sort)
|
||||
}
|
||||
})
|
||||
|
||||
return rootMenus.sort((a, b) => a.sort - b.sort)
|
||||
}
|
||||
|
||||
function getMenuIcon(menuName: string): string {
|
||||
const iconMap: Record<string, string> = {
|
||||
'系统管理': 'Setting',
|
||||
'审计日志': 'Document',
|
||||
'系统监控': 'Monitor',
|
||||
'用户管理': 'User',
|
||||
'角色管理': 'UserFilled',
|
||||
'菜单管理': 'Menu',
|
||||
'字典管理': 'Collection',
|
||||
'参数配置': 'Tools',
|
||||
'通知公告': 'Bell',
|
||||
'文件管理': 'Folder',
|
||||
'操作日志': 'Document',
|
||||
'登录日志': 'Document',
|
||||
'异常日志': 'Warning'
|
||||
}
|
||||
return iconMap[menuName] || 'Document'
|
||||
}
|
||||
|
||||
interface PermissionState {
|
||||
roles: string[]
|
||||
permissions: string[]
|
||||
menus: MenuItem[]
|
||||
loaded: boolean
|
||||
}
|
||||
|
||||
export const usePermissionStore = defineStore('permission', {
|
||||
state: (): PermissionState => ({
|
||||
roles: [],
|
||||
permissions: [],
|
||||
menus: [],
|
||||
loaded: false
|
||||
}),
|
||||
|
||||
getters: {
|
||||
hasRole: (state) => (role: string | string[]) => {
|
||||
if (Array.isArray(role)) {
|
||||
return role.some(r => state.roles.includes(r))
|
||||
}
|
||||
return state.roles.includes(role)
|
||||
},
|
||||
|
||||
hasPermission: (state) => (permission: string | string[]) => {
|
||||
if (Array.isArray(permission)) {
|
||||
return permission.some(p => state.permissions.includes(p))
|
||||
}
|
||||
return state.permissions.includes(permission)
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
setPermissionData(data: {
|
||||
roles: string[]
|
||||
permissions: string[]
|
||||
menus: MenuItem[]
|
||||
}) {
|
||||
this.roles = data.roles
|
||||
this.permissions = data.permissions
|
||||
this.menus = data.menus
|
||||
this.loaded = true
|
||||
|
||||
this.saveToStorage()
|
||||
},
|
||||
|
||||
clearPermissionData() {
|
||||
this.roles = []
|
||||
this.permissions = []
|
||||
this.menus = []
|
||||
this.loaded = false
|
||||
|
||||
localStorage.removeItem('permission')
|
||||
},
|
||||
|
||||
saveToStorage() {
|
||||
const data = {
|
||||
roles: this.roles,
|
||||
permissions: this.permissions,
|
||||
menus: this.menus
|
||||
}
|
||||
localStorage.setItem('permission', JSON.stringify(data))
|
||||
},
|
||||
|
||||
initFromStorage() {
|
||||
const stored = localStorage.getItem('permission')
|
||||
if (stored) {
|
||||
try {
|
||||
const data = JSON.parse(stored)
|
||||
this.roles = data.roles || []
|
||||
this.permissions = data.permissions || []
|
||||
this.menus = data.menus || []
|
||||
this.loaded = true
|
||||
} catch (error) {
|
||||
console.error('从 localStorage 恢复权限数据失败:', error)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async fetchUserMenus() {
|
||||
try {
|
||||
const res: any = await request.get('/menus')
|
||||
|
||||
if (res && Array.isArray(res)) {
|
||||
const transformedMenus = transformMenuData(res)
|
||||
|
||||
const permissions: string[] = []
|
||||
const extractPermissions = (menus: BackendMenuItem[]) => {
|
||||
menus.forEach(menu => {
|
||||
if (menu.perms) {
|
||||
permissions.push(menu.perms)
|
||||
}
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
extractPermissions(menu.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
extractPermissions(res)
|
||||
|
||||
this.setPermissionData({
|
||||
roles: JSON.parse(localStorage.getItem('roles') || '[]'),
|
||||
permissions: permissions,
|
||||
menus: transformedMenus
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户菜单失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
export interface MenuItem {
|
||||
id: number
|
||||
name: string
|
||||
path: string
|
||||
icon: string
|
||||
component: string
|
||||
parentId: number
|
||||
sort: number
|
||||
type: 'directory' | 'menu' | 'button'
|
||||
permission: string
|
||||
status: number
|
||||
visible: boolean
|
||||
children?: MenuItem[]
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface MenuTree {
|
||||
id: number
|
||||
name: string
|
||||
path: string
|
||||
icon: string
|
||||
parentId: number
|
||||
sort: number
|
||||
children?: MenuTree[]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
export interface PermissionState {
|
||||
permissions: string[]
|
||||
roles: string[]
|
||||
menus: MenuPermission[]
|
||||
}
|
||||
|
||||
export interface MenuPermission {
|
||||
id: number
|
||||
name: string
|
||||
path: string
|
||||
icon: string
|
||||
parentId: number
|
||||
sort: number
|
||||
children?: MenuPermission[]
|
||||
}
|
||||
|
||||
export interface PermissionCheckResult {
|
||||
hasPermission: boolean
|
||||
requiredPermission: string | null
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export interface AuthState {
|
||||
token: string | null
|
||||
userInfo: UserInfo | null
|
||||
isAuthenticated: boolean
|
||||
}
|
||||
|
||||
export interface UserInfo {
|
||||
id: number
|
||||
username: string
|
||||
nickname: string
|
||||
email: string
|
||||
phone: string
|
||||
avatar: string
|
||||
roles: string[]
|
||||
permissions: string[]
|
||||
}
|
||||
|
||||
export interface JwtPayload {
|
||||
sub: string
|
||||
username: string
|
||||
roles: string[]
|
||||
iat: number
|
||||
exp: number
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { message } from 'antd'
|
||||
|
||||
export interface ApiError {
|
||||
code: string
|
||||
@@ -51,59 +51,59 @@ export class ApiErrorHandler {
|
||||
}
|
||||
|
||||
private static handleNetworkError(error: any): void {
|
||||
ElMessage.error('网络连接失败,请检查网络设置')
|
||||
message.error('网络连接失败,请检查网络设置')
|
||||
console.error('Network Error:', error)
|
||||
}
|
||||
|
||||
private static handleBadRequest(error: ApiError): void {
|
||||
ElMessage.error(error.message || '请求参数错误')
|
||||
message.error(error.message || '请求参数错误')
|
||||
console.error('Bad Request:', error)
|
||||
}
|
||||
|
||||
private static handleUnauthorized(error: ApiError): void {
|
||||
ElMessage.error('登录已过期,请重新登录')
|
||||
message.error('登录已过期,请重新登录')
|
||||
localStorage.removeItem('token')
|
||||
window.location.href = '/login'
|
||||
console.error('Unauthorized:', error)
|
||||
}
|
||||
|
||||
private static handleForbidden(error: ApiError): void {
|
||||
ElMessage.error('没有权限访问该资源')
|
||||
message.error('没有权限访问该资源')
|
||||
console.error('Forbidden:', error)
|
||||
}
|
||||
|
||||
private static handleNotFound(error: ApiError): void {
|
||||
ElMessage.error(error.message || '请求的资源不存在')
|
||||
message.error(error.message || '请求的资源不存在')
|
||||
console.error('Not Found:', error)
|
||||
}
|
||||
|
||||
private static handleConflict(error: ApiError): void {
|
||||
ElMessage.error(error.message || '资源冲突,请刷新后重试')
|
||||
message.error(error.message || '资源冲突,请刷新后重试')
|
||||
console.error('Conflict:', error)
|
||||
}
|
||||
|
||||
private static handleValidationError(error: ApiError): void {
|
||||
if (error.details) {
|
||||
const messages = Object.values(error.details).join('、')
|
||||
ElMessage.error(messages)
|
||||
message.error(messages)
|
||||
} else {
|
||||
ElMessage.error(error.message || '数据验证失败')
|
||||
message.error(error.message || '数据验证失败')
|
||||
}
|
||||
console.error('Validation Error:', error)
|
||||
}
|
||||
|
||||
private static handleInternalServerError(error: ApiError): void {
|
||||
ElMessage.error('服务器内部错误,请稍后重试')
|
||||
message.error('服务器内部错误,请稍后重试')
|
||||
console.error('Internal Server Error:', error)
|
||||
}
|
||||
|
||||
private static handleServiceUnavailable(error: ApiError): void {
|
||||
ElMessage.error('服务暂时不可用,请稍后重试')
|
||||
message.error('服务暂时不可用,请稍后重试')
|
||||
console.error('Service Unavailable:', error)
|
||||
}
|
||||
|
||||
private static handleUnknownError(error: ApiError): void {
|
||||
ElMessage.error(error.message || '未知错误')
|
||||
message.error(error.message || '未知错误')
|
||||
console.error('Unknown Error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,235 +0,0 @@
|
||||
<template>
|
||||
<div class="exception-log">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div class="search-section">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索操作人或异常信息"
|
||||
clearable
|
||||
style="width: 300px"
|
||||
@clear="handleSearch"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSearch"
|
||||
>
|
||||
搜索
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="dataSource"
|
||||
style="width: 100%"
|
||||
@sort-change="handleSortChange"
|
||||
>
|
||||
<el-table-column
|
||||
prop="id"
|
||||
label="ID"
|
||||
sortable="custom"
|
||||
width="80"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="username"
|
||||
label="操作人"
|
||||
sortable="custom"
|
||||
width="120"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="operation"
|
||||
label="操作模块"
|
||||
sortable="custom"
|
||||
width="150"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="method"
|
||||
label="请求方法"
|
||||
sortable="custom"
|
||||
width="200"
|
||||
:show-overflow-tooltip="true"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="errorMsg"
|
||||
label="异常信息"
|
||||
:show-overflow-tooltip="true"
|
||||
width="250"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="ip"
|
||||
label="IP地址"
|
||||
sortable="custom"
|
||||
width="120"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="createTime"
|
||||
label="异常时间"
|
||||
sortable="custom"
|
||||
width="180"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="120"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleViewDetail(row)"
|
||||
>
|
||||
查看详情
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.current"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
style="margin-top: 16px; justify-content: flex-end"
|
||||
@current-change="handleTableChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
v-model="detailVisible"
|
||||
title="异常详情"
|
||||
width="800px"
|
||||
>
|
||||
<el-descriptions
|
||||
:column="1"
|
||||
border
|
||||
>
|
||||
<el-descriptions-item label="ID">
|
||||
{{ currentDetail.id }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="操作人">
|
||||
{{ currentDetail.username }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="操作模块">
|
||||
{{ currentDetail.operation }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="请求方法">
|
||||
{{ currentDetail.method }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="请求参数">
|
||||
<pre style="max-height: 200px; overflow: auto;">{{ currentDetail.params }}</pre>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="异常信息">
|
||||
<div style="color: #f56c6c; word-break: break-all;">
|
||||
{{ currentDetail.errorMsg }}
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="异常堆栈">
|
||||
<pre style="max-height: 300px; overflow: auto; font-size: 12px;">{{ currentDetail.exceptionStack }}</pre>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="IP地址">
|
||||
{{ currentDetail.ip }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="异常时间">
|
||||
{{ currentDetail.createTime }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #footer>
|
||||
<el-button @click="detailVisible = false">
|
||||
关闭
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { exceptionLogApi, ExceptionLog } from '@/api/exceptionLog'
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref<ExceptionLog[]>([])
|
||||
const searchKeyword = ref('')
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const sortInfo = reactive({
|
||||
sort: 'id',
|
||||
order: 'asc'
|
||||
})
|
||||
|
||||
const detailVisible = ref(false)
|
||||
const currentDetail = ref<ExceptionLog>({})
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await exceptionLogApi.getPage({
|
||||
page: pagination.current - 1,
|
||||
size: pagination.pageSize,
|
||||
sort: sortInfo.sort,
|
||||
order: sortInfo.order,
|
||||
keyword: searchKeyword.value || undefined
|
||||
})
|
||||
dataSource.value = res.content
|
||||
pagination.total = res.totalElements
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleTableChange = () => {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSizeChange = () => {
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSortChange = ({ prop, order }: any) => {
|
||||
sortInfo.sort = prop
|
||||
sortInfo.order = order === 'ascending' ? 'asc' : 'desc'
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleViewDetail = (row: ExceptionLog) => {
|
||||
currentDetail.value = { ...row }
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
onMounted(() => fetchData())
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.exception-log {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.search-section {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,176 +0,0 @@
|
||||
<template>
|
||||
<div class="login-log">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div class="search-section">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索用户名或IP地址"
|
||||
clearable
|
||||
style="width: 300px"
|
||||
@clear="handleSearch"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSearch"
|
||||
>
|
||||
搜索
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="dataSource"
|
||||
style="width: 100%"
|
||||
@sort-change="handleSortChange"
|
||||
>
|
||||
<el-table-column
|
||||
prop="id"
|
||||
label="ID"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="username"
|
||||
label="用户名"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="ip"
|
||||
label="IP地址"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="location"
|
||||
label="登录地点"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="browser"
|
||||
label="浏览器"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="os"
|
||||
label="操作系统"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column label="状态">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.status === '0' ? 'success' : 'danger'"
|
||||
effect="dark"
|
||||
style="font-weight: 500; font-size: 14px;"
|
||||
>
|
||||
{{ row.status === '0' ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="loginTime"
|
||||
label="登录时间"
|
||||
sortable="custom"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.loginTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.current"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
style="margin-top: 16px; justify-content: flex-end"
|
||||
@current-change="handleTableChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import request from '@/utils/request'
|
||||
import { formatDateTime } from '@/utils/dateFormat'
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref([])
|
||||
const searchKeyword = ref('')
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const sortInfo = reactive({
|
||||
sort: 'id',
|
||||
order: 'asc'
|
||||
})
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await request.get('/logs/login/page', {
|
||||
params: {
|
||||
page: pagination.current - 1,
|
||||
size: pagination.pageSize,
|
||||
sort: sortInfo.sort,
|
||||
order: sortInfo.order,
|
||||
keyword: searchKeyword.value || undefined
|
||||
}
|
||||
})
|
||||
dataSource.value = res.content
|
||||
pagination.total = res.totalElements
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleTableChange = () => {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSizeChange = () => {
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSortChange = ({ prop, order }: any) => {
|
||||
sortInfo.sort = prop
|
||||
sortInfo.order = order === 'ascending' ? 'asc' : 'desc'
|
||||
fetchData()
|
||||
}
|
||||
|
||||
onMounted(() => fetchData())
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.login-log {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.search-section {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,311 +0,0 @@
|
||||
<template>
|
||||
<div class="operation-log">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div class="search-section">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索操作人或操作模块"
|
||||
clearable
|
||||
style="width: 300px"
|
||||
@clear="handleSearch"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSearch"
|
||||
>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
@click="handleExport"
|
||||
>
|
||||
<el-icon><Download /></el-icon>
|
||||
导出
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="dataSource"
|
||||
style="width: 100%"
|
||||
@sort-change="handleSortChange"
|
||||
>
|
||||
<el-table-column
|
||||
prop="id"
|
||||
label="ID"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="username"
|
||||
label="操作人"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="operation"
|
||||
label="操作模块"
|
||||
sortable="custom"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="operation-cell">
|
||||
<el-icon class="operation-icon">
|
||||
<component :is="getOperationIcon(row.operation)" />
|
||||
</el-icon>
|
||||
<span>{{ row.operation }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="method"
|
||||
label="请求方法"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="params"
|
||||
label="请求参数"
|
||||
:show-overflow-tooltip="true"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div
|
||||
v-if="row.params"
|
||||
class="params-content"
|
||||
>
|
||||
<el-popover
|
||||
placement="top"
|
||||
:width="500"
|
||||
trigger="hover"
|
||||
>
|
||||
<template #reference>
|
||||
<div class="params-preview">
|
||||
{{ formatParams(row.params) }}
|
||||
</div>
|
||||
</template>
|
||||
<pre class="params-detail">{{ formatParams(row.params) }}</pre>
|
||||
</el-popover>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.status === '0' ? 'success' : 'danger'"
|
||||
effect="dark"
|
||||
style="font-weight: 500; font-size: 14px;"
|
||||
>
|
||||
{{ row.status === '0' ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="duration"
|
||||
label="耗时(ms)"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="createdAt"
|
||||
label="操作时间"
|
||||
sortable="custom"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.current"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
style="margin-top: 16px; justify-content: flex-end"
|
||||
@current-change="handleTableChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { Search, User, Document, Setting, Lock, View, Edit, Delete, Plus, Download } from '@element-plus/icons-vue'
|
||||
import { operationLogApi, OperationLog } from '@/api/operationLog'
|
||||
import { formatDateTime } from '@/utils/dateFormat'
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref<OperationLog[]>([])
|
||||
const searchKeyword = ref('')
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const sortInfo = reactive({
|
||||
sort: 'id',
|
||||
order: 'asc'
|
||||
})
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await operationLogApi.getPage({
|
||||
page: pagination.current - 1,
|
||||
size: pagination.pageSize,
|
||||
sort: sortInfo.sort,
|
||||
order: sortInfo.order,
|
||||
keyword: searchKeyword.value || undefined
|
||||
})
|
||||
dataSource.value = res.content
|
||||
pagination.total = res.totalElements
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleTableChange = () => {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSizeChange = () => {
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const params = new URLSearchParams()
|
||||
if (searchKeyword.value) {
|
||||
params.append('keyword', searchKeyword.value)
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/logs/operation/export?${params.toString()}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('导出失败')
|
||||
}
|
||||
|
||||
const blob = await response.blob()
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `operation_logs_${new Date().toISOString().slice(0, 19).replace(/[:-]/g, '')}.xlsx`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
} catch (error) {
|
||||
console.error('导出失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSortChange = ({ prop, order }: any) => {
|
||||
sortInfo.sort = prop
|
||||
sortInfo.order = order === 'ascending' ? 'asc' : 'desc'
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const formatParams = (params: string) => {
|
||||
if (!params) return ''
|
||||
try {
|
||||
const parsed = JSON.parse(params)
|
||||
return JSON.stringify(parsed, null, 2)
|
||||
} catch {
|
||||
return params
|
||||
}
|
||||
}
|
||||
|
||||
const getOperationIcon = (operation: string) => {
|
||||
if (!operation) return Document
|
||||
const op = operation.toLowerCase()
|
||||
if (op.includes('登录') || op.includes('login')) return User
|
||||
if (op.includes('删除') || op.includes('delete')) return Delete
|
||||
if (op.includes('编辑') || op.includes('修改') || op.includes('update') || op.includes('edit')) return Edit
|
||||
if (op.includes('查看') || op.includes('查询') || op.includes('view') || op.includes('get')) return View
|
||||
if (op.includes('新增') || op.includes('创建') || op.includes('add') || op.includes('create')) return Plus
|
||||
if (op.includes('下载') || op.includes('download')) return Download
|
||||
if (op.includes('设置') || op.includes('配置') || op.includes('setting') || op.includes('config')) return Setting
|
||||
if (op.includes('密码') || op.includes('password')) return Lock
|
||||
return Document
|
||||
}
|
||||
|
||||
onMounted(() => fetchData())
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.operation-log {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.search-section {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.operation-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.operation-icon {
|
||||
font-size: 16px;
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.params-content {
|
||||
.params-preview {
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.params-detail {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
background: #f5f7fa;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: #303133;
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,185 +0,0 @@
|
||||
<template>
|
||||
<div class="config-management">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-title">
|
||||
<span>参数配置</span>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleAdd"
|
||||
>
|
||||
新增配置
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="dataSource"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column
|
||||
prop="id"
|
||||
label="ID"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="configName"
|
||||
label="参数名称"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="configKey"
|
||||
label="参数键名"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="configValue"
|
||||
label="参数值"
|
||||
/>
|
||||
<el-table-column label="类型">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.configType === 'Y' ? 'info' : 'success'"
|
||||
effect="dark"
|
||||
style="font-weight: 500; font-size: 14px;"
|
||||
>
|
||||
{{ row.configType === 'Y' ? '内置' : '自定义' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="150"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
size="small"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
v-model="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="500px"
|
||||
>
|
||||
<el-form
|
||||
:model="formState"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item label="参数名称">
|
||||
<el-input v-model="formState.configName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="参数键名">
|
||||
<el-input v-model="formState.configKey" />
|
||||
</el-form-item>
|
||||
<el-form-item label="参数值">
|
||||
<el-input v-model="formState.configValue" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="modalVisible = false">
|
||||
取消
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleModalOk"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref([])
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const formState = reactive({ id: null, configName: '', configKey: '', configValue: '' })
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await request.get('/config')
|
||||
dataSource.value = res
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
modalTitle.value = '新增配置'
|
||||
Object.assign(formState, { id: null, configName: '', configKey: '', configValue: '' })
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
modalTitle.value = '编辑配置'
|
||||
Object.assign(formState, row)
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该配置吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await request.delete(`/config/${row.id}`)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('删除配置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleModalOk = async () => {
|
||||
try {
|
||||
console.log('handleModalOk called, formState:', formState)
|
||||
if (formState.id) {
|
||||
console.log('Sending PUT request to /config/' + formState.id)
|
||||
const response = await request.put(`/config/${formState.id}`, formState)
|
||||
console.log('PUT response:', response)
|
||||
} else {
|
||||
console.log('Sending POST request to /config')
|
||||
const response = await request.post('/config', formState)
|
||||
console.log('POST response:', response)
|
||||
}
|
||||
ElMessage.success('操作成功')
|
||||
modalVisible.value = false
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('handleModalOk error:', error)
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => fetchData())
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.config-management .card-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,194 +0,0 @@
|
||||
<template>
|
||||
<div class="dict-management">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-title">
|
||||
<span>字典管理</span>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleAdd"
|
||||
>
|
||||
新增字典
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="dataSource"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column
|
||||
prop="id"
|
||||
label="ID"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="dictName"
|
||||
label="字典名称"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="dictType"
|
||||
label="字典类型"
|
||||
/>
|
||||
<el-table-column label="状态">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.status === '0' ? 'success' : 'danger'"
|
||||
effect="dark"
|
||||
style="font-weight: 500; font-size: 14px;"
|
||||
>
|
||||
{{ row.status === '0' ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="remark"
|
||||
label="备注"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="200"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
size="small"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
v-model="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="500px"
|
||||
>
|
||||
<el-form
|
||||
:model="formState"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item label="字典名称">
|
||||
<el-input v-model="formState.dictName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="字典类型">
|
||||
<el-input v-model="formState.dictType" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="formState.status">
|
||||
<el-option
|
||||
value="0"
|
||||
label="正常"
|
||||
/>
|
||||
<el-option
|
||||
value="1"
|
||||
label="停用"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="formState.remark"
|
||||
type="textarea"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="modalVisible = false">
|
||||
取消
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleModalOk"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref([])
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const formState = reactive({ id: null, dictName: '', dictType: '', status: '0', remark: '' })
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await request.get('/dict/types')
|
||||
dataSource.value = res
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
modalTitle.value = '新增字典'
|
||||
Object.assign(formState, { id: null, dictName: '', dictType: '', status: '0', remark: '' })
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
modalTitle.value = '编辑字典'
|
||||
Object.assign(formState, row)
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该字典吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await request.delete(`/dict/types/${row.id}`)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('删除字典失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleModalOk = async () => {
|
||||
try {
|
||||
if (formState.id) {
|
||||
await request.put(`/dict/types/${formState.id}`, formState)
|
||||
} else {
|
||||
await request.post('/dict/types', formState)
|
||||
}
|
||||
ElMessage.success('操作成功')
|
||||
modalVisible.value = false
|
||||
fetchData()
|
||||
} catch {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => fetchData())
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.dict-management .card-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,200 +0,0 @@
|
||||
<template>
|
||||
<div class="file-management">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-title">
|
||||
<span>文件管理</span>
|
||||
<el-upload
|
||||
:before-upload="handleUpload"
|
||||
:show-file-list="false"
|
||||
>
|
||||
<el-button type="primary">
|
||||
<el-icon><Upload /></el-icon> 上传文件
|
||||
</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
</template>
|
||||
<div class="search-bar">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索文件名"
|
||||
clearable
|
||||
style="width: 300px"
|
||||
@input="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="filteredDataSource"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column
|
||||
prop="id"
|
||||
label="ID"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="fileName"
|
||||
label="文件名"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="fileSize"
|
||||
label="文件大小"
|
||||
/>
|
||||
<el-table-column label="文件类型">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getFileTypeTag(row.fileType)">
|
||||
{{ getFileTypeName(row.fileType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="storageType"
|
||||
label="存储方式"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="createdAt"
|
||||
label="上传时间"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="createBy"
|
||||
label="上传人"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="150"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click="handleDownload(row)"
|
||||
>
|
||||
下载
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
size="small"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Upload, Search } from '@element-plus/icons-vue'
|
||||
import request from '@/utils/request'
|
||||
import { formatDateTime } from '@/utils/dateFormat'
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref([])
|
||||
const searchKeyword = ref('')
|
||||
|
||||
const filteredDataSource = computed(() => {
|
||||
if (!searchKeyword.value) {
|
||||
return dataSource.value
|
||||
}
|
||||
return dataSource.value.filter((item: any) =>
|
||||
item.fileName.toLowerCase().includes(searchKeyword.value.toLowerCase())
|
||||
)
|
||||
})
|
||||
|
||||
const handleSearch = () => {
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await request.get('/files')
|
||||
dataSource.value = res
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpload = async (file: File) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
try {
|
||||
await request.post('/files/upload', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
ElMessage.success('上传成功')
|
||||
fetchData()
|
||||
} catch {
|
||||
ElMessage.error('上传失败')
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const handleDownload = (row: any) => {
|
||||
const downloadUrl = `/api/files/${row.id}/download`
|
||||
const link = document.createElement('a')
|
||||
link.href = downloadUrl
|
||||
link.download = row.fileName
|
||||
link.click()
|
||||
}
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该文件吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await request.delete(`/files/${row.id}`)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('删除文件失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const getFileTypeName = (fileType: string) => {
|
||||
if (!fileType) return '未知'
|
||||
if (fileType.startsWith('image/')) return '图片'
|
||||
if (fileType.startsWith('video/')) return '视频'
|
||||
if (fileType.startsWith('audio/')) return '音频'
|
||||
if (fileType.includes('pdf')) return 'PDF'
|
||||
if (fileType.includes('word') || fileType.includes('document')) return 'Word'
|
||||
if (fileType.includes('excel') || fileType.includes('spreadsheet')) return 'Excel'
|
||||
return '其他'
|
||||
}
|
||||
|
||||
const getFileTypeTag = (fileType: string): '' | 'success' | 'warning' | 'danger' | 'info' => {
|
||||
if (!fileType) return 'info'
|
||||
if (fileType.startsWith('image/')) return 'success'
|
||||
if (fileType.startsWith('video/')) return 'danger'
|
||||
if (fileType.startsWith('audio/')) return 'warning'
|
||||
if (fileType.includes('pdf')) return 'danger'
|
||||
if (fileType.includes('word')) return ''
|
||||
if (fileType.includes('excel')) return 'success'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
onMounted(() => fetchData())
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.file-management .card-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,222 +0,0 @@
|
||||
<template>
|
||||
<div class="notice-management">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-title">
|
||||
<span>通知公告</span>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleAdd"
|
||||
>
|
||||
新增公告
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="dataSource"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column
|
||||
prop="id"
|
||||
label="ID"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="noticeTitle"
|
||||
label="公告标题"
|
||||
/>
|
||||
<el-table-column
|
||||
label="公告类型"
|
||||
width="100"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.noticeType === '1' ? 'info' : 'success'"
|
||||
effect="dark"
|
||||
style="font-weight: 500; font-size: 14px;"
|
||||
>
|
||||
{{ row.noticeType === '1' ? '通知' : '公告' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="状态"
|
||||
width="80"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.status === '0' ? 'success' : 'danger'"
|
||||
effect="dark"
|
||||
style="font-weight: 500; font-size: 14px;"
|
||||
>
|
||||
{{ row.status === '0' ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="createdAt"
|
||||
label="发布时间"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="150"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
size="small"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
v-model="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="500px"
|
||||
>
|
||||
<el-form
|
||||
:model="formState"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item label="公告标题">
|
||||
<el-input v-model="formState.noticeTitle" />
|
||||
</el-form-item>
|
||||
<el-form-item label="公告类型">
|
||||
<el-select v-model="formState.noticeType">
|
||||
<el-option
|
||||
value="1"
|
||||
label="通知"
|
||||
/>
|
||||
<el-option
|
||||
value="2"
|
||||
label="公告"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="公告内容">
|
||||
<el-input
|
||||
v-model="formState.noticeContent"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="formState.status">
|
||||
<el-option
|
||||
value="0"
|
||||
label="正常"
|
||||
/>
|
||||
<el-option
|
||||
value="1"
|
||||
label="停用"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="modalVisible = false">
|
||||
取消
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleModalOk"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
import { formatDateTime } from '@/utils/dateFormat'
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref([])
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const formState = reactive({ id: null, noticeTitle: '', noticeType: '1', noticeContent: '', status: '0' })
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await request.get('/notices')
|
||||
dataSource.value = res
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
modalTitle.value = '新增公告'
|
||||
Object.assign(formState, { id: null, noticeTitle: '', noticeType: '1', noticeContent: '', status: '0' })
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
modalTitle.value = '编辑公告'
|
||||
Object.assign(formState, row)
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该公告吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await request.delete(`/notice/${row.id}`)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('删除通知失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleModalOk = async () => {
|
||||
try {
|
||||
if (formState.id) {
|
||||
await request.put(`/notices/${formState.id}`, formState)
|
||||
} else {
|
||||
await request.post('/notices', formState)
|
||||
}
|
||||
ElMessage.success('操作成功')
|
||||
modalVisible.value = false
|
||||
fetchData()
|
||||
} catch {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => fetchData())
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.notice-management .card-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,373 +0,0 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6">
|
||||
<el-card
|
||||
v-loading="loading"
|
||||
class="stat-card user-card"
|
||||
>
|
||||
<el-statistic
|
||||
title="用户总数"
|
||||
:value="stats.userCount"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="stat-icon user-icon">
|
||||
<User />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card
|
||||
v-loading="loading"
|
||||
class="stat-card role-card"
|
||||
>
|
||||
<el-statistic
|
||||
title="角色总数"
|
||||
:value="stats.roleCount"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="stat-icon role-icon">
|
||||
<UserFilled />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card
|
||||
v-loading="loading"
|
||||
class="stat-card login-card"
|
||||
>
|
||||
<el-statistic
|
||||
title="今日登录"
|
||||
:value="stats.todayLogin"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="stat-icon login-icon">
|
||||
<ArrowRight />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card
|
||||
v-loading="loading"
|
||||
class="stat-card log-card"
|
||||
>
|
||||
<el-statistic
|
||||
title="操作日志"
|
||||
:value="stats.operationLog"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="stat-icon log-icon">
|
||||
<Document />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row
|
||||
:gutter="16"
|
||||
style="margin-top: 16px"
|
||||
>
|
||||
<el-col :span="12">
|
||||
<el-card
|
||||
v-loading="loading"
|
||||
title="最近登录"
|
||||
class="recent-login-card"
|
||||
>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">最近登录</span>
|
||||
<el-icon class="header-icon">
|
||||
<Clock />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
<el-timeline>
|
||||
<el-timeline-item
|
||||
v-for="item in recentLogins"
|
||||
:key="item.id"
|
||||
:type="item.status === '0' ? 'success' : 'danger'"
|
||||
:timestamp="formatDateTime(item.loginTime)"
|
||||
placement="top"
|
||||
>
|
||||
<div class="login-item">
|
||||
<div class="login-user">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>{{ item.username }}</span>
|
||||
</div>
|
||||
<div class="login-ip">
|
||||
<el-icon><Location /></el-icon>
|
||||
<span>{{ item.ip }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
<el-timeline-item
|
||||
v-if="recentLogins.length === 0"
|
||||
placement="top"
|
||||
>
|
||||
<div class="empty-tip">
|
||||
暂无登录记录
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card
|
||||
v-loading="loading"
|
||||
title="系统信息"
|
||||
class="system-info-card"
|
||||
>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">系统信息</span>
|
||||
<el-icon class="header-icon">
|
||||
<Setting />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
<el-descriptions
|
||||
:column="1"
|
||||
border
|
||||
>
|
||||
<el-descriptions-item label="系统版本">
|
||||
<div class="info-item">
|
||||
<el-icon><Star /></el-icon>
|
||||
<span>{{ systemInfo.version }}</span>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="Java版本">
|
||||
<div class="info-item">
|
||||
<el-icon><Cpu /></el-icon>
|
||||
<span>{{ systemInfo.javaVersion }}</span>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="前端框架">
|
||||
<div class="info-item">
|
||||
<el-icon><Monitor /></el-icon>
|
||||
<span>{{ systemInfo.frontendFramework }}</span>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="数据库">
|
||||
<div class="info-item">
|
||||
<el-icon><Coin /></el-icon>
|
||||
<span>{{ systemInfo.database }}</span>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { User, UserFilled, ArrowRight, Document, Clock, Location, Setting, Star, Cpu, Monitor, Coin } from '@element-plus/icons-vue'
|
||||
import request from '@/utils/request'
|
||||
import { formatDateTime } from '@/utils/dateFormat'
|
||||
|
||||
const loading = ref(false)
|
||||
const stats = reactive({
|
||||
userCount: 0,
|
||||
roleCount: 0,
|
||||
todayLogin: 0,
|
||||
operationLog: 0
|
||||
})
|
||||
|
||||
const recentLogins = ref<any[]>([])
|
||||
const systemInfo = reactive({
|
||||
version: '1.0.0',
|
||||
javaVersion: '21',
|
||||
frontendFramework: 'Vue 3 + Element Plus',
|
||||
database: 'PostgreSQL'
|
||||
})
|
||||
|
||||
const fetchStats = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const [userCountRes, roleCountRes, todayLoginRes, operationLogRes] = await Promise.allSettled([
|
||||
request.get('/users/count'),
|
||||
request.get('/roles/count'),
|
||||
request.get('/logs/login/today/count'),
|
||||
request.get('/logs/operation/count')
|
||||
])
|
||||
|
||||
stats.userCount = userCountRes.status === 'fulfilled' ? Number(userCountRes.value || 0) : 0
|
||||
stats.roleCount = roleCountRes.status === 'fulfilled' ? Number(roleCountRes.value || 0) : 0
|
||||
stats.todayLogin = todayLoginRes.status === 'fulfilled' ? Number(todayLoginRes.value || 0) : 0
|
||||
stats.operationLog = operationLogRes.status === 'fulfilled' ? Number(operationLogRes.value || 0) : 0
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch stats:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const fetchRecentLogins = async () => {
|
||||
try {
|
||||
const res: any = await request.get('/logs/login/recent?limit=10')
|
||||
recentLogins.value = res || []
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch recent logins:', error)
|
||||
recentLogins.value = []
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchStats()
|
||||
fetchRecentLogins()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.dashboard {
|
||||
padding: 16px;
|
||||
|
||||
.stat-card {
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 8px;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 24px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&.user-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
|
||||
:deep(.el-statistic__head) {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
:deep(.el-statistic__content) {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&.role-card {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
color: white;
|
||||
|
||||
:deep(.el-statistic__head) {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
:deep(.el-statistic__content) {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&.login-card {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
color: white;
|
||||
|
||||
:deep(.el-statistic__head) {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
:deep(.el-statistic__content) {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&.log-card {
|
||||
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
||||
color: white;
|
||||
|
||||
:deep(.el-statistic__head) {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
:deep(.el-statistic__content) {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.recent-login-card,
|
||||
.system-info-card {
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-weight: 600;
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
color: #409eff;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-item {
|
||||
.login-user,
|
||||
.login-ip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin: 4px 0;
|
||||
font-size: 14px;
|
||||
|
||||
.el-icon {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.login-user {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.login-ip {
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.el-icon {
|
||||
color: #409eff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,45 +0,0 @@
|
||||
<template>
|
||||
<div class="forbidden-container">
|
||||
<el-result
|
||||
icon="warning"
|
||||
title="403"
|
||||
sub-title="抱歉,您没有权限访问此页面"
|
||||
>
|
||||
<template #extra>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="goBack"
|
||||
>
|
||||
返回上一页
|
||||
</el-button>
|
||||
<el-button @click="goHome">
|
||||
返回首页
|
||||
</el-button>
|
||||
</template>
|
||||
</el-result>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const goBack = () => {
|
||||
router.go(-1)
|
||||
}
|
||||
|
||||
const goHome = () => {
|
||||
router.push('/dashboard')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.forbidden-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
</style>
|
||||
@@ -1,143 +0,0 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<el-card class="login-card">
|
||||
<template #header>
|
||||
<h2>登录 - Novalon 管理系统</h2>
|
||||
</template>
|
||||
<el-form
|
||||
:model="formState"
|
||||
label-position="top"
|
||||
@submit.prevent="onFinish"
|
||||
>
|
||||
<el-form-item
|
||||
label="用户名"
|
||||
prop="username"
|
||||
:rules="[{ required: true, message: '请输入用户名', trigger: 'blur' }]"
|
||||
>
|
||||
<el-input
|
||||
v-model="formState.username"
|
||||
placeholder="请输入用户名"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="密码"
|
||||
prop="password"
|
||||
:rules="[{ required: true, message: '请输入密码', trigger: 'blur' }]"
|
||||
>
|
||||
<el-input
|
||||
v-model="formState.password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
native-type="submit"
|
||||
:loading="loading"
|
||||
style="width: 100%"
|
||||
>
|
||||
登录
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
import { onMounted } from 'vue'
|
||||
import { jwtDecode } from 'jwt-decode'
|
||||
import { usePermissionStore } from '@/stores/permission'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
const formState = reactive({
|
||||
username: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
document.title = '登录 - Novalon 管理系统'
|
||||
})
|
||||
|
||||
interface JwtPayload {
|
||||
userId: number
|
||||
username: string
|
||||
roles: string[]
|
||||
exp: number
|
||||
iat: number
|
||||
}
|
||||
|
||||
const onFinish = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
console.log('开始登录请求...')
|
||||
const res: any = await request.post('/auth/login', formState)
|
||||
console.log('登录响应:', res)
|
||||
|
||||
if (!res || !res.token) {
|
||||
console.error('登录失败:未收到有效响应')
|
||||
ElMessage.error('登录失败:未收到有效响应')
|
||||
return
|
||||
}
|
||||
|
||||
localStorage.setItem('token', res.token)
|
||||
if (res.userId) {
|
||||
localStorage.setItem('userId', String(res.userId))
|
||||
}
|
||||
if (res.username) {
|
||||
localStorage.setItem('username', res.username)
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwtDecode<JwtPayload>(res.token)
|
||||
if (decoded.roles && Array.isArray(decoded.roles)) {
|
||||
localStorage.setItem('roles', JSON.stringify(decoded.roles))
|
||||
}
|
||||
} catch (decodeError) {
|
||||
console.warn('解析Token中的角色信息失败:', decodeError)
|
||||
}
|
||||
|
||||
console.log('开始获取用户菜单...')
|
||||
try {
|
||||
await permissionStore.fetchUserMenus()
|
||||
console.log('获取用户菜单成功')
|
||||
} catch (menuError) {
|
||||
console.error('获取用户菜单失败:', menuError)
|
||||
}
|
||||
|
||||
ElMessage.success('登录成功')
|
||||
console.log('准备跳转到首页...')
|
||||
|
||||
await router.push('/')
|
||||
console.log('跳转完成')
|
||||
} catch (error: any) {
|
||||
console.error('登录错误:', error)
|
||||
ElMessage.error(error.response?.data?.message || error.message || '登录失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.login-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background: var(--el-color-primary-light-9);
|
||||
|
||||
.login-card {
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,291 +0,0 @@
|
||||
<template>
|
||||
<div class="menu-management">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-title">
|
||||
<span>菜单管理</span>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleAdd"
|
||||
>
|
||||
新增菜单
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="dataSource"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column
|
||||
prop="menuName"
|
||||
label="菜单名称"
|
||||
/>
|
||||
<el-table-column
|
||||
label="菜单类型"
|
||||
width="100"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.menuType === 'M' ? 'info' : row.menuType === 'C' ? 'success' : 'warning'"
|
||||
effect="dark"
|
||||
style="font-weight: 500; font-size: 14px;"
|
||||
>
|
||||
{{ row.menuType === 'M' ? '目录' : row.menuType === 'C' ? '菜单' : '按钮' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="perms"
|
||||
label="权限标识"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="component"
|
||||
label="组件"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="orderNum"
|
||||
label="排序"
|
||||
width="80"
|
||||
/>
|
||||
<el-table-column
|
||||
label="状态"
|
||||
width="80"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === '0' ? 'success' : 'danger'">
|
||||
{{ row.status === '0' ? '显示' : '隐藏' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="150"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
size="small"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
v-model="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="500px"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formState"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item
|
||||
label="菜单名称"
|
||||
prop="menuName"
|
||||
>
|
||||
<el-input v-model="formState.menuName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="父级菜单">
|
||||
<el-tree-select
|
||||
v-model="formState.parentId"
|
||||
:data="menuTree"
|
||||
placeholder="请选择父级菜单"
|
||||
clearable
|
||||
check-strictly
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="菜单类型"
|
||||
prop="menuType"
|
||||
>
|
||||
<el-select v-model="formState.menuType">
|
||||
<el-option
|
||||
value="M"
|
||||
label="目录"
|
||||
/>
|
||||
<el-option
|
||||
value="C"
|
||||
label="菜单"
|
||||
/>
|
||||
<el-option
|
||||
value="F"
|
||||
label="按钮"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<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="排序"
|
||||
prop="orderNum"
|
||||
>
|
||||
<el-input-number v-model="formState.orderNum" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="formState.status">
|
||||
<el-option
|
||||
value="0"
|
||||
label="显示"
|
||||
/>
|
||||
<el-option
|
||||
value="1"
|
||||
label="隐藏"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="modalVisible = false">
|
||||
取消
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleModalOk"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const loading = ref(false)
|
||||
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 {
|
||||
const res: any = await request.get('/menus')
|
||||
dataSource.value = res
|
||||
menuTree.value = buildTreeSelect(res)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const buildTreeSelect = (menus: any[]): any[] => {
|
||||
return menus.map(m => ({ value: m.id, label: m.menuName, children: m.children ? buildTreeSelect(m.children) : undefined }))
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
modalTitle.value = '新增菜单'
|
||||
Object.assign(formState, { id: null, menuName: '', parentId: 0, menuType: 'C', perms: '', component: '', orderNum: 0, status: '0' })
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
modalTitle.value = '编辑菜单'
|
||||
Object.assign(formState, row)
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该菜单吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await request.delete(`/menus/${row.id}`)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('删除菜单失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleModalOk = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
|
||||
if (formState.id) {
|
||||
await request.put(`/menus/${formState.id}`, formState)
|
||||
} else {
|
||||
await request.post('/menus', formState)
|
||||
}
|
||||
ElMessage.success('操作成功')
|
||||
modalVisible.value = false
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => fetchData())
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.menu-management .card-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,469 +0,0 @@
|
||||
<template>
|
||||
<div class="role-management">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div class="search-section">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索角色名称或标识"
|
||||
clearable
|
||||
style="width: 300px"
|
||||
@clear="handleSearch"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSearch"
|
||||
>
|
||||
搜索
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleAdd"
|
||||
>
|
||||
新增角色
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="dataSource"
|
||||
style="width: 100%"
|
||||
@sort-change="handleSortChange"
|
||||
>
|
||||
<el-table-column
|
||||
prop="id"
|
||||
label="ID"
|
||||
width="80"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="roleName"
|
||||
label="角色名称"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="roleKey"
|
||||
label="角色标识"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="roleSort"
|
||||
label="显示顺序"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
label="状态"
|
||||
width="100"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.status === RoleStatus.ACTIVE ? 'success' : 'danger'"
|
||||
effect="dark"
|
||||
style="font-weight: 500; font-size: 14px;"
|
||||
>
|
||||
{{ row.status === RoleStatus.ACTIVE ? '正常' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="createdAt"
|
||||
label="创建时间"
|
||||
sortable="custom"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="250"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
link
|
||||
@click="handleAssignPermissions(row)"
|
||||
>
|
||||
分配权限
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.current"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
style="margin-top: 16px; justify-content: flex-end"
|
||||
@current-change="handleTableChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
v-model="modalVisible"
|
||||
:title="modalTitle"
|
||||
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
|
||||
v-model="formState.roleKey"
|
||||
:disabled="!!formState.id"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="显示顺序"
|
||||
prop="roleSort"
|
||||
required
|
||||
>
|
||||
<el-input-number
|
||||
v-model="formState.roleSort"
|
||||
:min="1"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="formState.status">
|
||||
<el-option
|
||||
value="ACTIVE"
|
||||
label="正常"
|
||||
/>
|
||||
<el-option
|
||||
value="INACTIVE"
|
||||
label="禁用"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="modalVisible = false">
|
||||
取消
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleModalOk"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="permissionDialogVisible"
|
||||
title="分配权限"
|
||||
width="600px"
|
||||
>
|
||||
<el-tree
|
||||
ref="permissionTreeRef"
|
||||
:data="permissionTree"
|
||||
:props="{ label: 'name', children: 'children' }"
|
||||
show-checkbox
|
||||
node-key="id"
|
||||
:default-checked-keys="selectedPermissions"
|
||||
check-strictly
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="permissionDialogVisible = false">
|
||||
取消
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleAssignPermissionsOk"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { roleApi, type Role, type CreateRoleRequest, type UpdateRoleRequest, type Permission } from '@/api/role.api'
|
||||
import { handleApiError } from '@/utils/errorHandler'
|
||||
import { RoleStatus } from '@/constants/status'
|
||||
import { formatDateTime } from '@/utils/dateFormat'
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref<Role[]>([])
|
||||
const searchKeyword = ref('')
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const sortInfo = reactive({
|
||||
sortBy: 'id',
|
||||
sortOrder: 'asc' as 'asc' | 'desc'
|
||||
})
|
||||
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const formRef = ref()
|
||||
const formState = reactive<CreateRoleRequest & { id?: number; status?: RoleStatus }>({
|
||||
roleName: '',
|
||||
roleKey: '',
|
||||
roleSort: 1,
|
||||
permissions: [],
|
||||
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[]>([])
|
||||
const selectedPermissions = ref<number[]>([])
|
||||
const currentRoleId = ref<number | null>(null)
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await roleApi.getPage({
|
||||
page: pagination.current - 1,
|
||||
size: pagination.pageSize,
|
||||
sortBy: sortInfo.sortBy,
|
||||
sortOrder: sortInfo.sortOrder,
|
||||
roleName: searchKeyword.value || undefined
|
||||
})
|
||||
dataSource.value = res.content
|
||||
pagination.total = Number(res.totalElements) || 0
|
||||
} catch (error) {
|
||||
handleApiError(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleTableChange = () => {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSizeChange = () => {
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSortChange = ({ prop, order }: any) => {
|
||||
sortInfo.sortBy = prop
|
||||
sortInfo.sortOrder = order === 'ascending' ? 'asc' : 'desc'
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
modalTitle.value = '新增角色'
|
||||
Object.assign(formState, {
|
||||
id: undefined,
|
||||
roleName: '',
|
||||
roleKey: '',
|
||||
roleSort: 1,
|
||||
permissions: [],
|
||||
status: RoleStatus.ACTIVE
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row: Role) => {
|
||||
modalTitle.value = '编辑角色'
|
||||
Object.assign(formState, {
|
||||
id: row.id,
|
||||
roleName: row.roleName,
|
||||
roleKey: row.roleKey,
|
||||
roleSort: row.roleSort,
|
||||
status: row.status,
|
||||
permissions: []
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (row: Role) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该角色吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await roleApi.delete(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
handleApiError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleModalOk = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
|
||||
if (formState.id) {
|
||||
const updateData: UpdateRoleRequest = {
|
||||
roleName: formState.roleName,
|
||||
roleKey: formState.roleKey,
|
||||
roleSort: formState.roleSort,
|
||||
status: formState.status
|
||||
}
|
||||
await roleApi.update(formState.id, updateData)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
const createData: CreateRoleRequest = {
|
||||
roleName: formState.roleName,
|
||||
roleKey: formState.roleKey,
|
||||
roleSort: formState.roleSort,
|
||||
permissions: formState.permissions
|
||||
}
|
||||
await roleApi.create(createData)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
modalVisible.value = false
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
modalVisible.value = false
|
||||
if (error !== 'cancel') {
|
||||
handleApiError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleAssignPermissions = async (row: Role) => {
|
||||
currentRoleId.value = row.id
|
||||
try {
|
||||
const allPermissions = await roleApi.getAllPermissions()
|
||||
permissionTree.value = buildPermissionTree(allPermissions)
|
||||
|
||||
const rolePermissions = await roleApi.getPermissions(row.id)
|
||||
selectedPermissions.value = rolePermissions.map((p: Permission) => p.id)
|
||||
|
||||
permissionDialogVisible.value = true
|
||||
} catch (error) {
|
||||
handleApiError(error)
|
||||
}
|
||||
}
|
||||
|
||||
const buildPermissionTree = (permissions: Permission[]): any[] => {
|
||||
const tree: any[] = []
|
||||
const resourceMap = new Map<string, Permission[]>()
|
||||
|
||||
permissions.forEach(p => {
|
||||
if (!resourceMap.has(p.resource)) {
|
||||
resourceMap.set(p.resource, [])
|
||||
}
|
||||
resourceMap.get(p.resource)!.push(p)
|
||||
})
|
||||
|
||||
resourceMap.forEach((perms, resource) => {
|
||||
tree.push({
|
||||
id: `resource-${resource}`,
|
||||
name: resource,
|
||||
children: perms.map(p => ({
|
||||
id: p.id,
|
||||
name: p.name
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
return tree
|
||||
}
|
||||
|
||||
const handleAssignPermissionsOk = async () => {
|
||||
if (!currentRoleId.value) return
|
||||
|
||||
try {
|
||||
const checkedNodes = permissionTreeRef.value.getCheckedNodes()
|
||||
const permissionIds = checkedNodes
|
||||
.filter((node: any) => typeof node.id === 'number')
|
||||
.map((node: any) => node.id)
|
||||
|
||||
await roleApi.assignPermissions(currentRoleId.value, permissionIds)
|
||||
ElMessage.success('权限分配成功')
|
||||
permissionDialogVisible.value = false
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
handleApiError(error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.role-management {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.search-section {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,461 +0,0 @@
|
||||
<template>
|
||||
<div class="user-management">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div class="search-section">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索用户名或邮箱"
|
||||
clearable
|
||||
style="width: 300px"
|
||||
@clear="handleSearch"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSearch"
|
||||
>
|
||||
搜索
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleAdd"
|
||||
>
|
||||
新增用户
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="dataSource"
|
||||
style="width: 100%"
|
||||
@sort-change="handleSortChange"
|
||||
>
|
||||
<el-table-column
|
||||
prop="id"
|
||||
label="ID"
|
||||
width="80"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="username"
|
||||
label="用户名"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="nickname"
|
||||
label="昵称"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="email"
|
||||
label="邮箱"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="phone"
|
||||
label="手机号"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
label="状态"
|
||||
width="100"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.status === 1 ? 'success' : 'danger'"
|
||||
effect="dark"
|
||||
style="font-weight: 500; font-size: 14px;"
|
||||
>
|
||||
{{ row.status === 1 ? '正常' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="createdAt"
|
||||
label="创建时间"
|
||||
sortable="custom"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="250"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
link
|
||||
@click="handleAssignRoles(row)"
|
||||
>
|
||||
分配角色
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.current"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
style="margin-top: 16px; justify-content: flex-end"
|
||||
@current-change="handleTableChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
v-model="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="500px"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formState"
|
||||
:rules="formRules"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item
|
||||
label="用户名"
|
||||
prop="username"
|
||||
required
|
||||
>
|
||||
<el-input
|
||||
v-model="formState.username"
|
||||
:disabled="!!formState.id"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="!formState.id"
|
||||
label="密码"
|
||||
prop="password"
|
||||
required
|
||||
>
|
||||
<el-input
|
||||
v-model="formState.password"
|
||||
type="password"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="昵称">
|
||||
<el-input v-model="formState.nickname" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱">
|
||||
<el-input v-model="formState.email" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号">
|
||||
<el-input v-model="formState.phone" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="formState.status">
|
||||
<el-option
|
||||
:value="UserStatus.ACTIVE"
|
||||
label="正常"
|
||||
/>
|
||||
<el-option
|
||||
:value="UserStatus.INACTIVE"
|
||||
label="禁用"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleModalCancel">
|
||||
取消
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleModalOk"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="roleDialogVisible"
|
||||
title="分配角色"
|
||||
width="500px"
|
||||
>
|
||||
<el-transfer
|
||||
v-model="selectedRoles"
|
||||
:data="allRoles"
|
||||
:titles="['可选角色', '已分配角色']"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="roleDialogVisible = false">
|
||||
取消
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleAssignRolesOk"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { userApi, type User, type CreateUserRequest, type UpdateUserRequest } from '@/api/user.api'
|
||||
import { roleApi, type Role } from '@/api/role.api'
|
||||
import { handleApiError } from '@/utils/errorHandler'
|
||||
import { UserStatus } from '@/constants/status'
|
||||
import { formatDateTime } from '@/utils/dateFormat'
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref<User[]>([])
|
||||
const searchKeyword = ref('')
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const sortInfo = reactive({
|
||||
sortBy: 'id',
|
||||
sortOrder: 'asc' as 'asc' | 'desc'
|
||||
})
|
||||
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const formRef = ref()
|
||||
const formState = reactive<CreateUserRequest & { id?: string; status?: UserStatus }>({
|
||||
username: '',
|
||||
password: '',
|
||||
nickname: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
roles: [],
|
||||
status: UserStatus.ACTIVE
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 3, max: 50, message: '用户名长度在 3 到 50 个字符', trigger: 'blur' },
|
||||
{ pattern: /^[a-zA-Z0-9_-]+$/, message: '用户名只能包含字母、数字、下划线和横线', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 8, max: 20, message: '密码长度在 8 到 20 个字符', trigger: 'blur' },
|
||||
{ pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/, message: '密码必须包含大小写字母和数字', trigger: 'blur' }
|
||||
],
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' },
|
||||
{ max: 100, message: '邮箱长度不能超过100个字符', trigger: 'blur' }
|
||||
],
|
||||
phone: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const roleDialogVisible = ref(false)
|
||||
const selectedRoles = ref<string[]>([])
|
||||
const allRoles = ref<{ key: string; label: string }[]>([])
|
||||
const currentUserId = ref<string | null>(null)
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await userApi.getPage({
|
||||
page: pagination.current - 1,
|
||||
size: pagination.pageSize,
|
||||
sortBy: sortInfo.sortBy,
|
||||
sortOrder: sortInfo.sortOrder,
|
||||
keyword: searchKeyword.value || undefined
|
||||
})
|
||||
dataSource.value = res.content
|
||||
pagination.total = Number(res.totalElements) || 0
|
||||
} catch (error) {
|
||||
handleApiError(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleTableChange = () => {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSizeChange = () => {
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSortChange = ({ prop, order }: any) => {
|
||||
sortInfo.sortBy = prop
|
||||
sortInfo.sortOrder = order === 'ascending' ? 'asc' : 'desc'
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
modalTitle.value = '新增用户'
|
||||
Object.assign(formState, {
|
||||
id: undefined,
|
||||
username: '',
|
||||
password: '',
|
||||
nickname: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
roles: [],
|
||||
status: 'ACTIVE'
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row: User) => {
|
||||
modalTitle.value = '编辑用户'
|
||||
Object.assign(formState, {
|
||||
id: row.id,
|
||||
username: row.username,
|
||||
nickname: row.nickname,
|
||||
email: row.email,
|
||||
phone: row.phone,
|
||||
status: row.status,
|
||||
roles: []
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (row: User) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该用户吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await userApi.delete(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
handleApiError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleModalOk = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
|
||||
if (formState.id) {
|
||||
const updateData: UpdateUserRequest = {
|
||||
nickname: formState.nickname,
|
||||
email: formState.email,
|
||||
phone: formState.phone,
|
||||
status: formState.status
|
||||
}
|
||||
await userApi.update(formState.id, updateData)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
const createData: CreateUserRequest = {
|
||||
username: formState.username,
|
||||
password: formState.password,
|
||||
nickname: formState.nickname,
|
||||
email: formState.email,
|
||||
phone: formState.phone,
|
||||
roles: formState.roles
|
||||
}
|
||||
await userApi.create(createData)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
modalVisible.value = false
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
modalVisible.value = false
|
||||
if (error !== 'cancel') {
|
||||
handleApiError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleModalCancel = () => {
|
||||
modalVisible.value = false
|
||||
}
|
||||
|
||||
const handleAssignRoles = async (row: User) => {
|
||||
currentUserId.value = row.id
|
||||
try {
|
||||
const roles = await roleApi.getAll()
|
||||
allRoles.value = roles.map((role: Role) => ({
|
||||
key: String(role.id),
|
||||
label: role.roleName
|
||||
}))
|
||||
selectedRoles.value = row.roles || []
|
||||
roleDialogVisible.value = true
|
||||
} catch (error) {
|
||||
handleApiError(error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAssignRolesOk = async () => {
|
||||
if (!currentUserId.value) return
|
||||
|
||||
try {
|
||||
await userApi.assignRoles(currentUserId.value, selectedRoles.value)
|
||||
ElMessage.success('角色分配成功')
|
||||
roleDialogVisible.value = false
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
roleDialogVisible.value = false
|
||||
handleApiError(error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.user-management {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.search-section {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,8 +3,9 @@
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"types": ["node"],
|
||||
"types": ["node", "vite/client"],
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
|
||||
Reference in New Issue
Block a user