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

784
COMPONENT_USAGE_GUIDE.md Normal file
View File

@@ -0,0 +1,784 @@
# 资产管理系统 - 组件使用文档
## 目录
1. [批量导入组件](#批量导入组件)
2. [批量导出组件](#批量导出组件)
3. [扫码查询组件](#扫码查询组件)
4. [资产分配组件](#资产分配组件)
5. [维修管理组件](#维修管理组件)
6. [统计报表组件](#统计报表组件)
---
## 1. 批量导入组件
### 组件信息
- **路径**: `src/views/assets/components/BatchImportDialog.vue`
- **名称**: `BatchImportDialog`
- **功能**: 批量导入资产数据
### Props
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| modelValue | boolean | - | 对话框显示状态 |
### Events
| 事件名 | 参数 | 说明 |
|--------|------|------|
| update:modelValue | (value: boolean) | 显示状态变化 |
| success | - | 导入成功触发 |
### 使用示例
```vue
<template>
<el-button @click="handleImport">批量导入</el-button>
<BatchImportDialog
v-model="importVisible"
@success="handleImportSuccess"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import BatchImportDialog from '@/views/assets/components/BatchImportDialog.vue'
import { ElMessage } from 'element-plus'
const importVisible = ref(false)
const handleImport = () => {
importVisible.value = true
}
const handleImportSuccess = () => {
ElMessage.success('导入成功')
// 刷新列表
}
</script>
```
### 功能说明
#### 三步导入流程
**步骤1: 上传文件**
- 支持拖拽上传
- 支持 .xlsx 和 .xls 格式
- 提供模板下载
**步骤2: 数据预览**
- 显示解析后的数据
- 标记错误行(红色背景)
- 显示错误信息
- 统计错误数量
**步骤3: 导入结果**
- 显示导入统计(总数、成功、失败)
- 失败明细列表
- 导出错误日志
- 导入进度条
### 注意事项
- 文件大小限制建议不超过10MB
- 单次导入数量最多1000条
- 必须先下载模板,按模板格式填写
- 错误数据不会导入,需修改后重新导入
---
## 2. 批量导出组件
### 组件信息
- **路径**: `src/views/assets/components/BatchExportDialog.vue`
- **名称**: `BatchExportDialog`
- **功能**: 批量导出资产数据
### Props
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| modelValue | boolean | - | 对话框显示状态 |
### Events
| 事件名 | 参数 | 说明 |
|--------|------|------|
| update:modelValue | (value: boolean) | 显示状态变化 |
### 使用示例
```vue
<template>
<el-button @click="handleExport">批量导出</el-button>
<BatchExportDialog v-model="exportVisible" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import BatchExportDialog from '@/views/assets/components/BatchExportDialog.vue'
const exportVisible = ref(false)
const handleExport = () => {
exportVisible.value = true
}
</script>
```
### 功能说明
#### 导出字段选择
可选择的字段:
- 资产编码assetCode
- 资产名称assetName
- 设备类型deviceTypeName
- 品牌brandName
- 型号modelName
- 序列号serialNumber
- 所属网点orgName
- 位置location
- 状态status
- 采购日期purchaseDate
- 采购价格purchasePrice
- 保修截止warrantyExpireDate
#### 筛选条件
- 设备类型
- 所属网点
- 资产状态
- 关键词搜索
#### 导出格式
- Excel (.xlsx)
- CSV (.csv)
---
## 3. 扫码查询组件
### 组件信息
- **路径**: `src/views/assets/AssetScan.vue`
- **名称**: `AssetScan`
- **功能**: 扫码查询资产
### 主要功能
#### 1. 相机扫码
```typescript
// 启动相机
const startCamera = async () => {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment' }
})
videoRef.value.srcObject = stream
}
// 停止相机
const stopCamera = () => {
const stream = videoRef.value.srcObject
stream.getTracks().forEach(track => track.stop())
}
```
#### 2. 手动输入
```vue
<el-input
v-model="inputCode"
placeholder="请输入资产编码"
@keyup.enter="handleManualSearch"
>
<template #append>
<el-button @click="handleManualSearch">查询</el-button>
</template>
</el-input>
```
#### 3. 扫码历史
- 保存在 localStorage
- 最多保存20条
- 点击历史记录可快速查询
#### 4. 扫码音效
```typescript
// 使用Web Audio API
const playBeep = () => {
const audioContext = new AudioContext()
const oscillator = audioContext.createOscillator()
oscillator.frequency.value = 800
oscillator.start()
setTimeout(() => oscillator.stop(), 100)
}
```
### 使用示例
```vue
<template>
<router-link to="/assets/scan">
<el-button>扫码查询</el-button>
</router-link>
</template>
```
### 注意事项
- 摄像头访问需要HTTPS或localhost
- 需要授予摄像头权限
- 二维码识别需集成 @zxing/library
---
## 4. 资产分配组件
### 4.1 分配单列表
**路径**: `src/views/allocation/AllocationList.vue`
#### 筛选条件
- 单据类型allocation/transfer/recovery/maintenance/scrap
- 审批状态pending/approved/rejected/cancelled
- 执行状态pending/executing/completed
- 关键词(单号/申请人)
#### 操作按钮
- 新建分配单
- 查看详情
- 编辑(草稿状态)
- 删除(草稿状态)
- 提交审批
- 审批(待审批状态)
- 执行(已通过状态)
### 4.2 创建分配单对话框
**路径**: `src/views/allocation/components/CreateAllocationDialog.vue`
#### Props
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| modelValue | boolean | - | 对话框显示状态 |
| orderId | number \| null | null | 分配单ID编辑时传入 |
#### Events
| 事件名 | 参数 | 说明 |
|--------|------|------|
| update:modelValue | (value: boolean) | 显示状态变化 |
| success | - | 操作成功触发 |
#### 表单字段
```typescript
{
orderType: 'allocation', // 单据类型
targetOrganizationId: 1, // 目标机构ID
title: '分配单标题', // 标题
assetIds: [1, 2, 3], // 资产ID列表
remark: '备注信息' // 备注
}
```
### 4.3 资产选择器对话框
**路径**: `src/views/allocation/components/AssetSelectorDialog.vue`
#### Props
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| modelValue | boolean | - | 对话框显示状态 |
| excludeIds | number[] | [] | 排除的资产ID |
#### Events
| 事件名 | 参数 | 说明 |
|--------|------|------|
| update:modelValue | (value: boolean) | 显示状态变化 |
| confirm | (assets: any[]) | 确认选择 |
#### 使用示例
```vue
<template>
<el-button @click="showSelector">选择资产</el-button>
<AssetSelectorDialog
v-model="selectorVisible"
:exclude-ids="selectedIds"
@confirm="handleConfirm"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import AssetSelectorDialog from '@/views/allocation/components/AssetSelectorDialog.vue'
const selectorVisible = ref(false)
const selectedIds = ref<number[]>([])
const showSelector = () => {
selectorVisible.value = true
}
const handleConfirm = (assets: any[]) => {
console.log('已选择:', assets)
selectedIds.value = assets.map(a => a.id)
}
</script>
```
### 4.4 分配单详情对话框
**路径**: `src/views/allocation/components/AllocationDetailDialog.vue`
#### Tabs
1. **基本信息** - 分配单基本信息
2. **资产明细** - 分配的资产列表
3. **审批流程** - 审批历史时间轴
#### 操作功能
- 审批(通过/拒绝)
- 执行(开始/完成)
- 查看审批历史
---
## 5. 维修管理组件
### 5.1 维修管理页面
**路径**: `src/views/assets/MaintenanceManagement.vue`
#### 筛选条件
- 状态(待维修/维修中/已完成/已取消)
- 优先级(低/中/高)
- 关键词(资产名称/编码)
#### 操作按钮
- 新建维修记录
- 查看
- 编辑(待维修状态)
- 开始维修
- 完成维修
- 取消维修
### 5.2 维修记录对话框
**路径**: `src/views/assets/components/MaintenanceDialog.vue`
#### Props
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| modelValue | boolean | - | 对话框显示状态 |
| recordId | number \| null | null | 记录ID编辑时传入 |
| assetId | number \| null | null | 资产ID预选 |
#### Events
| 事件名 | 参数 | 说明 |
|--------|------|------|
| update:modelValue | (value: boolean) | 显示状态变化 |
| success | - | 操作成功触发 |
#### 表单字段
```typescript
{
assetId: 1, // 资产ID
faultType: 'hardware', // 故障类型
priority: 'medium', // 优先级
maintenanceType: 'self_repair', // 维修类型
faultDescription: '...', // 故障描述
maintenancePersonnel: '张三', // 维修人员
maintenanceCost: 500.00, // 维修费用
startDate: '2025-01-24', // 开始日期
endDate: '2025-01-25', // 结束日期
remark: '备注', // 备注
photos: [] // 维修照片
}
```
#### 使用示例
```vue
<template>
<el-button @click="showMaintenance">新建维修记录</el-button>
<MaintenanceDialog
v-model="maintenanceVisible"
:asset-id="currentAssetId"
@success="handleSuccess"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import MaintenanceDialog from '@/views/assets/components/MaintenanceDialog.vue'
const maintenanceVisible = ref(false)
const currentAssetId = ref<number>(1)
const showMaintenance = () => {
maintenanceVisible.value = true
}
const handleSuccess = () => {
console.log('维修记录已保存')
}
</script>
```
---
## 6. 统计报表组件
### 组件信息
- **路径**: `src/views/assets/StatisticsDashboard.vue`
- **名称**: `StatisticsDashboard`
- **功能**: 资产统计和可视化
### 主要功能
#### 1. 统计卡片
```vue
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon total">
<el-icon><Box /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ totalAssets }}</div>
<div class="stat-label">资产总数</div>
</div>
</div>
</el-card>
```
卡片类型:
- 资产总数(紫色)
- 在用资产(绿色)
- 维修中(橙色)
- 待报废(红色)
#### 2. ECharts图表
**图表1: 资产状态分布(饼图)**
```typescript
const statusPieOption = {
series: [{
type: 'pie',
radius: ['40%', '70%'], // 环形
data: [
{ value: 735, name: '在用' },
{ value: 580, name: '在库' },
{ value: 484, name: '维修中' },
{ value: 300, name: '待报废' }
]
}]
}
```
**图表2: 资产类型分布(柱状图)**
```typescript
const typeBarOption = {
xAxis: { data: ['计算机', '打印机', '复印机', ...] },
series: [{
type: 'bar',
data: [326, 208, 156, ...]
}]
}
```
**图表3: 资产价值趋势(折线图)**
```typescript
const valueTrendOption = {
xAxis: { data: ['1月', '2月', '3月', ...] },
yAxis: [
{ type: 'value', name: '数量' },
{ type: 'value', name: '价值(万元)' }
],
series: [
{ name: '资产数量', type: 'line' },
{ name: '资产价值', type: 'line', yAxisIndex: 1 }
]
}
```
**图表4: 机构资产分布(树图)**
```typescript
const orgDistributionOption = {
series: [{
type: 'tree',
data: [
{
name: '广东省',
children: [
{ name: '广州市', children: [...] },
{ name: '深圳市', children: [...] }
]
}
]
}]
}
```
**图表5: 维修统计(堆叠柱状图)**
```typescript
const maintenanceOption = {
series: [
{ name: '硬件故障', type: 'bar', stack: 'total' },
{ name: '软件故障', type: 'bar', stack: 'total' },
{ name: '其他', type: 'bar', stack: 'total' }
]
}
```
### 使用示例
```vue
<template>
<router-link to="/assets/statistics">
<el-button>统计报表</el-button>
</router-link>
</template>
```
### ECharts按需引入
```typescript
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { PieChart, BarChart, LineChart, TreeChart } from 'echarts/charts'
import {
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent
} from 'echarts/components'
use([
CanvasRenderer,
PieChart,
BarChart,
LineChart,
TreeChart,
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent
])
```
---
## 通用组件模式
### 对话框组件模式
所有对话框组件遵循统一的模式:
```vue
<template>
<el-dialog
v-model="visible"
:title="title"
width="800px"
:close-on-click-modal="false"
@close="handleClose"
>
<!-- 内容 -->
<el-form ref="formRef" :model="formData" :rules="formRules">
<!-- 表单字段 -->
</el-form>
<!-- 底部按钮 -->
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
interface Props {
modelValue: boolean
}
interface Emits {
(e: 'update:modelValue', value: boolean): void
(e: 'success'): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const visible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const handleClose = () => {
visible.value = false
}
</script>
```
### 表单验证模式
```typescript
const formRules = {
fieldName: [
{ required: true, message: '请输入', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在2-50个字符', trigger: 'blur' }
]
}
const handleSubmit = async () => {
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return
// 提交逻辑
}
```
### API调用模式
```typescript
const fetchData = async () => {
loading.value = true
try {
const data = await apiFunction(params)
// 处理数据
} catch (error) {
ElMessage.error('操作失败')
} finally {
loading.value = false
}
}
```
---
## 样式规范
### SCSS变量
```scss
// 主题色
$primary-color: #409EFF;
$success-color: #67C23A;
$warning-color: #E6A23C;
$danger-color: #F56C6C;
$info-color: #909399;
// 文本色
$text-primary: #303133;
$text-regular: #606266;
$text-secondary: #909399;
// 边框色
$border-base: #DCDFE6;
$border-light: #E4E7ED;
$border-lighter: #EBEEF5;
$border-extra-light: #F2F6FC;
// 背景色
$bg-color: #F5F7FA;
```
### 响应式断点
```scss
// 屏幕断点
$sm: 768px;
$md: 992px;
$lg: 1200px;
$xl: 1920px;
@media (max-width: $sm) {
// 小屏幕样式
}
```
---
## 常见问题
### Q: 如何自定义表单验证?
```typescript
const customValidator = (rule: any, value: any, callback: any) => {
if (!value) {
callback(new Error('不能为空'))
} else if (value.length < 6) {
callback(new Error('长度不能少于6位'))
} else {
callback()
}
}
const formRules = {
password: [
{ validator: customValidator, trigger: 'blur' }
]
}
```
### Q: 如何处理文件上传?
```vue
<el-upload
action="/api/upload"
:on-success="handleSuccess"
:on-error="handleError"
:before-upload="beforeUpload"
>
<el-button>上传文件</el-button>
</el-upload>
```
### Q: 如何实现分页?
```typescript
import { usePagination } from '@/composables/usePagination'
const { pagination, resetPage, setTotal } = usePagination()
const fetchData = async () => {
const data = await apiFunction({
page: pagination.page,
page_size: pagination.pageSize
})
setTotal(data.total)
}
```
---
## 最佳实践
### 1. 组件命名
- 使用大驼峰命名
- 文件名与组件名一致
- 对话框以Dialog结尾
### 2. Props定义
- 使用TypeScript接口
- 提供默认值
- 添加注释说明
### 3. 事件命名
- 使用kebab-case
- 事件名语义明确
- 参数类型明确
### 4. 样式编写
- 使用scoped避免污染
- 使用SCSS变量
- 遵循BEM命名
### 5. 性能优化
- 合理使用computed
- 避免不必要的watch
- 按需引入组件
---
**更新时间**: 2025-01-24
**版本**: v1.0.0