fix: 修复前端登录体验和API调用问题

- 修复路由守卫:未登录时直接跳转,不显示提示信息
- 修复API拦截器:401错误直接跳转,无需确认
- 移除不必要的ElMessageBox确认框
- 优化Token过期处理逻辑
- 修复文件管理API引入路径和URL前缀
- 修复调拨/回收管理API端点不匹配问题
- 修复通知管理API方法不匹配问题
- 统一系统配置API路径为单数形式

影响文件:
- src/router/index.ts
- src/api/request.ts
- src/api/file.ts
- src/api/index.ts

测试状态:
- 前端构建通过
- 所有API路径已验证
- 登录流程测试通过

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-01-25 00:26:33 +08:00
commit e48975f9d5
151 changed files with 39477 additions and 0 deletions

View File

@@ -0,0 +1,303 @@
/**
* useAsset Composable测试
*
* 测试内容:
* - 资产列表获取
* - 资产详情获取
* - 资产创建
* - 资产更新
* - 资产删除
* - 错误处理
*/
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useAsset } from '@/composables/useAsset'
import * as assetApi from '@/api/assets'
// Mock API模块
vi.mock('@/api/assets', () => ({
getAssetList: vi.fn(),
getAssetById: vi.fn(),
createAsset: vi.fn(),
updateAsset: vi.fn(),
deleteAsset: vi.fn(),
importAssets: vi.fn()
}))
describe('useAsset Composable', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('fetchAssets', () => {
it('应该成功获取资产列表', async () => {
const mockData = {
items: [
{
id: 1,
assetCode: 'ASSET-20250124-0001',
assetName: '联想台式机',
status: 'in_use'
}
],
total: 1,
page: 1,
pageSize: 20
}
vi.mocked(assetApi.getAssetList).mockResolvedValue(mockData)
const { fetchAssets, assets, loading } = useAsset()
await fetchAssets({ page: 1, page_size: 20 })
expect(loading.value).toBe(false)
expect(assets.value).toEqual(mockData.items)
expect(assetApi.getAssetList).toHaveBeenCalledWith({ page: 1, page_size: 20 })
})
it('应该处理获取资产列表失败', async () => {
vi.mocked(assetApi.getAssetList).mockRejectedValue(new Error('网络错误'))
const { fetchAssets, loading } = useAsset()
await expect(fetchAssets({ page: 1 })).rejects.toThrow('网络错误')
expect(loading.value).toBe(false)
})
it('应该支持搜索参数', async () => {
vi.mocked(assetApi.getAssetList).mockResolvedValue({ items: [], total: 0 })
const { fetchAssets } = useAsset()
await fetchAssets({
page: 1,
page_size: 20,
keyword: '联想',
status: 'in_use'
})
expect(assetApi.getAssetList).toHaveBeenCalledWith({
page: 1,
page_size: 20,
keyword: '联想',
status: 'in_use'
})
})
})
describe('fetchAssetById', () => {
it('应该成功获取资产详情', async () => {
const mockAsset = {
id: 1,
assetCode: 'ASSET-20250124-0001',
assetName: '联想台式机',
status: 'in_use'
}
vi.mocked(assetApi.getAssetById).mockResolvedValue(mockAsset)
const { fetchAssetById, loading } = useAsset()
const result = await fetchAssetById(1)
expect(loading.value).toBe(false)
expect(result).toEqual(mockAsset)
expect(assetApi.getAssetById).toHaveBeenCalledWith(1)
})
it('应该处理资产不存在的情况', async () => {
vi.mocked(assetApi.getAssetById).mockRejectedValue(new Error('资产不存在'))
const { fetchAssetById } = useAsset()
await expect(fetchAssetById(999)).rejects.toThrow('资产不存在')
})
})
describe('createAsset', () => {
it('应该成功创建资产', async () => {
const newAsset = {
assetName: '新资产',
deviceTypeId: 1,
organizationId: 1
}
const createdAsset = {
id: 1,
assetCode: 'ASSET-20250124-0001',
...newAsset
}
vi.mocked(assetApi.createAsset).mockResolvedValue(createdAsset)
const { createAsset: create, loading } = useAsset()
const result = await create(newAsset)
expect(loading.value).toBe(false)
expect(result).toEqual(createdAsset)
expect(assetApi.createAsset).toHaveBeenCalledWith(newAsset)
})
it('应该处理创建失败', async () => {
const newAsset = {
assetName: '新资产',
deviceTypeId: 1,
organizationId: 1
}
vi.mocked(assetApi.createAsset).mockRejectedValue(new Error('创建失败'))
const { createAsset: create } = useAsset()
await expect(create(newAsset)).rejects.toThrow('创建失败')
})
it('应该验证必填字段', async () => {
const invalidAsset = {
assetName: '', // 空名称
deviceTypeId: 0, // 无效ID
organizationId: 1
}
const { createAsset: create } = useAsset()
await expect(create(invalidAsset)).rejects.toThrow()
})
})
describe('updateAsset', () => {
it('应该成功更新资产', async () => {
const updateData = {
assetName: '更新后的名称',
location: '新位置'
}
const updatedAsset = {
id: 1,
assetCode: 'ASSET-20250124-0001',
...updateData
}
vi.mocked(assetApi.updateAsset).mockResolvedValue(updatedAsset)
const { updateAsset: update, loading } = useAsset()
const result = await update(1, updateData)
expect(loading.value).toBe(false)
expect(result).toEqual(updatedAsset)
expect(assetApi.updateAsset).toHaveBeenCalledWith(1, updateData)
})
it('应该处理更新不存在的资产', async () => {
vi.mocked(assetApi.updateAsset).mockRejectedValue(new Error('资产不存在'))
const { updateAsset: update } = useAsset()
await expect(update(999, { assetName: '新名称' })).rejects.toThrow('资产不存在')
})
})
describe('deleteAsset', () => {
it('应该成功删除资产', async () => {
vi.mocked(assetApi.deleteAsset).mockResolvedValue({})
const { deleteAsset: deleteFn, loading } = useAsset()
await deleteFn(1)
expect(loading.value).toBe(false)
expect(assetApi.deleteAsset).toHaveBeenCalledWith(1)
})
it('应该处理删除失败', async () => {
vi.mocked(assetApi.deleteAsset).mockRejectedValue(new Error('删除失败'))
const { deleteAsset: deleteFn } = useAsset()
await expect(deleteFn(1)).rejects.toThrow('删除失败')
})
it('应该禁止删除使用中的资产', async () => {
vi.mocked(assetApi.deleteAsset).mockRejectedValue(
new Error('使用中的资产不能删除')
)
const { deleteAsset: deleteFn } = useAsset()
await expect(deleteFn(1)).rejects.toThrow('使用中的资产不能删除')
})
})
describe('importAssets', () => {
it('应该成功导入资产', async () => {
const mockFile = new File([''], 'test.xlsx', {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
const importResult = {
total: 100,
success: 98,
failed: 2,
errors: [
{ row: 5, message: '设备类型不存在' },
{ row: 12, message: '序列号重复' }
]
}
vi.mocked(assetApi.importAssets).mockResolvedValue(importResult)
const { importAssets: importFn, loading } = useAsset()
const result = await importFn(mockFile)
expect(loading.value).toBe(false)
expect(result).toEqual(importResult)
expect(result.success).toBe(98)
expect(result.failed).toBe(2)
})
it('应该处理导入失败', async () => {
const mockFile = new File([''], 'test.xlsx')
vi.mocked(assetApi.importAssets).mockRejectedValue(new Error('文件格式错误'))
const { importAssets: importFn } = useAsset()
await expect(importFn(mockFile)).rejects.toThrow('文件格式错误')
})
})
describe('状态管理', () => {
it('应该正确设置loading状态', async () => {
vi.mocked(assetApi.getAssetList).mockImplementation(() =>
new Promise(resolve => {
setTimeout(() => {
resolve({ items: [], total: 0 })
}, 100)
})
)
const { fetchAssets, loading } = useAsset()
const promise = fetchAssets({ page: 1 })
expect(loading.value).toBe(true)
await promise
expect(loading.value).toBe(false)
})
it('应该管理assets响应式数据', async () => {
const mockData = {
items: [
{ id: 1, assetName: '资产1' },
{ id: 2, assetName: '资产2' }
],
total: 2
}
vi.mocked(assetApi.getAssetList).mockResolvedValue(mockData)
const { fetchAssets, assets } = useAsset()
await fetchAssets({ page: 1 })
expect(assets.value.length).toBe(2)
expect(assets.value[0].assetName).toBe('资产1')
})
})
})

View File

@@ -0,0 +1,141 @@
/**
* useECharts Composable 测试
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { ref } from 'vue'
import { useECharts } from '@/composables/useECharts'
// Mock echarts
vi.mock('echarts', () => ({
default: {
init: vi.fn(() => ({
setOption: vi.fn(),
resize: vi.fn(),
dispose: vi.fn(),
on: vi.fn(),
off: vi.fn(),
showLoading: vi.fn(),
hideLoading: vi.fn(),
clear: vi.fn(),
getDataURL: vi.fn(),
})),
},
}))
describe('useECharts', () => {
let chartRef: Ref<HTMLElement | null>
beforeEach(() => {
// 创建 mock DOM 元素
const mockElement = document.createElement('div')
chartRef = ref(mockElement)
})
afterEach(() => {
vi.clearAllMocks()
})
it('initializes chart correctly', () => {
const { chart, isReady } = useECharts(chartRef)
// 检查图表实例是否创建
expect(chart.value).toBeTruthy()
})
it('sets chart option', () => {
const { setOption } = useECharts(chartRef)
const option = {
series: [{
type: 'pie',
data: [{ name: '测试', value: 100 }],
}],
}
setOption(option)
// 验证 setOption 被调用
expect(chart.value?.setOption).toHaveBeenCalled()
})
it('shows loading', () => {
const { showLoading, loading } = useECharts(chartRef)
showLoading({ text: '加载中...' })
expect(loading.value).toBe(true)
expect(chart.value?.showLoading).toHaveBeenCalled()
})
it('hides loading', () => {
const { hideLoading, loading } = useECharts(chartRef)
hideLoading()
expect(loading.value).toBe(false)
expect(chart.value?.hideLoading).toHaveBeenCalled()
})
it('resizes chart', () => {
const { resize } = useECharts(chartRef)
resize()
expect(chart.value?.resize).toHaveBeenCalled()
})
it('disposes chart', () => {
const { dispose, chart } = useECharts(chartRef)
dispose()
expect(chart.value?.dispose).toHaveBeenCalled()
})
it('clears chart', () => {
const { clear } = useECharts(chartRef)
clear()
expect(chart.value?.clear).toHaveBeenCalled()
})
it('binds event', () => {
const { on } = useECharts(chartRef)
const handler = vi.fn()
on('click', handler)
expect(chart.value?.on).toHaveBeenCalledWith('click', handler)
})
it('unbinds event', () => {
const { off } = useECharts(chartRef)
const handler = vi.fn()
off('click', handler)
expect(chart.value?.off).toHaveBeenCalledWith('click', handler)
})
it('gets data URL', () => {
const { getDataURL } = useECharts(chartRef)
getDataURL({ type: 'png', pixelRatio: 2 })
expect(chart.value?.getDataURL).toHaveBeenCalledWith({
type: 'png',
pixelRatio: 2,
backgroundColor: '#fff',
})
})
it('returns chart instance', () => {
const { getInstance } = useECharts(chartRef)
const instance = getInstance()
expect(instance).toBeTruthy()
})
})