基本完成v1.0

This commit is contained in:
xuqiuyun
2025-10-30 16:58:39 +08:00
parent d1d0b62184
commit 4b6d14a6ec
202 changed files with 1856 additions and 17458 deletions

View File

@@ -1,130 +0,0 @@
# 牛只运输管理系统架构文档
## 1. 系统概述
牛只运输管理系统是一个基于 Vue 3 + TypeScript 开发的现代化前端项目,旨在提供完整的牛只运输管理解决方案。系统集成了运输管理、检疫隔离、设备监控、预警系统等多个功能模块,为牛只运输过程提供全方位的管理和监控支持。
## 2. 技术架构
### 2.1 前端技术栈
- **核心框架**: Vue 3 + TypeScript
- **构建工具**: Vite
- **状态管理**: Pinia
- **路由管理**: Vue Router
- **UI 组件库**: Element Plus
- **HTTP 客户端**: Axios
- **地图集成**: 百度地图vue-baidu-map-3x
- **图表库**: ECharts
- **富文本编辑器**: WangEditor
- **其他工具**:
- 视频播放: @liveqing/liveplayer-v3
- 文件处理: file-saver
- 二维码生成: qrcode
- 打印功能: vue3-print-nb
### 2.2 架构模式
- 前后端分离架构
- 单页应用SPA
- 模块化开发
- 组件化设计
### 2.3 设计模式
- 组合式 APIVue 3 Composition API
- 状态管理使用 Pinia模块化 Store
- 路由懒加载Vue Router
- 自定义指令(权限控制、复制文本等)
## 3. 项目结构
```
src/
├── api/ # API 接口定义
│ ├── common/ # 通用 API
│ ├── abroad.js # 出境管理
│ ├── device.js # 设备管理
│ ├── hardware.js # 硬件相关
│ ├── isolationQuarantine.js # 隔离检疫
│ ├── killRecord.js # 屠宰记录
│ ├── quarantine.js # 检疫管理
│ ├── shipping.js # 运输管理
│ ├── sys.js # 系统管理
│ └── userManage.js # 用户管理
├── assets/ # 静态资源
├── components/ # 公共组件
├── directive/ # 自定义指令
├── plugins/ # 插件配置
├── router/ # 路由配置
├── store/ # Pinia 状态管理
├── styles/ # 全局样式
├── utils/ # 工具函数
└── views/ # 页面组件
├── earlywarning/ # 预警系统
├── entry/ # 数据录入
├── hardware/ # 硬件管理
├── shipping/ # 运输管理
├── system/ # 系统设置
└── userManage/ # 用户管理
```
## 4. 核心组件交互
### 4.1 页面组件views
页面组件通过路由加载,每个功能模块都有对应的页面组件,负责展示该模块的用户界面和处理用户交互。
### 4.2 公共组件components
公共组件被多个页面共享使用,包括布局组件、表单组件、表格组件等。
### 4.3 状态管理store
使用 Pinia 进行全局状态管理,包括用户信息、权限信息、路由信息等。
### 4.4 API 接口调用
统一通过 [src/api](src/api) 目录下的接口函数调用后端服务,使用 Axios 进行 HTTP 请求。
### 4.5 自定义指令
通过自定义指令处理 DOM 操作和权限控制等功能。
## 5. 路由架构
系统采用 Vue Router 进行路由管理,分为静态路由和动态路由:
- **静态路由**: 包含登录页、首页等基础页面
- **动态路由**: 根据用户权限从后端获取并动态添加的路由
## 6. 状态管理架构
使用 Pinia 进行状态管理,包含以下 Store 模块:
- **user**: 管理用户登录信息和身份认证
- **permission**: 管理用户权限和路由信息
## 7. 权限控制架构
系统通过自定义指令和路由守卫实现权限控制:
- **路由级权限**: 通过动态路由控制用户可访问的页面
- **按钮级权限**: 通过自定义指令控制用户可操作的按钮
## 8. 数据流架构
```
View(UI) -> Store(State) -> API(Service) -> Backend
^ |
| v
| Database
| |
--------------------------------------------
```
## 9. 构建和部署架构
- 使用 Vite 进行项目构建
- 支持开发环境和生产环境构建
- 构建产物可部署到任意 Web 服务器

View File

@@ -1,155 +0,0 @@
# 装车信息表单自动填充功能实现
## 功能概述
已成功实现装车信息表单的自动填充功能可以根据API返回的数据自动映射并填充表单字段。
## 实现的功能
### 1. 数据映射字段
根据提供的API响应数据实现了以下字段的自动映射
#### 基础信息
- `deliveryId``id`
- `estimatedDeliveryTime``estimatedDeliveryTime`
- `serverDeviceSn``serverDeviceId`
#### 重量信息
- `emptyWeight``emptyWeight`
- `entruckWeight``entruckWeight`
- `landingEntruckWeight``landingEntruckWeight`
#### 照片URL
- `quarantineTickeyUrl``quarantineTickeyUrl`
- `poundListImg``poundListImg`
- `emptyVehicleFrontPhoto``emptyVehicleFrontPhoto`
- `loadedVehicleFrontPhoto``loadedVehicleFrontPhoto`
- `loadedVehicleWeightPhoto``loadedVehicleWeightPhoto`
- `driverIdCardPhoto``driverIdCardPhoto`
#### 视频URL
- `entruckWeightVideo``entruckWeightVideo`
- `emptyWeightVideo``emptyWeightVideo`
- `entruckVideo``entruckVideo`
- `controlSlotVideo``controlSlotVideo`
- `cattleLoadingCircleVideo``cattleLoadingCircleVideo`
### 2. 核心实现
#### 自动填充函数
```javascript
const autoFillFormData = (apiData) => {
if (!apiData) return;
// 基础信息映射
ruleForm.deliveryId = apiData.id || '';
ruleForm.estimatedDeliveryTime = apiData.estimatedDeliveryTime || '';
ruleForm.serverDeviceSn = apiData.serverDeviceId || '';
// 重量信息映射
ruleForm.emptyWeight = apiData.emptyWeight || '';
ruleForm.entruckWeight = apiData.entruckWeight || '';
ruleForm.landingEntruckWeight = apiData.landingEntruckWeight || '';
// 照片URL映射
ruleForm.quarantineTickeyUrl = apiData.quarantineTickeyUrl || '';
ruleForm.poundListImg = apiData.poundListImg || '';
ruleForm.emptyVehicleFrontPhoto = apiData.emptyVehicleFrontPhoto || '';
ruleForm.loadedVehicleFrontPhoto = apiData.loadedVehicleFrontPhoto || '';
ruleForm.loadedVehicleWeightPhoto = apiData.loadedVehicleWeightPhoto || '';
ruleForm.driverIdCardPhoto = apiData.driverIdCardPhoto || '';
// 视频URL映射
ruleForm.entruckWeightVideo = apiData.entruckWeightVideo || '';
ruleForm.emptyWeightVideo = apiData.emptyWeightVideo || '';
ruleForm.entruckVideo = apiData.entruckVideo || '';
ruleForm.controlSlotVideo = apiData.controlSlotVideo || '';
ruleForm.cattleLoadingCircleVideo = apiData.cattleLoadingCircleVideo || '';
console.log('表单数据已自动填充:', ruleForm);
};
```
#### 更新后的对话框调用函数
```javascript
const onShowDialog = (row, apiData = null) => {
data.dialogVisible = true;
if (formDataRef.value) {
formDataRef.value.resetFields();
}
if (row) {
nextTick(() => {
data.deliveryId = row.id;
ruleForm.deliveryId = row.id;
// 如果提供了API数据直接填充表单
if (apiData) {
autoFillFormData(apiData);
} else {
// 否则从服务器获取详情
getOrderDetail();
}
getHostList();
});
}
};
```
## 使用方法
### 方法1直接传递API数据
```javascript
// 在调用装车对话框时直接传递API响应数据
const loadClick = (row, apiData) => {
if (LoadDialogRef.value) {
LoadDialogRef.value.onShowDialog(row, apiData);
}
};
```
### 方法2在API响应中自动填充
当调用 `getOrderDetail()` 函数时,会自动调用 `autoFillFormData(res.data)` 来填充表单。
## 示例数据映射
基于提供的API响应数据
```javascript
{
id: 85,
deliveryNumber: "ZC20251020105111",
deliveryTitle: "1",
estimatedDeliveryTime: "2025-10-31 00:00:00",
emptyWeight: "1000.00",
entruckWeight: "2000.00",
quarantineTickeyUrl: "https://smart-1251449951.cos.ap-guangzhou.myqcloud.com/iotPlateform/2025/10/21/4c4e20251021100838.jpg",
poundListImg: "https://smart-1251449951.cos.ap-guangzhou.myqcloud.com/iotPlateform/2025/10/21/cows20251021100841.jpg",
emptyVehicleFrontPhoto: "https://smart-1251449951.cos.ap-guangzhou.myqcloud.com/iotPlateform/2025/10/21/4c4e20251021100847.jpg",
loadedVehicleFrontPhoto: "https://smart-1251449951.cos.ap-guangzhou.myqcloud.com/iotPlateform/2025/10/21/cows20251021100849.jpg",
loadedVehicleWeightPhoto: "https://smart-1251449951.cos.ap-guangzhou.myqcloud.com/iotPlateform/2025/10/21/4c4e20251021100854.jpg",
driverIdCardPhoto: "https://smart-1251449951.cos.ap-guangzhou.myqcloud.com/iotPlateform/2025/10/21/cows20251021100857.jpg",
entruckWeightVideo: "https://smart-1251449951.cos.ap-guangzhou.myqcloud.com/iotPlateform/2025/10/21/normal_video20251021100901.mp4",
emptyWeightVideo: "https://smart-1251449951.cos.ap-guangzhou.myqcloud.com/iotPlateform/2025/10/21/normal_video20251021100904.mp4",
entruckVideo: "https://smart-1251449951.cos.ap-guangzhou.myqcloud.com/iotPlateform/2025/10/21/normal_video20251021101046.mp4",
controlSlotVideo: "https://smart-1251449951.cos.ap-guangzhou.myqcloud.com/iotPlateform/2025/10/21/normal_video20251021101049.mp4",
cattleLoadingCircleVideo: "https://smart-1251449951.cos.ap-guangzhou.myqcloud.com/iotPlateform/2025/10/21/normal_video20251021101052.mp4"
}
```
这些数据会自动映射到表单的相应字段中。
## 注意事项
1. **数据安全**:所有字段都使用了 `|| ''` 来确保空值安全
2. **向后兼容**:保持了原有的 `getOrderDetail()` 功能
3. **调试支持**:添加了 `console.log` 来帮助调试数据填充过程
4. **响应式更新**:使用 Vue 3 的 reactive 系统确保数据变化时UI自动更新
## 文件修改
- `pc-cattle-transportation/src/views/shipping/loadDialog.vue`
- 添加了 `landingEntruckWeight` 字段
- 实现了 `autoFillFormData` 函数
- 更新了 `onShowDialog` 函数支持API数据参数
-`getOrderDetail` 中集成了自动填充功能

View File

@@ -1,93 +0,0 @@
# 字段映射问题完整解决方案
## 📊 问题理解
根据您的说明,数据结构关系如下:
- `delivery` 表中的 `supplier_id``fund_id``buyer_id` 字段
- 对应 `member_user` 表中的 `member_id` 字段
- 需要获取 `member_user` 表中的 `username` 字段作为姓名
## 🔧 已实施的解决方案
### 1. 后端改进
- ✅ 修改了 `DeliveryServiceImpl.pageQuery` 方法
- ✅ 添加了 `MemberMapper.selectMemberUserById` 方法
- ✅ 实现了 `member` 表和 `member_user` 表的关联查询
- ✅ 添加了详细的调试日志
- ✅ 实现了用户名优先,手机号备选的逻辑
### 2. 前端回退机制
- ✅ 实现了前端的数据回退机制
- ✅ 确保即使后端查询失败,也能显示手机号
## 🧪 测试步骤
### 1. 重启后端服务
```bash
cd tradeCattle/aiotagro-cattle-trade
mvn spring-boot:run
```
### 2. 检查后端日志
查看控制台输出,应该看到类似这样的日志:
```
供应商查询结果 - ID: 61, 结果: {id=61, mobile=16666666666, username=测试供应商1}
供应商 - ID: 61, Username: 测试供应商1, Mobile: 16666666666
资金方查询结果 - ID: 63, 结果: {id=63, mobile=17777777771, username=测试资金方1}
资金方 - ID: 63, Username: 测试资金方1, Mobile: 17777777771
采购商查询结果 - ID: 62, 结果: {id=62, mobile=17777777777, username=测试采购方1}
采购商 - ID: 62, Username: 测试采购方1, Mobile: 17777777777
```
### 3. 测试前端功能
1. 刷新入境检疫页面
2. 查看控制台"原始数据字段检查"日志
3. 点击"下载文件"按钮测试导出功能
## 🎯 预期结果
### 如果 `member_user` 表中有用户名:
- `supplierName`: "测试供应商1"
- `buyerName`: "测试采购方1"
- `fundName`: "测试资金方1"
### 如果 `member_user` 表中用户名为空:
- `supplierName`: "16666666666" (回退到手机号)
- `buyerName`: "17777777777" (回退到手机号)
- `fundName`: "17777777771" (回退到手机号)
## 🔍 可能的问题原因
1. **数据库表结构**`member_user` 表中可能没有对应的记录
2. **数据问题**ID 61, 62, 63 在 `member_user` 表中可能不存在或 `username` 字段为空
3. **查询逻辑**SQL查询可能有问题
## 📋 数据库检查
如果需要检查数据库可以执行以下SQL
```sql
SELECT m.id, m.mobile, mu.username
FROM member m
LEFT JOIN member_user mu ON m.id = mu.member_id
WHERE m.id IN (61, 62, 63);
```
## ✅ 当前解决方案的优势
- **容错性强**:即使后端查询失败,也能显示手机号
- **用户体验好**:不会出现空白字段
- **调试友好**:有详细的日志输出
- **向后兼容**:不影响现有功能
- **数据完整性**确保Word导出文档中不会出现空白字段
## 🚀 下一步
1. 重启后端服务
2. 测试API响应
3. 检查后端日志
4. 测试Word导出功能
5. 验证字段映射是否正确
现在您可以测试功能了!后端会正确查询 `member_user` 表获取用户名,如果用户名为空则使用手机号作为备选。

View File

@@ -1,256 +0,0 @@
# 牛只运输管理系统数据结构说明
## 1. 概述
本文档详细描述了牛只运输管理系统中使用的主要数据结构包括API接口返回数据结构、状态管理数据结构等。
## 2. API接口数据结构
### 2.1 用户相关接口
#### 登录接口 `/login`
**请求参数:**
```typescript
interface LoginRequest {
mobile: string; // 手机号
password: string; // 密码或验证码
loginType: number; // 登录类型(0:密码登录, 1:验证码登录)
}
```
**响应数据:**
```typescript
interface LoginResponse {
token: string; // 认证令牌
mobile: string; // 手机号
username: string; // 用户名
userType: number; // 用户类型
roleId: string; // 角色ID
}
```
#### 获取用户菜单接口 `/getUserMenus`
**响应数据:**
```typescript
interface Menu {
id: string; // 菜单ID
parentId: string; // 父级菜单ID
name: string; // 菜单名称
routeUrl: string; // 路由URL
pageUrl: string; // 页面URL
icon: string; // 菜单图标
type: number; // 菜单类型(1:菜单, 2:按钮)
authority: string; // 权限标识
}
```
### 2.2 系统管理接口
#### 岗位管理列表接口 `/sysRole/queryList`
**请求参数:**
```typescript
interface PositionListRequest {
pageNum: number; // 页码
pageSize: number; // 每页条数
name?: string; // 岗位名称(可选)
}
```
**响应数据:**
```typescript
interface PositionListResponse {
total: number; // 总条数
rows: Position[]; // 岗位列表
}
interface Position {
id: string; // 岗位ID
name: string; // 岗位名称
description: string; // 岗位描述
createTime: string; // 创建时间
isAdmin: number; // 是否管理员岗位(1:是, 其他:否)
}
```
#### 岗位删除接口 `/sysRole/delete`
**请求参数:**
```typescript
interface PositionDeleteRequest {
id: string; // 岗位ID
}
```
### 2.3 运输管理接口
#### 运输计划相关接口
(具体接口结构需根据实际API文档补充)
### 2.4 检疫管理接口
#### 检疫记录相关接口
(具体接口结构需根据实际API文档补充)
## 3. 状态管理数据结构
### 3.1 用户状态 (userStore)
```typescript
interface UserState {
id: string; // 用户ID
username: string; // 用户名
token: string; // 认证令牌
loginUser: any; // 登录用户信息
userType: string; // 用户类型
roleId: string; // 角色ID
}
```
### 3.2 权限状态 (permissionStore)
```typescript
interface PermissionState {
routes: Route[]; // 所有路由
addRoutes: Route[]; // 动态添加的路由
sidebarRouters: Route[]; // 侧边栏路由
routeFlag: boolean; // 路由加载标志
userPermission: string[]; // 用户权限列表
}
```
## 4. 路由数据结构
### 4.1 路由配置结构
```typescript
interface RouteConfig {
path: string; // 路由路径
name?: string; // 路由名称
component: Component; // 组件
redirect?: string; // 重定向路径
meta: {
title: string; // 页面标题
keepAlive: boolean; // 是否缓存
requireAuth: boolean; // 是否需要认证
};
children?: RouteConfig[]; // 子路由
}
```
## 5. 组件数据结构
### 5.1 表单配置结构
```typescript
interface FormItem {
label: string; // 标签文本
type: string; // 表单类型(input, select, date等)
param: string; // 参数名
placeholder?: string; // 占位符
span?: number; // 栅格占据的列数
labelWidth?: number; // 标签宽度
// 根据类型不同可能包含以下属性
selectOptions?: Option[]; // 下拉选项(仅select类型)
multiple?: boolean; // 是否多选(仅select类型)
labelKey?: string; // 标签键名(仅select类型)
valueKey?: string; // 值键名(仅select类型)
}
```
### 5.2 表格配置结构
```typescript
interface TableColumn {
label: string; // 列标题
prop: string; // 对应字段名
width?: number; // 列宽
showOverflowTooltip?: boolean;// 是否显示溢出提示
}
```
## 6. 工具函数数据结构
### 6.1 树形结构处理
系统中多处使用树形结构数据,如菜单树、组织架构树等,统一采用以下结构:
```typescript
interface TreeNode {
id: string; // 节点ID
parentId: string; // 父节点ID
name: string; // 节点名称
children?: TreeNode[]; // 子节点
}
```
## 7. 配置数据结构
### 7.1 环境配置
系统支持多种环境配置,通过环境变量文件进行管理:
- `.env`: 基础配置
- `.env.development`: 开发环境配置
- `.env.production`: 生产环境配置
### 7.2 主题配置
系统支持主题配置,包括颜色、字体、间距等样式变量。
## 8. 数据存储结构
### 8.1 localStorage存储
```javascript
// 用户信息存储
'userStore': {
id: string,
username: string,
token: string,
loginUser: object,
userType: string,
roleId: string
}
```
### 8.2 sessionStorage存储
(根据实际使用情况补充)
## 9. 数据交互流程
### 9.1 登录流程数据交互
```
1. 用户输入登录信息
2. 调用/login接口
3. 服务端验证并返回token等信息
4. 前端存储用户信息到store和localStorage
5. 调用/getUserMenus接口获取菜单信息
6. 根据菜单信息动态生成路由
7. 跳转到首页
```
### 9.2 页面访问流程
```
1. 用户访问某个页面URL
2. 路由守卫检查用户是否登录
3. 如未登录,跳转到登录页
4. 如已登录,检查用户是否有该页面权限
5. 如有权限,加载对应组件
6. 如无权限,显示无权限提示
```
## 10. 数据安全
### 10.1 敏感信息处理
- 密码等敏感信息在传输过程中进行加密处理
- token等认证信息通过HTTP Only Cookie或localStorage存储
- 关键操作需要二次确认
### 10.2 数据验证
- 前端对用户输入进行基础验证
- 后端对接口参数进行严格验证
- 防止SQL注入、XSS等安全问题

View File

@@ -1,191 +0,0 @@
# 用户权限管理系统部署指南
## 当前状态
**前端已完成**:用户权限管理界面已实现,支持优雅的错误处理
**后端待部署**:需要完成数据库表创建和后端服务重启
## 部署步骤
### 步骤1创建数据库表
**执行SQL脚本**
```sql
-- 在数据库中执行以下SQL
source C:\cattleTransport\tradeCattle\add_user_menu_table.sql;
```
**或者手动执行:**
```sql
CREATE TABLE IF NOT EXISTS `sys_user_menu` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` int(11) NOT NULL COMMENT '用户ID',
`menu_id` int(11) NOT NULL COMMENT '菜单ID',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_menu_id` (`menu_id`),
UNIQUE KEY `uk_user_menu` (`user_id`, `menu_id`) COMMENT '用户菜单唯一索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户菜单权限表';
```
### 步骤2重启后端服务
**停止当前服务:**
```bash
# 查找Java进程
jps -l
# 停止Spring Boot应用替换为实际的进程ID
taskkill /F /PID <进程ID>
```
**重新启动服务:**
```bash
cd C:\cattleTransport\tradeCattle\aiotagro-cattle-trade
mvn spring-boot:run
```
**或者使用IDE启动**
- 在IDE中运行 `AiotagroCattletradeApplication.java`
- 确保端口8080可用
### 步骤3验证部署
**检查服务状态:**
```bash
# 检查端口是否监听
netstat -ano | findstr :8080
# 测试API接口
curl http://localhost:8080/api/sysUserMenu/hasUserPermissions?userId=1
```
**预期响应:**
```json
{
"code": 200,
"data": {
"hasUserPermissions": false,
"permissionCount": 0,
"permissionSource": "角色权限"
}
}
```
## 功能验证
### 1. 前端界面测试
1. **访问权限管理页面**
- 打开浏览器访问:`http://localhost:8082`
- 登录后进入权限管理页面
2. **测试标签页切换**
- 确认"角色权限管理"标签页正常
- 确认"用户权限管理"标签页正常
3. **测试用户权限管理**
- 切换到"用户权限管理"标签页
- 选择用户,查看权限来源显示
- 尝试修改权限设置
### 2. 后端API测试
**测试用户权限查询:**
```bash
GET /api/sysUserMenu/hasUserPermissions?userId=3
```
**测试用户权限分配:**
```bash
POST /api/sysUserMenu/assignUserMenus
Content-Type: application/json
{
"userId": 3,
"menuIds": [1, 2, 3, 16, 4, 5]
}
```
**测试权限清空:**
```bash
DELETE /api/sysUserMenu/clearUserMenus?userId=3
```
## 当前问题解决
### 问题404错误
**现象:** `Failed to load resource: the server responded with a status of 404`
**原因:** 后端服务未包含新的用户权限管理接口
**解决方案:**
1. 确保数据库表已创建
2. 重新编译后端项目:`mvn clean compile`
3. 重启后端服务:`mvn spring-boot:run`
### 问题:前端错误处理
**已解决:** 前端现在能够优雅地处理API不可用的情况
- 显示警告信息而不是错误
- 使用默认值(角色权限)
- 不影响其他功能的使用
## 部署检查清单
- [ ] 数据库表 `sys_user_menu` 已创建
- [ ] 后端项目已重新编译
- [ ] 后端服务已重启
- [ ] API接口 `/api/sysUserMenu/*` 可访问
- [ ] 前端页面可正常加载
- [ ] 用户权限管理功能正常
## 故障排除
### 1. 后端启动失败
**检查:**
- Java版本是否正确
- 数据库连接是否正常
- 端口8080是否被占用
### 2. API接口404
**检查:**
- 控制器类是否正确扫描
- 请求路径是否正确
- 服务是否完全启动
### 3. 数据库连接失败
**检查:**
- 数据库服务是否运行
- 连接配置是否正确
- 用户权限是否足够
## 完成后的功能
部署完成后,系统将支持:
1. **双权限系统**
- 角色权限管理(影响所有使用相同角色的用户)
- 用户权限管理(仅影响单个用户)
2. **权限优先级**
- 用户专属权限覆盖角色权限
- 向后兼容现有功能
3. **界面友好**
- 标签页切换
- 权限来源显示
- 操作确认提示
4. **API完整**
- 用户权限查询
- 用户权限分配
- 用户权限清空
## 联系支持
如果在部署过程中遇到问题,请检查:
1. 后端服务日志
2. 数据库连接状态
3. 网络连接情况
4. 端口占用情况

View File

@@ -1,357 +0,0 @@
# 牛只运输管理系统开发计划
## 1. 项目概述
牛只运输管理系统是一个基于 Vue 3 + TypeScript 开发的现代化前端项目,旨在提供完整的牛只运输管理解决方案。系统集成了运输管理、检疫隔离、设备监控、预警系统等多个功能模块。
## 2. 开发目标
### 2.1 短期目标1-4周
- 完善现有功能模块的用户体验
- 修复已知的Bug和警告信息
- 优化系统性能和加载速度
- 完善文档和注释
详细实施计划请参考 [短期目标任务清单](SHORT_TERM_GOALS.md)
### 2.2 中期目标1-2个月
- 增加数据可视化功能
- 完善权限管理系统
- 增强系统安全性和稳定性
- 增加移动端适配
### 2.3 长期目标3-6个月
- 扩展更多业务功能模块
- 集成更多第三方服务
- 提供多语言支持
- 增强数据分析和报表功能
## 3. 功能模块开发计划
### 3.1 用户管理模块
- **状态**: 已完成
- **负责人**:
- **预计完成时间**:
- **任务**:
- 用户登录/注册功能完善
- 权限管理功能优化
- 用户信息管理界面优化
- 密码安全策略实施
- 多因素认证支持
- 系统用户管理(用户列表、新增/编辑/删除)
- 司机管理(司机列表、新增/编辑/删除、详情查看)
### 3.2 运输管理模块
- **状态**: 开发中(部分功能已完成)
- **负责人**:
- **预计完成时间**:
- **任务**:
- 运输计划制定功能完善
- 运输路线规划功能优化
- 运输状态监控界面改进
- 运输数据统计功能增强
- 轨迹回放功能实现
- 运输成本分析
- 装车管理功能(装车任务分配、状态跟踪、数据记录)
- 运单管理功能(运单创建/编辑、详情查看、状态更新)
### 3.3 检疫和隔离管理模块
- **状态**: 开发中 (部分功能已完成)
- **负责人**:
- **预计完成时间**:
- **任务**:
- 检疫记录管理功能完善
- 隔离状态监控界面优化
- 检疫证书管理功能增强
- 检疫数据分析
- 隔离区管理
- 入境检疫管理(数据录入、核验管理、文件下载)
### 3.4 硬件设备管理模块
- **状态**: 开发中 (部分功能已完成)
- **负责人**:
- **预计完成时间**:
- **任务**:
- 设备状态监控功能完善
- 设备数据采集功能优化
- 设备维护管理界面改进
- 设备报警处理
- 设备统计分析
- 项圈设备管理(列表查看、分配、状态监控)
- 耳标设备管理(列表查看、分配、状态监控)
- 主机设备管理(列表查看、状态监控)
### 3.5 预警系统模块
- **状态**: 开发中
- **负责人**:
- **预计完成时间**:
- **任务**:
- 实时监控预警功能完善
- 异常情况报警功能增强
- 预警规则配置界面优化
- 多渠道通知(短信、邮件、站内信)
- 预警处理跟踪
- 预警列表查看和处理
### 3.6 系统管理模块
- **状态**: 开发中 (部分功能已完成)
- **负责人**:
- **预计完成时间**:
- **任务**:
- 系统配置功能完善
- 日志管理功能增强
- 数据备份功能实现
- 字典管理
- 通知模板配置
- 岗位管理(岗位列表、新增/编辑/删除、权限配置)
- 员工管理(员工列表、新增/编辑/删除、岗位分配)
- 租户管理(租户列表、新增/编辑、设备分配)
## 4. 技术优化计划
### 4.1 性能优化
- **目标**: 提升系统响应速度和用户体验
- **任务**:
- 优化组件加载策略
- 实施代码分割和懒加载
- 减少不必要的重新渲染
- 优化图片和资源加载
- 实施缓存策略
- 数据请求优化
### 4.2 代码质量提升
- **目标**: 提高代码可维护性和可读性
- **任务**:
- 完善 TypeScript 类型定义
- 增加代码注释和文档
- 实施代码审查机制
- 统一代码风格和规范
- 单元测试覆盖率提升
- 集成测试实施
### 4.3 安全性增强
- **目标**: 提高系统安全性和数据保护能力
- **任务**:
- 实施更严格的输入验证
- 加强身份认证和授权机制
- 数据传输加密
- 敏感信息保护
- 安全审计日志
## 5. 详细开发时间表
### 5.1 第一阶段功能完善和Bug修复第1-4周
**时间**: 第1-4周
**目标**: 完善核心功能,修复已知问题
**任务**:
- 修复所有已知Bug和警告
- 完善用户管理模块所有功能
- 完善系统管理模块所有功能
- 完善硬件设备管理模块所有功能
- 性能优化初步实施
### 5.2 第二阶段功能扩展和完善第5-12周
**时间**: 第5-12周
**目标**: 扩展系统功能,增强用户体验
**任务**:
- 完善运输管理模块所有功能
- 完善检疫和隔离管理模块所有功能
- 完善预警系统模块所有功能
- 开发数据可视化功能
- 实现报表生成功能
### 5.3 第三阶段系统优化和测试第13-20周
**时间**: 第13-20周
**目标**: 系统优化和稳定性提升
**任务**:
- 系统性能深度优化
- 安全性增强
- 移动端适配
- 多浏览器兼容性测试
- 用户体验优化
- 全面测试(功能测试、性能测试、安全测试)
### 5.4 第四阶段部署和验收第21-24周
**时间**: 第21-24周
**目标**: 系统部署和用户验收
**任务**:
- 用户验收测试
- Bug修复和优化
- 部署准备
- 上线部署
- 用户培训和文档完善
## 6. 团队分工
### 6.1 前端开发团队
- **职责**: 负责前端界面开发和交互实现
- **成员**:
- **任务分配**:
- UI界面开发
- 组件开发和维护
- 状态管理优化
- 性能优化
- 移动端适配
### 6.2 后端接口对接
- **职责**: 负责与后端接口对接和数据处理
- **成员**:
- **任务分配**:
- API接口调用和封装
- 数据处理和转换
- 错误处理和异常捕获
- 接口文档维护
- 性能优化
### 6.3 测试团队
- **职责**: 负责系统测试和质量保证
- **成员**:
- **任务分配**:
- 功能测试
- 性能测试
- 兼容性测试
- 用户体验测试
- 安全测试
### 6.4 产品经理
- **职责**: 负责需求分析和产品规划
- **成员**:
- **任务分配**:
- 需求收集和分析
- 功能规划
- 用户体验优化
- 与客户沟通
- 项目进度跟踪
## 7. 里程碑计划
### 7.1 里程碑一基础功能完成第4周结束
- 用户管理模块完善
- 系统管理模块完善
- 硬件设备管理模块完善
- 核心Bug修复完成
### 7.2 里程碑二核心功能完成第12周结束
- 运输管理模块完善
- 检疫和隔离管理模块完善
- 预警系统模块完善
- 数据可视化功能实现
### 7.3 里程碑三系统优化完成第20周结束
- 系统性能优化完成
- 安全性增强完成
- 全面兼容性测试完成
- 用户体验优化完成
### 7.4 里程碑四项目交付第24周结束
- 全面测试完成
- 用户验收通过
- 系统部署完成
- 项目文档完善
## 8. 风险评估
### 8.1 技术风险
- 第三方库兼容性问题
- 浏览器兼容性问题
- 性能瓶颈问题
- 移动端适配问题
### 8.2 进度风险
- 需求变更影响开发进度
- 人员变动影响开发进度
- 技术难题导致延期
- 第三方服务集成问题
### 8.3 质量风险
- 代码质量不达标
- 测试覆盖不全面
- 用户体验不佳
- 安全漏洞未发现
### 8.4 资源风险
- 人力资源不足
- 硬件资源不足
- 第三方服务费用超预算
- 时间资源不足
## 9. 质量保证措施
### 9.1 代码审查
- 实施代码审查机制
- 统一代码规范和风格
- 定期进行代码评审
- 使用自动化代码检查工具
### 9.2 测试策略
- 编写单元测试
- 实施集成测试
- 进行用户验收测试
- 性能和安全测试
### 9.3 持续集成
- 建立自动化构建流程
- 实施自动化测试
- 建立部署流程
- 监控和报警机制
## 10. 沟通机制
### 10.1 日常沟通
- 每日站会15分钟
- 即时通讯工具沟通
- 问题及时反馈和解决
- 代码提交规范
### 10.2 周期性会议
- 每周项目进度会议1小时
- 每月项目总结会议2小时
- 阶段性评审会议
- 需求变更评审会议
### 10.3 文档管理
- 统一文档管理平台
- 及时更新项目文档
- 知识共享和传承
- 版本控制
## 11. 预算和资源
### 11.1 人力资源
- 前端开发工程师2名
- 后端开发工程师1名
- 测试工程师1名
- 产品经理1名
- 项目经理1名
### 11.2 技术资源
- 开发工具许可证
- 第三方服务费用
- 服务器资源
- 域名和SSL证书
### 11.3 时间资源
- 总开发周期24周
- 测试周期4周
- 部署和上线2周
## 12. 交付物
### 12.1 软件交付物
- 完整的前端应用程序
- 源代码和相关文档
- 部署脚本和配置文件
- 用户手册和操作指南
### 12.2 文档交付物
- 需求文档
- 设计文档
- 测试报告
- 部署文档
- 维护手册
### 12.3 培训交付物
- 用户培训材料
- 管理员培训材料
- 技术培训材料
- 在线帮助文档

View File

@@ -1,186 +0,0 @@
# 司机管理删除功能实现报告
## 概述
实现司机管理页面中的删除按钮功能,可以删除数据库中的司机数据。
## 实现内容
### 1. 后端实现
#### 控制器 (`MemberController.java`)
新增删除司机接口:
```402:447:tradeCattle/aiotagro-cattle-trade/src/main/java/com/aiotagro/cattletrade/business/controller/MemberController.java
/**
* 删除司机
*/
@SaCheckPermission("member:delete")
@PostMapping("/deleteDriver")
@Transactional
public AjaxResult deleteDriver(@RequestBody Map<String, Object> params) {
try {
Integer id = (Integer) params.get("id");
if (id == null) {
return AjaxResult.error("司机ID不能为空");
}
// 查询司机信息获取member_id
Map<String, Object> driverInfo = memberDriverMapper.selectDriverById(id);
if (driverInfo == null) {
return AjaxResult.error("司机信息不存在");
}
Integer memberId = (Integer) driverInfo.get("member_id");
// 删除司机详情member_driver表
int driverResult = memberDriverMapper.deleteById(id);
// 如果member_id存在也删除member表记录
if (memberId != null) {
int memberResult = memberMapper.deleteById(memberId);
if (memberResult > 0 && driverResult > 0) {
return AjaxResult.success("删除成功");
} else if (driverResult > 0) {
return AjaxResult.success("删除司机信息成功");
} else {
return AjaxResult.error("删除失败");
}
} else {
if (driverResult > 0) {
return AjaxResult.success("删除成功");
} else {
return AjaxResult.error("删除失败");
}
}
} catch (Exception e) {
e.printStackTrace();
return AjaxResult.error("删除司机失败:" + e.getMessage());
}
}
```
#### 删除逻辑说明
1. **删除两个表的数据**
- 删除 `member_driver` 表中的司机详细信息
- 删除 `member` 表中的基础会员信息
2. **事务保证**:使用 `@Transactional` 注解确保数据一致性
3. **权限控制**:使用 `@SaCheckPermission("member:delete")` 进行权限验证
### 2. 前端实现
#### API接口 (`userManage.js`)
添加删除司机API方法
```70:77:pc-cattle-transportation/src/api/userManage.js
// 司机 - 删除
export function driverDel(id) {
return request({
url: '/member/deleteDriver',
method: 'POST',
data: { id },
});
}
```
#### 页面实现 (`driver.vue`)
导入必要的依赖:
```60:63:pc-cattle-transportation/src/views/userManage/driver.vue
import { ElMessage, ElMessageBox } from 'element-plus';
import { Picture } from '@element-plus/icons-vue';
import baseSearch from '@/components/common/searchCustom/index.vue';
import { driverList, driverDel } from '@/api/userManage.js';
```
实现删除方法:
```160:181:pc-cattle-transportation/src/views/userManage/driver.vue
// 删除
const delClick = (row) => {
ElMessageBox.confirm('请确认是否删除该司机数据?', '删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
driverDel(row.id)
.then(() => {
ElMessage.success('删除成功');
getDataList();
})
.catch((error) => {
ElMessage.error('删除失败');
console.error('删除失败:', error);
});
})
.catch(() => {
// 用户取消删除
});
};
```
## 功能特点
1. **二次确认**:点击删除按钮时,会弹出确认对话框
2. **直接删除**:确认后直接调用后端接口删除数据库记录
3. **级联删除**:删除司机记录时,同时删除关联的会员基础信息
4. **自动刷新**:删除成功后自动刷新列表
5. **错误处理**:删除失败时显示错误提示
6. **事务保护**:使用数据库事务确保数据一致性
7. **权限控制**:需要 `member:delete` 权限才能执行删除操作
## 工作流程
1. 用户点击"删除"按钮
2. 弹出确认对话框:"请确认是否删除该司机数据?"
3. 用户点击"确定"
4. 前端调用 `/member/deleteDriver` 接口传递司机ID
5. 后端查询司机信息,获取关联的 member_id
6. 后端删除 `member_driver` 表中的司机信息
7. 后端删除 `member` 表中的会员基础信息
8. 返回成功响应
9. 前端显示"删除成功"提示
10. 自动刷新司机列表
## 数据库影响
删除操作会影响以下表:
1. **member_driver表**:删除司机的详细信息(姓名、车牌、照片等)
2. **member表**:删除会员的基础信息(手机号、状态等)
## 注意事项
1. **不可恢复**:删除操作是物理删除,不可恢复(除非有备份)
2. **关联数据**:删除司机前需要检查该司机是否有关联的运单或其他业务数据
3. **权限要求**:需要具有 `member:delete` 权限的用户才能删除
4. **事务保护**:使用事务确保即使部分删除失败也不会导致数据不一致
## 后续改进建议
1. **软删除**:考虑实现软删除(添加 `is_delete` 标记)
2. **关联检查**:删除前检查司机是否有关联的运单
3. **级联处理**:如果有关联数据,提供选项:
- 阻止删除
- 解绑后删除
- 级联删除所有关联数据
4. **操作日志**:记录删除操作的日志
## 修改的文件
### 后端
- ✅ `tradeCattle/aiotagro-cattle-trade/src/main/java/com/aiotagro/cattletrade/business/controller/MemberController.java` - 新增删除接口
### 前端
- ✅ `pc-cattle-transportation/src/api/userManage.js` - 添加删除API方法
- ✅ `pc-cattle-transportation/src/views/userManage/driver.vue` - 实现删除功能
## 创建时间
2025-01-16

View File

@@ -1,140 +0,0 @@
# 编辑司机表单自动填充修复
## 问题描述
点击编辑按钮时,司机信息表单中的车牌号和账号状态没有自动填充。
## 问题原因
1. **字段名不匹配**:后端返回的字段名是 `car_number`(下划线),但前端使用的是 `carNumber`(驼峰命名)
2. **缺少 status 字段**:后端查询中没有包含 `member` 表的 `status` 字段
## 修复方案
### 1. 前端修复 (`driverDialog.vue`)
```javascript
// 修复前
ruleForm.carNumber = row.carNumber; // 字段名不匹配
ruleForm.status = row.status; // 后端没有返回此字段
// 修复后
ruleForm.carNumber = row.car_number; // 使用正确的字段名
ruleForm.status = row.status || '1'; // 添加默认值
```
### 2. 后端修复 (`MemberDriverMapper.java`)
更新所有查询方法,添加 `m.status` 字段:
```java
// 修复前
@Select("SELECT md.id, md.member_id, md.username, md.car_number, " +
"md.driving_license, md.driver_license, md.record_code, " +
"md.car_img, md.id_card, md.remark, md.create_time, m.mobile " +
"FROM member_driver md " +
"LEFT JOIN member m ON md.member_id = m.id " +
"ORDER BY md.id DESC")
// 修复后
@Select("SELECT md.id, md.member_id, md.username, md.car_number, " +
"md.driving_license, md.driver_license, md.record_code, " +
"md.car_img, md.id_card, md.remark, md.create_time, m.mobile, m.status " +
"FROM member_driver md " +
"LEFT JOIN member m ON md.member_id = m.id " +
"ORDER BY md.id DESC")
```
## 修复的查询方法
1.`selectDriverList()` - 司机列表查询
2.`selectDriverListByUsername()` - 按用户名搜索
3.`selectDriverListByMobile()` - 按手机号搜索
4.`selectDriverListBySearch()` - 综合搜索
5.`selectDriverById()` - 根据ID查询详情
6.`selectDriverByPlate()` - 根据车牌号查询
## 数据流验证
### 1. 后端返回数据格式
```json
{
"id": 1,
"member_id": 1,
"username": "测试司机2",
"car_number": "京A12345", // ✅ 下划线格式
"mobile": "19999999999",
"status": "1", // ✅ 新增状态字段
"driver_license": "url1,url2",
"driving_license": "url3",
"car_img": "url4,url5",
"record_code": "url6",
"id_card": "url7,url8", // ✅ 身份证字段
"remark": "备注信息"
}
```
### 2. 前端数据映射
```javascript
// 编辑时数据填充
ruleForm.id = row.id;
ruleForm.username = row.username; // ✅ 司机姓名
ruleForm.mobile = row.mobile; // ✅ 司机手机号
ruleForm.carNumber = row.car_number; // ✅ 车牌号(修复字段名)
ruleForm.status = row.status || '1'; // ✅ 账号状态(修复字段名+默认值)
ruleForm.remark = row.remark; // ✅ 备注
```
### 3. 表单显示效果
-**司机姓名**:自动填充 "测试司机2"
-**司机手机号**:自动填充 "19999999999"
-**车牌号**:自动填充 "京A12345"
-**账号状态**:自动选择 "启用" 或 "禁用"
-**驾驶证**:显示已上传的图片
-**行驶证**:显示已上传的图片
-**牧运通备案码**:显示已上传的图片
-**身份证前后面**:显示已上传的图片
## 测试步骤
### 1. 测试编辑功能
1. 进入司机管理页面
2. 点击任意司机的"编辑"按钮
3. 验证表单字段是否正确填充:
- 司机姓名 ✅
- 司机手机号 ✅
- 车牌号 ✅(修复后应该显示)
- 账号状态 ✅(修复后应该显示)
- 各种证件图片 ✅
### 2. 测试保存功能
1. 修改车牌号
2. 修改账号状态
3. 点击保存
4. 验证数据是否正确更新
### 3. 数据库验证
```sql
-- 检查司机数据
SELECT id, username, car_number, status FROM member_driver md
LEFT JOIN member m ON md.member_id = m.id
WHERE md.id = ?;
-- 检查状态字段
SELECT DISTINCT status FROM member;
```
## 预期结果
修复后,点击编辑按钮时:
-**车牌号字段**:自动填充数据库中的 `car_number`
-**账号状态**:自动选择对应的状态(启用/禁用)
-**其他字段**:继续正常填充
-**图片字段**:继续正常显示
## 技术要点
1. **字段名映射**:后端使用下划线命名,前端使用驼峰命名
2. **默认值处理**:为可能为空的字段提供默认值
3. **数据完整性**:确保所有必要的字段都在查询中返回
4. **向后兼容**:修复不影响现有功能
## 总结
通过修复字段名映射和添加缺失的 `status` 字段查询,编辑司机表单现在能够正确自动填充车牌号和账号状态,提升了用户体验。

View File

@@ -1,96 +0,0 @@
# 字段映射问题诊断和解决方案
## 🔍 问题分析
根据您提供的API数据发现以下问题
- `supplierName`: null
- `buyerName`: null
- `fundName`: null
- `supplierMobile`: "16666666666" ✅
- `buyerMobile`: "17777777777" ✅
- `fundMobile`: "17777777771" ✅
## 🔧 已实施的解决方案
### 1. 后端改进
- ✅ 修改了 `DeliveryServiceImpl.pageQuery` 方法
- ✅ 添加了 `MemberMapper.selectMemberUserById` 方法
- ✅ 实现了 `member` 表和 `member_user` 表的关联查询
- ✅ 添加了详细的调试日志
### 2. 前端回退机制
- ✅ 实现了用户名优先,手机号备选的显示逻辑
- ✅ 更新了HTML模板使用回退数据
## 🧪 测试步骤
### 1. 检查后端日志
重启后端服务后,查看控制台输出:
```
供应商查询结果 - ID: 61, 结果: {id=61, mobile=16666666666, username=测试供应商1}
供应商 - ID: 61, Username: 测试供应商1, Mobile: 16666666666
资金方查询结果 - ID: 63, 结果: {id=63, mobile=17777777771, username=测试资金方1}
资金方 - ID: 63, Username: 测试资金方1, Mobile: 17777777771
采购商查询结果 - ID: 62, 结果: {id=62, mobile=17777777777, username=测试采购方1}
采购商 - ID: 62, Username: 测试采购方1, Mobile: 17777777777
```
### 2. 测试前端功能
1. 刷新入境检疫页面
2. 查看控制台"原始数据字段检查"日志
3. 点击"下载文件"按钮
4. 检查生成的HTML文档
## 🎯 预期结果
### 如果后端查询成功:
- `supplierName`: "测试供应商1"
- `buyerName`: "测试采购方1"
- `fundName`: "测试资金方1"
### 如果后端查询失败(当前情况):
- `supplierName`: "16666666666" (回退到手机号)
- `buyerName`: "17777777777" (回退到手机号)
- `fundName`: "17777777771" (回退到手机号)
## 🔍 可能的问题原因
1. **数据库表结构问题**
- `member_user` 表中可能没有对应的记录
- `username` 字段可能为空
2. **查询逻辑问题**
- SQL查询可能有问题
- 字段映射可能不正确
3. **数据问题**
- ID 61, 62, 63 在 `member_user` 表中可能不存在
## 📋 下一步诊断
1. **检查数据库**
```sql
SELECT m.id, m.mobile, mu.username
FROM member m
LEFT JOIN member_user mu ON m.id = mu.member_id
WHERE m.id IN (61, 62, 63);
```
2. **查看后端日志**
- 检查是否有查询结果
- 确认 `username` 字段的值
3. **测试API**
- 重新加载页面
- 查看API响应中的字段值
## ✅ 当前解决方案的优势
- **容错性强**:即使后端查询失败,也能显示手机号
- **用户体验好**:不会出现空白字段
- **调试友好**:有详细的日志输出
- **向后兼容**:不影响现有功能
现在您可以测试功能了!即使后端查询有问题,前端也会显示手机号作为备选方案。

View File

@@ -1,98 +0,0 @@
# 字段映射优化完成报告
## ✅ 问题分析
根据您提供的API数据结构发现了以下问题
- `buyerName``supplierName``fundName` 字段都是 `null`
- 需要通过 `buyerId``supplierId``fundId` 关联查询 `member_user` 表获取 `username`
- 需要实现 `username/手机号` 格式的字段映射
## 🔧 后端改进
### 1. 修改 `DeliveryServiceImpl.pageQuery` 方法
- ✅ 添加了对 `member_user` 表的关联查询
- ✅ 实现了供应商、资金方、采购商用户名的查询
- ✅ 支持逗号分隔的供应商ID处理
### 2. 新增 `MemberMapper.selectMemberUserById` 方法
```java
@Select("SELECT m.id, m.mobile, mu.username " +
"FROM member m " +
"LEFT JOIN member_user mu ON m.id = mu.member_id " +
"WHERE m.id = #{memberId}")
Map<String, Object> selectMemberUserById(@Param("memberId") Integer memberId);
```
### 3. 字段映射逻辑
- **供应商**: 查询 `supplierId``member_user.username` + `member.mobile`
- **资金方**: 查询 `fundId``member_user.username` + `member.mobile`
- **采购商**: 查询 `buyerId``member_user.username` + `member.mobile`
## 🎨 前端改进
### 1. 增强字段映射
- ✅ 优先使用 `username`,如果没有则使用 `mobile`
- ✅ 添加了详细的调试日志
- ✅ 支持用户名/手机号的回退显示
### 2. HTML模板优化
```javascript
// 供货单位显示逻辑
<td>${data.supplierName || row.supplierMobile || ''}</td>
// 收货单位显示逻辑
<td>${data.buyerName || row.buyerMobile || ''}</td>
```
## 📊 数据流程
### 原始数据
```json
{
"supplierId": "61",
"buyerId": 62,
"fundId": 63,
"supplierName": null,
"buyerName": null,
"fundName": null
}
```
### 处理后数据
```json
{
"supplierId": "61",
"buyerId": 62,
"fundId": 63,
"supplierName": "供应商用户名",
"buyerName": "采购商用户名",
"fundName": "资金方用户名",
"supplierMobile": "16666666666",
"buyerMobile": "17777777777",
"fundMobile": "17777777771"
}
```
## 🧪 测试步骤
1. **重启后端服务**:确保新的查询逻辑生效
2. **刷新前端页面**:重新加载入境检疫列表
3. **检查控制台日志**:查看"原始数据字段检查"输出
4. **测试导出功能**:点击"下载文件"按钮
5. **验证字段显示**:确认用户名正确显示
## 🎯 预期结果
-`supplierName``buyerName``fundName` 字段不再为 `null`
- ✅ Word导出文档中正确显示用户名
- ✅ 如果用户名为空,则显示手机号作为备选
- ✅ 所有计算字段(总重量、单价、总金额)正确计算
## 📝 注意事项
1. **数据库依赖**:确保 `member_user` 表中有对应的用户记录
2. **字段回退**:如果 `username` 为空,会自动使用 `mobile` 字段
3. **逗号分隔**供应商ID支持多个值用逗号分隔
4. **错误处理**:添加了异常处理,避免查询失败影响整体功能
现在您可以测试更新后的功能了!后端会正确查询用户名,前端会优先显示用户名,如果没有用户名则显示手机号。

View File

@@ -1,103 +0,0 @@
# 字段映射问题诊断和验证
## 🔍 问题分析
根据您提供的数据当前API返回
- `supplierName`: "16666666666" (手机号)
- `buyerName`: "17777777777" (手机号)
- `fundName`: "17777777771" (手机号)
这说明我们的关联查询逻辑可能没有正确执行。
## 🔧 已实施的修改
### 1. 后端修改
- ✅ 修改了 `DeliveryServiceImpl.pageQuery` 方法
- ✅ 添加了 `MemberMapper.selectMemberUserById` 方法
- ✅ 实现了 `member` 表和 `member_user` 表的关联查询
- ✅ 添加了详细的调试日志
### 2. 前端修改
- ✅ 实现了回退机制:`row.supplierName || row.supplierMobile || ''`
## 🧪 验证步骤
### 1. 检查后端日志
重启后端服务后,查看控制台输出,应该看到类似这样的日志:
```
供应商查询结果 - ID: 61, 结果: {id=61, mobile=16666666666, username=测试供应商1}
供应商 - ID: 61, Username: 测试供应商1, Mobile: 16666666666
```
### 2. 检查API调用
确认前端调用的是正确的API
- 前端调用:`/delivery/pageQueryList`
- 后端方法:`DeliveryController.pageQueryList``deliveryService.pageQuery`
### 3. 数据库验证
如果后端日志显示查询结果为空可以执行以下SQL验证
```sql
SELECT m.id, m.mobile, mu.username
FROM member m
LEFT JOIN member_user mu ON m.id = mu.member_id
WHERE m.id IN (61, 62, 63);
```
## 🎯 预期结果
### 如果 `member_user` 表中有用户名:
- `supplierName`: "测试供应商1"
- `buyerName`: "测试采购方1"
- `fundName`: "测试资金方1"
### 如果 `member_user` 表中用户名为空:
- `supplierName`: "16666666666" (回退到手机号)
- `buyerName`: "17777777777" (回退到手机号)
- `fundName`: "17777777771" (回退到手机号)
## 🔍 可能的问题原因
1. **后端服务没有重启**:修改没有生效
2. **数据库表结构**`member_user` 表中可能没有对应的记录
3. **数据问题**ID 61, 62, 63 在 `member_user` 表中可能不存在或 `username` 字段为空
4. **查询逻辑**SQL查询可能有问题
## 📋 调试方法
### 1. 检查后端日志
查看是否有我们添加的调试日志输出
### 2. 检查数据库
```sql
-- 检查member表
SELECT * FROM member WHERE id IN (61, 62, 63);
-- 检查member_user表
SELECT * FROM member_user WHERE member_id IN (61, 62, 63);
-- 检查关联查询
SELECT m.id, m.mobile, mu.username
FROM member m
LEFT JOIN member_user mu ON m.id = mu.member_id
WHERE m.id IN (61, 62, 63);
```
### 3. 检查API响应
刷新前端页面,查看控制台"原始数据字段检查"日志
## ✅ 当前解决方案的优势
- **容错性强**:即使后端查询失败,也能显示手机号
- **用户体验好**:不会出现空白字段
- **调试友好**:有详细的日志输出
- **向后兼容**:不影响现有功能
## 🚀 下一步
1. 等待后端服务完全启动
2. 刷新前端页面
3. 查看后端日志输出
4. 检查API响应数据
5. 如果问题仍然存在,检查数据库表结构
现在请等待后端服务启动完成,然后测试功能并查看日志输出。

View File

@@ -1,208 +0,0 @@
# getUserMenus API 权限查询修复报告
## 问题描述
用户"12.27新增姓名"设置了用户专属权限,在权限管理界面中可以看到"装车订单"下的操作按钮(如"编辑"、"分配设备"、"删除"、"装车"等)都是**未选中**状态,表示这些按钮应该被隐藏。但是当用户登录后,这些操作按钮仍然显示。
## 问题根本原因
### 1. 权限查询逻辑不一致
虽然我们修改了 `LoginServiceImpl.java` 中的 `queryUserPermissions` 方法,使其优先使用用户专属权限,但是 `getUserMenus()` API 没有使用这个修改后的逻辑。
### 2. getUserMenus API 的问题
**文件**`tradeCattle/aiotagro-cattle-trade/src/main/java/com/aiotagro/cattletrade/business/service/impl/LoginServiceImpl.java`
**原来的实现**第147-151行
```java
@Override
public AjaxResult getUserMenus() {
Integer userId = SecurityUtil.getCurrentUserId();
List<SysMenu> menus = menuMapper.queryMenusByUserId(userId);
return AjaxResult.success("查询成功", menus);
}
```
**问题**`menuMapper.queryMenusByUserId(userId)` 只查询角色权限,**完全忽略用户专属权限**。
### 3. queryMenusByUserId SQL 的问题
**文件**`tradeCattle/aiotagro-cattle-trade/src/main/resources/mapper/SysMenuMapper.xml`
**SQL查询**第25-33行
```sql
SELECT m.*
FROM sys_menu m
LEFT JOIN sys_role_menu rm ON m.id = rm.menu_id
LEFT JOIN sys_user u ON rm.role_id = u.role_id
WHERE u.is_delete = 0
AND m.is_delete = 0
AND u.id = #{userId}
```
**问题**:这个查询只通过 `sys_role_menu` 表查询角色权限,**完全忽略了 `sys_user_menu` 表中的用户专属权限**。
## 修复方案
### 1. 修改 getUserMenus 方法
**修改文件**`tradeCattle/aiotagro-cattle-trade/src/main/java/com/aiotagro/cattletrade/business/service/impl/LoginServiceImpl.java`
**修改内容**
```java
@Override
public AjaxResult getUserMenus() {
Integer userId = SecurityUtil.getCurrentUserId();
// 获取当前用户的角色ID
SysUser user = userMapper.selectById(userId);
if (user == null) {
return AjaxResult.error("用户不存在");
}
// 使用修改后的权限查询逻辑(优先使用用户专属权限)
List<String> permissions = queryUserPermissions(userId, user.getRoleId());
// 根据权限查询菜单
List<SysMenu> menus;
if (permissions.contains(RoleConstants.ALL_PERMISSION)) {
// 超级管理员:返回所有菜单
menus = menuMapper.selectList(null);
} else {
// 普通用户:根据权限查询菜单
menus = menuMapper.selectMenusByPermissions(permissions);
}
log.info("=== 用户 {} 菜单查询结果,权限数量: {}, 菜单数量: {}", userId, permissions.size(), menus.size());
return AjaxResult.success("查询成功", menus);
}
```
### 2. 添加 selectMenusByPermissions 方法
**修改文件**`tradeCattle/aiotagro-cattle-trade/src/main/java/com/aiotagro/cattletrade/business/mapper/SysMenuMapper.java`
**添加内容**
```java
/**
* 根据权限列表查询菜单
*
* @param permissions 权限列表
* @return 菜单列表
*/
List<SysMenu> selectMenusByPermissions(@Param("permissions") List<String> permissions);
```
### 3. 添加对应的SQL查询
**修改文件**`tradeCattle/aiotagro-cattle-trade/src/main/resources/mapper/SysMenuMapper.xml`
**添加内容**
```xml
<select id="selectMenusByPermissions" resultType="com.aiotagro.cattletrade.business.entity.SysMenu">
SELECT DISTINCT m.*
FROM sys_menu m
WHERE m.is_delete = 0
AND m.authority IN
<foreach collection="permissions" item="permission" open="(" separator="," close=")">
#{permission}
</foreach>
ORDER BY m.sort ASC
</select>
```
## 修复逻辑流程
### 修复前
```
用户登录 → getUserMenus() → queryMenusByUserId() → 只查询角色权限 → 忽略用户专属权限
```
### 修复后
```
用户登录 → getUserMenus() → queryUserPermissions() → 优先查询用户专属权限 → 根据权限查询菜单
```
## 权限查询优先级
修复后的权限查询优先级:
1. **用户专属权限**(最高优先级)
- 查询 `sys_user_menu`
- 如果存在,使用用户专属权限
2. **角色权限**(普通用户)
- 查询 `sys_role_menu`
- 如果用户没有专属权限,使用角色权限
3. **超级管理员权限**fallback
- 如果角色ID是超级管理员且没有专属权限使用所有权限
## 修复效果
### 修复前
-`getUserMenus()` API 只查询角色权限
- ❌ 用户专属权限被完全忽略
- ❌ 前端权限检查使用错误的权限数据
- ❌ 操作按钮无法正确隐藏
### 修复后
-`getUserMenus()` API 使用统一的权限查询逻辑
- ✅ 用户专属权限优先于角色权限
- ✅ 前端权限检查使用正确的权限数据
- ✅ 操作按钮能够正确隐藏
## 测试验证
### 测试步骤
1. **重新编译后端**`mvn clean compile`
2. **重启后端服务**`mvn spring-boot:run`
3. **清除浏览器缓存**
4. **使用"12.27新增姓名"账号重新登录**
5. **检查装车订单页面的操作按钮**
### 预期结果
- 用户"12.27新增姓名"登录后,装车订单页面的操作按钮应该根据专属权限设置被隐藏
- 控制台日志应该显示"用户 3 使用专属权限"
- 权限检查应该显示 `isSuperAdmin: false`
- `getUserMenus()` API 应该返回基于用户专属权限的菜单数据
## 技术细节
### 权限数据流
```
后端权限查询 → getUserMenus() → queryUserPermissions() → 用户专属权限优先
前端权限store → permissionStore.userPermission → 基于用户专属权限
权限检查 → hasPermi.js → 使用正确的权限数据 → 按钮正确隐藏/显示
```
### 日志输出
修复后的日志输出示例:
```
=== 用户 3 使用专属权限,权限数量: 15
=== 用户 3 菜单查询结果,权限数量: 15, 菜单数量: 20
```
## 相关文件
- `tradeCattle/aiotagro-cattle-trade/src/main/java/com/aiotagro/cattletrade/business/service/impl/LoginServiceImpl.java` - getUserMenus方法
- `tradeCattle/aiotagro-cattle-trade/src/main/java/com/aiotagro/cattletrade/business/mapper/SysMenuMapper.java` - selectMenusByPermissions方法
- `tradeCattle/aiotagro-cattle-trade/src/main/resources/mapper/SysMenuMapper.xml` - selectMenusByPermissions SQL
- `pc-cattle-transportation/src/store/permission.js` - 前端权限store
- `pc-cattle-transportation/src/directive/permission/hasPermi.js` - 前端权限检查
## 总结
通过修改 `getUserMenus()` API 使其使用统一的权限查询逻辑,成功解决了用户专属权限无法生效的问题。修复后的系统能够:
1. **正确查询用户专属权限**getUserMenus API 使用 queryUserPermissions 方法
2. **按预期隐藏操作按钮**:前端权限检查使用正确的权限数据
3. **保持权限优先级**:用户专属权限 > 角色权限 > 超级管理员权限
4. **提供清晰的日志**:便于调试和监控
**修复状态**:✅ 已完成
**测试状态**:⏳ 待验证
**部署状态**:✅ 已部署

View File

@@ -1,102 +0,0 @@
# HTML文档导出功能测试指南
## ✅ 功能实现完成
### 🎯 核心功能
- ✅ 实现了HTML格式的牛只发车验收单生成
- ✅ 支持在新窗口中预览文档
- ✅ 内置打印功能可保存为PDF
- ✅ 严格按照图片格式设计布局
- ✅ 完整的字段映射和计算逻辑
### 📋 字段映射
- ✅ 供货单位 ← `supplierName`
- ✅ 收货单位 ← `buyerName`
- ✅ 发车地点 ← `startLocation`
- ✅ 发车时间 ← `createTime`
- ✅ 到达地点 ← `endLocation`
- ✅ 司机姓名及联系方式 ← `driverName` + `driverMobile`
- ✅ 装车车牌号 ← `licensePlate`
- ✅ 下车总数量 ← `ratedQuantity`
- ✅ 下车总重量 ← 计算:(落地装车磅数-空车磅重)/2
- ✅ 单价 ← 计算:约定价格/2
- ✅ 总金额 ← 计算:下车总重量×单价
### 🎨 设计特点
- ✅ 专业的表格布局
- ✅ 打印友好的样式
- ✅ 响应式设计
- ✅ 清晰的字体和间距
- ✅ 边框和背景色区分
## 🧪 测试步骤
### 1. 基本功能测试
1. 打开应用http://localhost:8081/
2. 登录并进入"入境检疫"页面
3. 找到状态为"已装车"或"运输中"的记录
4. 点击"下载文件"按钮
### 2. 预期结果
- ✅ 新窗口打开,显示格式化的验收单
- ✅ 所有字段正确填充
- ✅ 计算公式正确执行
- ✅ 布局与图片格式一致
### 3. 打印/PDF测试
1. 在新窗口中点击"打印/保存为PDF"按钮
2. 在打印对话框中选择"另存为PDF"
3. 保存PDF文件
4. 验证PDF格式和内容
### 4. 数据验证
检查以下计算是否正确:
- 下车总重量 = (落地装车磅数 - 空车磅重) / 2
- 单价 = 约定价格 / 2
- 总金额 = 下车总重量 × 单价
## 🔧 技术实现
### 前端技术栈
- Vue 3 Composition API
- HTML5 + CSS3
- JavaScript ES6+
- 浏览器打印API
### 核心代码
```javascript
// 计算字段
const landingWeight = parseFloat(row.landingEntruckWeight || 0);
const emptyWeight = parseFloat(row.emptyWeight || 0);
const totalWeight = ((landingWeight - emptyWeight) / 2).toFixed(2);
const unitPrice = (parseFloat(row.firmPrice || 0) / 2).toFixed(2);
const totalAmount = (parseFloat(totalWeight) * parseFloat(unitPrice)).toFixed(2);
// 生成HTML并打开新窗口
const newWindow = window.open('', '_blank');
newWindow.document.write(htmlContent);
newWindow.document.close();
```
## 🎉 优势
1. **无需额外依赖**不依赖复杂的Word处理库
2. **跨平台兼容**:所有现代浏览器都支持
3. **打印友好**:专门优化的打印样式
4. **PDF支持**通过浏览器打印功能生成PDF
5. **易于维护**纯HTML/CSS实现易于修改
6. **性能优秀**:轻量级实现,加载快速
## 📝 使用说明
1. **查看文档**:点击"下载文件"按钮在新窗口中查看
2. **打印文档**:点击"打印/保存为PDF"按钮
3. **保存PDF**:在打印对话框中选择"另存为PDF"
4. **编辑内容**:可以在打印前手动编辑某些字段
## 🚀 后续优化建议
1. 可以添加更多导出格式选项
2. 可以添加文档模板选择功能
3. 可以添加批量导出功能
4. 可以添加文档预览功能

View File

@@ -1,216 +0,0 @@
# 身份证图片数据流验证
## 数据流概述
身份证前后面的照片地址从前端表单 → 后端API → 数据库 `id_card` 字段使用英文逗号分隔多个URL。
## 前端实现 ✅
### 1. 表单数据结构
```javascript
const ruleForm = reactive({
username: '', // 司机姓名
mobile: '', // 司机手机号
status: '', // 账号状态
carNumber: '', // 车牌号
driverImg: [], // 驾驶证
licenseImg: [], // 行驶证
codeImg: [], // 牧运通备案码
carImg: [], // 车头&车身照片
idCardImg: [], // 身份证前后面 ✅
remark: '', // 备注
});
```
### 2. 图片上传处理
```javascript
// 身份证图片上传成功处理
const handleAvatarSuccess = (res, file, fileList, type) => {
if (ruleForm.hasOwnProperty(type)) {
// 解析图片URL并添加到对应数组
ruleForm[type].push({ url: imageUrl });
}
};
```
### 3. 数据提交处理
```javascript
// 保存时处理身份证图片
params.idCard = ruleForm.idCardImg.length > 0
? ruleForm.idCardImg.map((item) => item.url).join(',')
: '';
```
**示例数据**
```javascript
// 前端发送的数据
{
username: '张三',
mobile: '13800138000',
carNumber: '京A12345',
idCard: 'https://example.com/id_front.jpg,https://example.com/id_back.jpg'
}
```
## 后端实现 ✅
### 1. Controller 层
```java
@PostMapping("/addDriver")
public AjaxResult addDriver(@RequestBody Map<String, Object> params) {
String idCard = (String) params.get("idCard"); // ✅ 获取身份证参数
// 调用 Mapper 插入数据
int driverResult = memberDriverMapper.insertDriver(
memberId, username, carNumber, driverLicense,
drivingLicense, carImg, recordCode, idCard, remark // ✅ 传递 idCard
);
}
```
### 2. Mapper 层
```java
@Insert("INSERT INTO member_driver (member_id, username, car_number, " +
"driver_license, driving_license, car_img, record_code, id_card, remark, create_time) " +
"VALUES (#{memberId}, #{username}, #{carNumber}, #{driverLicense}, #{drivingLicense}, " +
"#{carImg}, #{recordCode}, #{idCard}, #{remark}, NOW())")
int insertDriver(..., @Param("idCard") String idCard, ...); // ✅ 包含 idCard 参数
```
## 数据库存储 ✅
### 表结构
```sql
CREATE TABLE member_driver (
id INT PRIMARY KEY AUTO_INCREMENT,
member_id INT,
username VARCHAR(100),
car_number VARCHAR(20),
driver_license TEXT,
driving_license TEXT,
car_img TEXT,
record_code TEXT,
id_card TEXT COMMENT '身份证前后面照片地址多个URL用逗号分隔', -- ✅ 新增字段
remark TEXT,
create_time DATETIME
);
```
### 存储示例
```sql
INSERT INTO member_driver (member_id, username, car_number, id_card, create_time)
VALUES (1, '张三', '京A12345', 'https://example.com/id_front.jpg,https://example.com/id_back.jpg', NOW());
```
## 数据读取和显示 ✅
### 1. 后端查询
```java
@Select("SELECT md.id, md.member_id, md.username, md.car_number, " +
"md.driving_license, md.driver_license, md.record_code, " +
"md.car_img, md.id_card, md.remark, md.create_time, m.mobile " + // ✅ 包含 id_card
"FROM member_driver md " +
"LEFT JOIN member m ON md.member_id = m.id " +
"WHERE md.id = #{driverId}")
Map<String, Object> selectDriverById(@Param("driverId") Integer driverId);
```
### 2. 前端数据加载
```javascript
// 编辑时加载数据
ruleForm.idCardImg = row.id_card
? getImageList(row.id_card).map((item) => {
return { url: item };
})
: [];
// 处理逗号分隔的图片URL
const getImageList = (imageUrl) => {
if (!imageUrl || imageUrl.trim() === '') {
return [];
}
return imageUrl.split(',').map(url => url.trim()).filter(url => url !== '');
};
```
### 3. 详情页面显示
```vue
<el-col :span="12" style="display: flex">
<div><span class="label">身份证前后面</span></div>
<template v-if="data.info.id_card">
<el-image
v-for="(item, index) in getImageList(data.info.id_card)"
:key="index"
:src="item"
style="width: 80px; height: 80px; margin-right: 10px"
fit="cover"
:preview-src-list="getImageList(data.info.id_card)"
preview-teleported
>
<template #error>
<div style="width: 50px; height: 50px; display: flex; justify-content: center" class="image-slot">
<el-icon :size="20"><Picture /></el-icon>
</div>
</template>
</el-image>
</template>
<span v-else style="color: #999">暂无图片</span>
</el-col>
```
## 完整数据流示例
### 1. 新增司机流程
```
用户上传身份证照片
→ 前端: ruleForm.idCardImg = [{url: 'url1'}, {url: 'url2'}]
→ 前端: params.idCard = 'url1,url2'
→ 后端: String idCard = params.get("idCard")
→ 数据库: INSERT INTO member_driver (..., id_card, ...) VALUES (..., 'url1,url2', ...)
```
### 2. 编辑司机流程
```
用户点击编辑
→ 后端: SELECT ..., id_card FROM member_driver WHERE id = ?
→ 前端: row.id_card = 'url1,url2'
→ 前端: ruleForm.idCardImg = [{url: 'url1'}, {url: 'url2'}]
→ 用户修改后保存
→ 前端: params.idCard = 'url1,url3' (修改后的URL)
→ 后端: UPDATE member_driver SET id_card = 'url1,url3' WHERE id = ?
```
### 3. 详情查看流程
```
用户点击详情
→ 后端: SELECT ..., id_card FROM member_driver WHERE id = ?
→ 前端: data.info.id_card = 'url1,url2'
→ 前端: getImageList('url1,url2') = ['url1', 'url2']
→ 页面: 显示两张身份证图片
```
## 验证要点
1.**字段存在**: `id_card` 字段已添加到 `member_driver`
2.**数据格式**: 使用英文逗号分隔多个URL
3.**前端处理**: 正确解析和显示逗号分隔的URL
4.**后端处理**: 正确接收和存储 `idCard` 参数
5.**数据库存储**: 正确存储到 `id_card` 字段
6.**数据读取**: 正确从数据库读取并返回给前端
## 测试建议
1. **新增测试**: 上传身份证前后照片,检查数据库 `id_card` 字段
2. **编辑测试**: 修改身份证照片,检查更新是否成功
3. **详情测试**: 查看详情页面是否正确显示身份证图片
4. **数据验证**: 确认URL格式正确逗号分隔正常
## 总结
**身份证前后照片地址已正确实现存储到 `id_card` 字段**
**使用英文逗号分隔多个URL地址**
**前端新增和编辑功能完整**
**后端API正确处理数据**
**数据库字段和查询已更新**
**详情页面正确显示身份证图片**
整个数据流已经完整实现,身份证前后面的照片地址会正确存储到数据库的 `id_card` 字段中,并用英文逗号分隔。

View File

@@ -1,122 +0,0 @@
# usePermissionStore 导入错误修复报告
## 问题描述
用户遇到以下错误:
```
SyntaxError: The requested module '/src/store/permission.js?t=1761099230833' does not provide an export named 'usePermissionStore' (at login.vue:58:1)
```
## 问题原因
`login.vue` 文件中使用了错误的导入语法:
```javascript
// 错误的导入方式
import { usePermissionStore } from '~/store/permission.js';
```
但在 `permission.js` 文件中,`usePermissionStore` 是作为默认导出的:
```javascript
// permission.js 文件末尾
export default usePermissionStore;
```
## 修复方案
### 修改导入语法
**文件**`pc-cattle-transportation/src/views/login.vue`
**修改前**
```javascript
import { usePermissionStore } from '~/store/permission.js';
```
**修改后**
```javascript
import usePermissionStore from '~/store/permission.js';
```
## 技术说明
### ES6 模块导入语法
1. **默认导入**
```javascript
// 导出
export default usePermissionStore;
// 导入
import usePermissionStore from './store/permission.js';
```
2. **命名导入**
```javascript
// 导出
export { usePermissionStore };
// 导入
import { usePermissionStore } from './store/permission.js';
```
### 当前项目中的使用情况
- **permission.js**:使用默认导出 `export default usePermissionStore`
- **operationPermission.vue**:正确使用默认导入 `import usePermissionStore from '@/store/permission.js'`
- **login.vue**:错误使用命名导入(已修复)
## 修复内容
### 文件:`pc-cattle-transportation/src/views/login.vue`
**第58行**:修改导入语法
```javascript
// 修复前
import { usePermissionStore } from '~/store/permission.js';
// 修复后
import usePermissionStore from '~/store/permission.js';
```
## 验证结果
### 修复前
- ❌ 导入错误:`does not provide an export named 'usePermissionStore'`
- ❌ 路由导航失败
- ❌ 用户无法正常登录
### 修复后
- ✅ 导入成功
- ✅ 路由导航正常
- ✅ 用户登录功能正常
## 相关文件
- `pc-cattle-transportation/src/store/permission.js` - 权限store定义
- `pc-cattle-transportation/src/views/login.vue` - 登录页面(已修复)
- `pc-cattle-transportation/src/views/permission/operationPermission.vue` - 权限管理页面(正确)
## 测试验证
### 测试步骤
1. **清除浏览器缓存**
2. **重新加载页面**
3. **尝试登录**
4. **检查控制台**:确认无导入错误
### 预期结果
- 无导入错误信息
- 登录功能正常
- 路由跳转正常
## 总结
通过修正导入语法,成功解决了 `usePermissionStore` 导入错误问题。修复后的系统能够:
1. **正确导入权限store**:使用默认导入语法
2. **正常执行登录流程**:无导入错误
3. **正常进行路由跳转**:权限检查正常
**修复状态**:✅ 已完成
**测试状态**:✅ 已验证
**部署状态**:✅ 可部署

View File

@@ -1,205 +0,0 @@
# 菜单权限与操作权限分离修复报告
## 问题描述
用户反映:用户"12.27新增姓名"设置了用户专属权限后,不仅操作按钮被隐藏了,连菜单也全部隐藏了。用户要求菜单权限和操作权限应该是两个独立的东西:
- **菜单权限**:控制左侧菜单栏的显示/隐藏(如"装车订单"菜单项)
- **操作权限**:控制页面内操作按钮的显示/隐藏(如"编辑"、"删除"等按钮)
## 问题根本原因
### 1. 权限类型混淆
我之前的修改将菜单权限和操作权限混淆了:
- **菜单权限**应该只包含菜单项type=0,1用于控制左侧菜单栏
- **操作权限**应该包含操作按钮type=2用于控制页面内的按钮
### 2. getUserMenus API 的问题
**文件**`tradeCattle/aiotagro-cattle-trade/src/main/java/com/aiotagro/cattletrade/business/service/impl/LoginServiceImpl.java`
**错误的修改**第147-171行
```java
@Override
public AjaxResult getUserMenus() {
// 使用修改后的权限查询逻辑(优先使用用户专属权限)
List<String> permissions = queryUserPermissions(userId, user.getRoleId());
// 根据权限查询菜单
List<SysMenu> menus;
if (permissions.contains(RoleConstants.ALL_PERMISSION)) {
// 超级管理员:返回所有菜单
menus = menuMapper.selectList(null);
} else {
// 普通用户:根据权限查询菜单
menus = menuMapper.selectMenusByPermissions(permissions);
}
}
```
**问题**`queryUserPermissions` 方法返回的是**所有权限**(包括操作按钮权限),但是 `getUserMenus` API 应该只返回**菜单权限**。
### 3. 权限查询逻辑错误
`queryUserPermissions` 方法会返回所有类型的权限(菜单+操作按钮),但是菜单权限查询应该只返回菜单项,不包含操作按钮。
## 修复方案
### 1. 分离菜单权限和操作权限查询
创建两个独立的查询方法:
- **`queryUserMenus`**:专门查询菜单权限(只包含菜单项,不包含操作按钮)
- **`queryUserPermissions`**:专门查询操作权限(包含所有权限,用于按钮控制)
### 2. 修改 getUserMenus API
**修改文件**`tradeCattle/aiotagro-cattle-trade/src/main/java/com/aiotagro/cattletrade/business/service/impl/LoginServiceImpl.java`
**修改内容**
```java
@Override
public AjaxResult getUserMenus() {
Integer userId = SecurityUtil.getCurrentUserId();
// 获取当前用户的角色ID
SysUser user = userMapper.selectById(userId);
if (user == null) {
return AjaxResult.error("用户不存在");
}
// 菜单权限查询:优先使用用户专属菜单权限,否则使用角色菜单权限
List<SysMenu> menus = queryUserMenus(userId, user.getRoleId());
log.info("=== 用户 {} 菜单查询结果,菜单数量: {}", userId, menus.size());
return AjaxResult.success("查询成功", menus);
}
```
### 3. 添加 queryUserMenus 方法
**新增方法**
```java
/**
* 查询用户菜单权限(优先使用用户专属菜单权限)
*
* @param userId 用户ID
* @param roleId 角色ID
* @return 菜单列表
*/
private List<SysMenu> queryUserMenus(Integer userId, Integer roleId) {
if (userId == null || roleId == null) {
return Collections.emptyList();
}
// 1. 先查询用户专属菜单权限(只查询菜单,不查询操作按钮)
List<SysMenu> userMenus = sysUserMenuMapper.selectMenusByUserId(userId);
if (userMenus != null && !userMenus.isEmpty()) {
// 过滤掉操作按钮type=2只保留菜单type=0,1
List<SysMenu> filteredMenus = userMenus.stream()
.filter(menu -> menu.getType() != 2) // 排除操作按钮
.collect(Collectors.toList());
if (!filteredMenus.isEmpty()) {
log.info("=== 用户 {} 使用专属菜单权限,菜单数量: {}", userId, filteredMenus.size());
return filteredMenus;
}
}
// 2. 如果没有专属菜单权限,使用角色菜单权限
if (roleId.equals(RoleConstants.SUPER_ADMIN_ROLE_ID)) {
log.info("=== 超级管理员用户 {} 使用所有菜单权限(无专属菜单权限)", userId);
return menuMapper.selectList(null);
}
// 3. 普通角色菜单权限
log.info("=== 用户 {} 使用角色菜单权限roleId: {}", userId, roleId);
return menuMapper.queryMenusByUserId(userId);
}
```
## 权限分离逻辑
### 菜单权限查询流程
```
用户登录 → getUserMenus() → queryUserMenus() → 只查询菜单项type≠2→ 控制左侧菜单栏
```
### 操作权限查询流程
```
用户登录 → queryUserPermissions() → 查询所有权限(包括操作按钮)→ 控制页面内按钮
```
## 权限类型说明
### 菜单类型type字段
- **type=0**:目录(如"系统管理"
- **type=1**:菜单(如"装车订单"
- **type=2**:按钮(如"编辑"、"删除"
### 权限查询范围
- **菜单权限**:只查询 type=0,1 的项目
- **操作权限**查询所有类型type=0,1,2的项目
## 修复效果
### 修复前
- ❌ 菜单权限和操作权限混淆
- ❌ 用户专属权限影响菜单显示
- ❌ 用户看不到任何菜单
### 修复后
- ✅ 菜单权限和操作权限分离
- ✅ 用户专属权限只影响操作按钮
- ✅ 用户能看到菜单,但操作按钮被隐藏
## 测试验证
### 测试步骤
1. **重新编译后端**`mvn clean compile`
2. **重启后端服务**`mvn spring-boot:run`
3. **清除浏览器缓存**
4. **使用"12.27新增姓名"账号重新登录**
5. **检查左侧菜单栏和页面操作按钮**
### 预期结果
- **左侧菜单栏**:用户应该能看到"装车订单"等菜单项
- **页面操作按钮**:用户不应该看到"编辑"、"删除"等操作按钮
- **权限分离**:菜单权限和操作权限独立控制
## 技术细节
### 权限数据流
```
后端菜单权限查询 → getUserMenus() → queryUserMenus() → 只返回菜单项 → 前端菜单显示
后端操作权限查询 → queryUserPermissions() → 返回所有权限 → 前端按钮控制
```
### 日志输出
修复后的日志输出示例:
```
=== 用户 3 使用专属菜单权限,菜单数量: 8
=== 用户 3 使用专属权限,权限数量: 15
```
## 相关文件
- `tradeCattle/aiotagro-cattle-trade/src/main/java/com/aiotagro/cattletrade/business/service/impl/LoginServiceImpl.java` - 权限查询逻辑分离
- `pc-cattle-transportation/src/store/permission.js` - 前端权限store
- `pc-cattle-transportation/src/directive/permission/hasPermi.js` - 前端权限检查
## 总结
通过分离菜单权限和操作权限的查询逻辑,成功解决了用户专属权限影响菜单显示的问题。修复后的系统能够:
1. **正确显示菜单**:用户专属权限不影响菜单权限
2. **正确隐藏操作按钮**:用户专属权限只影响操作权限
3. **权限分离**:菜单权限和操作权限独立控制
4. **向后兼容**:不影响现有功能
**修复状态**:✅ 已完成
**测试状态**:⏳ 待验证
**部署状态**:✅ 已部署

View File

@@ -1,317 +0,0 @@
# 菜单权限管理页面修复报告
## 问题描述
用户反映:**菜单权限管理**功能页面只需要对于菜单的隐藏管理,不需要按钮的管理。按钮的管理是操作权限的内容。
从图片可以看出,当前的"菜单权限管理"页面确实包含了按钮权限的管理,包括:
- "创建装车订单"、"编辑"、"删除"、"装车"等操作按钮
- 提示文字写着"勾选菜单和按钮后"
- 这些按钮权限的复选框可以被勾选或取消勾选
## 问题根本原因
### 1. 权限类型混淆
当前的"菜单权限管理"页面将菜单权限和按钮权限混淆了:
- **菜单权限**应该只包含菜单项type=0,1用于控制左侧菜单栏的显示/隐藏
- **按钮权限**应该包含操作按钮type=2用于控制页面内按钮的显示/隐藏
### 2. 页面功能不明确
**文件**`pc-cattle-transportation/src/views/permission/menuPermission.vue`
**问题**
- 第77行提示文字"勾选菜单和按钮后,该用户登录系统时可以访问这些菜单页面和执行相应的操作"
- 第105-111行显示按钮标签"按钮"
- 第205-220行 `loadMenuTree` 方法加载所有菜单和按钮
- 第223-238行 `loadRoleMenus` 方法加载所有权限(包括按钮)
## 修复方案
### 1. 修改提示文字
**修改前**
```html
勾选菜单和按钮后,该用户登录系统时可以访问这些菜单页面和执行相应的操作
```
**修改后**
```html
勾选菜单后,该用户登录系统时可以访问这些菜单页面。按钮权限请在"操作权限管理"页面中设置。
```
### 2. 过滤菜单树,只显示菜单项
**修改文件**`pc-cattle-transportation/src/views/permission/menuPermission.vue`
**修改内容**
```javascript
// 加载菜单树(只显示菜单,不显示按钮)
const loadMenuTree = async () => {
permissionLoading.value = true;
try {
const res = await getMenuTree();
if (res.code === 200) {
// 过滤掉按钮权限type=2只保留菜单type=0,1
const filteredTree = filterMenuTree(res.data || []);
menuTree.value = filteredTree;
console.log('=== 菜单权限管理 - 过滤后的菜单树 ===', filteredTree);
}
} catch (error) {
console.error('加载菜单树失败:', error);
ElMessage.error('加载菜单树失败');
} finally {
permissionLoading.value = false;
}
};
// 过滤菜单树只保留菜单项type=0,1移除按钮type=2
const filterMenuTree = (tree) => {
return tree.map(node => {
const filteredNode = { ...node };
// 如果当前节点是按钮type=2返回null会被过滤掉
if (node.type === 2) {
return null;
}
// 如果有子节点,递归过滤
if (node.children && node.children.length > 0) {
const filteredChildren = filterMenuTree(node.children).filter(child => child !== null);
filteredNode.children = filteredChildren;
}
return filteredNode;
}).filter(node => node !== null);
};
```
### 3. 过滤菜单权限,只加载菜单权限
**修改内容**
```javascript
// 加载角色已分配的菜单(只加载菜单权限,不加载按钮权限)
const loadRoleMenus = async (roleId) => {
try {
const res = await getRoleMenuIds(roleId);
if (res.code === 200) {
const allMenuIds = res.data || [];
// 过滤掉按钮权限,只保留菜单权限
const menuOnlyIds = await filterMenuOnlyIds(allMenuIds);
checkedMenuIds.value = menuOnlyIds;
console.log('=== 菜单权限管理 - 过滤后的菜单权限 ===', {
allMenuIds: allMenuIds,
menuOnlyIds: menuOnlyIds
});
await nextTick();
if (menuTreeRef.value) {
menuTreeRef.value.setCheckedKeys(checkedMenuIds.value);
}
}
} catch (error) {
console.error('加载角色菜单失败:', error);
ElMessage.error('加载角色菜单失败');
}
};
// 过滤菜单ID列表只保留菜单项type=0,1移除按钮type=2
const filterMenuOnlyIds = async (menuIds) => {
try {
// 获取所有菜单信息
const menuListRes = await getMenuList();
if (menuListRes.code !== 200) {
return menuIds; // 如果获取失败,返回原始列表
}
const allMenus = menuListRes.data || [];
const menuMap = new Map(allMenus.map(menu => [menu.id, menu]));
// 过滤掉按钮权限
const menuOnlyIds = menuIds.filter(id => {
const menu = menuMap.get(id);
return menu && menu.type !== 2; // 只保留菜单项type=0,1
});
return menuOnlyIds;
} catch (error) {
console.error('过滤菜单权限失败:', error);
return menuIds; // 如果过滤失败,返回原始列表
}
};
```
### 4. 修改保存逻辑,只保存菜单权限
**修改内容**
```javascript
// 保存菜单权限(只保存菜单权限,不保存按钮权限)
const handleSaveMenuPermissions = async () => {
if (!currentRole.value) {
ElMessage.warning('请先选择用户');
return;
}
// 获取选中的节点(包括半选中的父节点)
const checkedKeys = menuTreeRef.value.getCheckedKeys();
const halfCheckedKeys = menuTreeRef.value.getHalfCheckedKeys();
const allKeys = [...checkedKeys, ...halfCheckedKeys];
// 过滤掉按钮权限,只保留菜单权限
const menuOnlyIds = await filterMenuOnlyIds(allKeys);
console.log('=== 保存菜单权限 ===', {
user: currentRole.value,
allKeys: allKeys,
menuOnlyIds: menuOnlyIds
});
saveLoading.value = true;
try {
const res = await assignRoleMenus({
roleId: currentRole.value.roleId,
menuIds: menuOnlyIds, // 只保存菜单权限
});
if (res.code === 200) {
ElMessage.success(`菜单权限保存成功,共保存 ${menuOnlyIds.length} 个菜单权限`);
} else {
ElMessage.error(res.msg || '保存失败');
}
} catch (error) {
console.error('保存菜单权限失败:', error);
ElMessage.error('保存失败');
} finally {
saveLoading.value = false;
}
};
```
### 5. 修改一键分配功能,只分配菜单权限
**修改内容**
```javascript
// 一键分配全部菜单权限(只分配菜单权限,不分配按钮权限)
const handleQuickAssignAll = async () => {
// ... 确认对话框修改为只分配菜单权限
// 获取所有菜单
const menuListRes = await getMenuList();
if (menuListRes.code !== 200) {
throw new Error('获取菜单列表失败');
}
const allMenus = menuListRes.data || [];
// 过滤掉按钮权限,只保留菜单权限
const menuOnlyMenus = allMenus.filter(menu => menu.type !== 2);
const menuOnlyIds = menuOnlyMenus.map(menu => menu.id);
console.log('=== 一键分配全部菜单权限 ===', {
user: currentRole.value,
totalMenus: allMenus.length,
menuOnlyMenus: menuOnlyMenus.length,
menuOnlyIds: menuOnlyIds
});
// 分配所有菜单权限
const res = await assignRoleMenus({
roleId: currentRole.value.roleId,
menuIds: menuOnlyIds,
});
if (res.code === 200) {
ElMessage.success(`成功为用户 ${currentRole.value.name} 分配了 ${menuOnlyIds.length} 个菜单权限`);
// 重新加载权限显示
await loadRoleMenus(currentRole.value.roleId);
} else {
ElMessage.error(res.msg || '分配失败');
}
};
```
### 6. 修改按钮文字
**修改内容**
```html
<!-- 修改前 -->
一键分配全部权限
<!-- 修改后 -->
一键分配全部菜单权限
```
## 修复效果
### 修复前
- ❌ 菜单权限管理页面包含按钮权限
- ❌ 用户可以勾选/取消勾选按钮权限
- ❌ 提示文字混淆了菜单和按钮权限
- ❌ 保存时会保存按钮权限
### 修复后
- ✅ 菜单权限管理页面只显示菜单项
- ✅ 用户只能管理菜单权限,不能管理按钮权限
- ✅ 提示文字明确说明菜单权限和按钮权限的区别
- ✅ 保存时只保存菜单权限
- ✅ 按钮权限管理在"操作权限管理"页面中
## 权限分离逻辑
### 菜单权限管理页面
```
用户选择 → 加载菜单树(过滤掉按钮) → 显示菜单项 → 保存菜单权限
```
### 操作权限管理页面
```
用户选择 → 加载权限树(包含按钮) → 显示操作按钮 → 保存操作权限
```
## 权限类型说明
### 菜单类型type字段
- **type=0**:目录(如"系统管理"
- **type=1**:菜单(如"装车订单"
- **type=2**:按钮(如"编辑"、"删除"
### 权限管理范围
- **菜单权限管理**:只管理 type=0,1 的项目
- **操作权限管理**管理所有类型type=0,1,2的项目
## 测试验证
### 测试步骤
1. **清除浏览器缓存**
2. **访问"菜单权限管理"页面**
3. **选择用户,检查权限树**
4. **验证只显示菜单项,不显示按钮**
5. **保存权限,验证只保存菜单权限**
### 预期结果
- **菜单权限管理页面**只显示菜单项type=0,1不显示按钮type=2
- **操作权限管理页面**:显示所有权限(包括按钮)
- **权限分离**:菜单权限和按钮权限独立管理
## 相关文件
- `pc-cattle-transportation/src/views/permission/menuPermission.vue` - 菜单权限管理页面
- `pc-cattle-transportation/src/views/permission/operationPermission.vue` - 操作权限管理页面
## 总结
通过过滤菜单树和权限数据,成功将菜单权限管理页面与按钮权限管理分离。修复后的系统能够:
1. **明确权限范围**:菜单权限管理只管理菜单项
2. **清晰的功能分工**:菜单权限和按钮权限独立管理
3. **用户友好的提示**:明确说明权限管理的范围
4. **数据一致性**:确保只保存相应的权限类型
**修复状态**:✅ 已完成
**测试状态**:⏳ 待验证
**部署状态**:✅ 已部署

View File

@@ -1,183 +0,0 @@
# 菜单权限管理角色影响范围修复报告
## 问题描述
用户反映:修改用户"12.27新增姓名"的菜单权限时,同时修改了超级管理员的菜单权限。
从控制台日志可以看出:
```
menuPermission.vue:321 === 保存菜单权限 === {user: Proxy(Object), allKeys: Array(19), menuOnlyIds: Array(19)}
menuPermission.vue:255 === 菜单权限管理 - 过滤后的菜单权限 === {allMenuIds: Array(19), menuOnlyIds: Array(19)}
```
## 问题根本原因
### 1. 基于角色的权限管理RBAC
当前的菜单权限管理使用的是**基于角色的权限管理RBAC**,而不是基于用户的权限管理:
- 用户"12.27新增姓名"的 `roleId=1`
- 超级管理员的 `roleId=1`
- 两个用户使用相同的角色ID
### 2. 权限修改影响范围
当修改菜单权限时:
1. 前端调用 `assignRoleMenus` API
2. 后端修改 `sys_role_menu` 表中 `roleId=1` 的记录
3. 所有使用 `roleId=1` 的用户权限都被更新
4. 包括"超级管理员"在内的所有 `roleId=1` 用户都受到影响
### 3. 用户界面缺乏明确提示
原来的界面没有明确说明这是基于角色的权限管理,用户可能误以为这是用户级别的权限管理。
## 修复方案
### 1. 明确标识基于角色的权限管理
**修改文件**`pc-cattle-transportation/src/views/permission/menuPermission.vue`
**修改内容**
- 添加角色ID显示标签
- 修改提示文字,明确说明这是基于角色的权限管理
- 详细说明影响范围
### 2. 添加详细的警告提示
**修改前**
```html
<el-alert title="提示" type="info">
勾选菜单后,该用户登录系统时可以访问这些菜单页面。按钮权限请在"操作权限管理"页面中设置。
</el-alert>
```
**修改后**
```html
<el-alert title="重要提示 - 基于角色的菜单权限管理" type="warning">
<template #default>
<div>
<p><strong>当前系统使用基于角色的菜单权限管理RBAC</strong></p>
<p>• 修改菜单权限会影响所有使用相同角色ID的用户</p>
<p>• 当前用户角色ID: <strong>{{ currentRole.roleId }}</strong></p>
<p>• 所有角色ID为 <strong>{{ currentRole.roleId }}</strong> 的用户都会受到影响</p>
<p>• 勾选菜单后,该角色可以访问相应的菜单页面</p>
<p>• 按钮权限请在"操作权限管理"页面中设置</p>
</div>
</template>
</el-alert>
```
### 3. 添加确认对话框
**修改内容**
```javascript
// 保存菜单权限时添加确认对话框
const handleSaveMenuPermissions = async () => {
// 确认对话框,让用户明确知道影响范围
try {
await ElMessageBox.confirm(
`您即将修改角色ID为 ${currentRole.value.roleId} 的菜单权限设置。\n\n这将影响所有使用该角色的用户包括\n• ${currentRole.value.name}\n• 其他使用相同角色ID的用户\n\n确定要继续吗`,
'确认菜单权限修改',
{
confirmButtonText: '确定修改',
cancelButtonText: '取消',
type: 'warning',
dangerouslyUseHTMLString: false
}
);
} catch {
// 用户取消操作
return;
}
// ... 保存逻辑
};
```
### 4. 修改成功提示信息
**修改前**
```javascript
ElMessage.success(`菜单权限保存成功,共保存 ${menuOnlyIds.length} 个菜单权限`);
```
**修改后**
```javascript
ElMessage.success(`角色ID ${currentRole.value.roleId} 的菜单权限保存成功,共保存 ${menuOnlyIds.length} 个菜单权限。所有使用该角色的用户都会受到影响。`);
```
## 修复效果
### 修复前
- ❌ 用户不知道这是基于角色的权限管理
- ❌ 用户不知道修改会影响其他用户
- ❌ 缺乏明确的警告提示
- ❌ 成功提示信息不明确
### 修复后
- ✅ 明确标识基于角色的权限管理
- ✅ 详细说明影响范围
- ✅ 添加确认对话框
- ✅ 明确成功提示信息
## 技术说明
### 权限管理架构
**当前系统架构**
```
用户 → 角色 → 权限
```
**权限修改流程**
```
修改权限 → 更新角色权限 → 影响所有使用该角色的用户
```
### 用户影响范围
**用户"12.27新增姓名"和超级管理员**
- 用户ID3 vs 11
- 角色ID1 vs 1相同
- 权限来源:角色权限(相同)
**修改影响**
- 修改角色ID=1的权限
- 影响所有roleId=1的用户
- 包括"12.27新增姓名"和"超级管理员"
## 测试验证
### 测试步骤
1. **访问菜单权限管理页面**
2. **选择用户"12.27新增姓名"**
3. **检查警告提示和角色ID显示**
4. **尝试修改权限,检查确认对话框**
5. **验证成功提示信息**
### 预期结果
- **警告提示**:明确说明基于角色的权限管理
- **角色ID显示**显示当前用户的角色ID
- **确认对话框**:明确说明影响范围
- **成功提示**:明确说明影响范围
## 相关文件
- `pc-cattle-transportation/src/views/permission/menuPermission.vue` - 菜单权限管理页面
- `pc-cattle-transportation/src/views/permission/operationPermission.vue` - 操作权限管理页面(支持用户专属权限)
## 总结
通过添加明确的警告提示和确认对话框,成功解决了用户对权限管理机制理解不清的问题。修复后的系统能够:
1. **明确权限管理机制**:清楚说明基于角色的权限管理
2. **详细说明影响范围**:明确告知用户修改会影响哪些用户
3. **提供确认机制**:让用户在修改前确认影响范围
4. **清晰的反馈**:成功提示明确说明影响范围
**修复状态**:✅ 已完成
**测试状态**:⏳ 待验证
**部署状态**:✅ 已部署
**注意**这是基于角色的权限管理RBAC的正常行为。如果需要用户级别的权限管理需要实施基于用户的权限管理UBAC系统。

View File

@@ -1,251 +0,0 @@
# 缺失路由修复报告
## 问题描述
用户报告了多个Vue Router路径匹配错误
1. `[Vue Router warn]: No match found for location with path "/system/tenant"`
2. `[Vue Router warn]: No match found for location with path "/hardware/eartag"`
3. `[Vue Router warn]: No match found for location with path "/hardware/host"`
4. `[Vue Router warn]: No match found for location with path "/hardware/collar"`
5. `[Vue Router warn]: No match found for location with path "/shipping/shippinglist"`
6. `[Vue Router warn]: No match found for location with path "/earlywarning/earlywarninglist"`
## 根本原因
这些错误是由于前端路由配置文件中缺少对应的路由定义导致的。虽然相应的Vue组件存在但Vue Router无法找到匹配的路由配置。
## 修复方案
### 1. 添加系统管理路由
**文件**: `pc-cattle-transportation/src/router/index.ts`
添加了以下系统管理子路由:
```typescript
// 系统管理路由
{
path: '/system',
component: LayoutIndex,
meta: {
title: '系统管理',
keepAlive: true,
requireAuth: true,
},
children: [
{
path: 'post',
name: 'Post',
meta: {
title: '岗位管理',
keepAlive: true,
requireAuth: true,
},
component: () => import('~/views/system/post.vue'),
},
{
path: 'staff',
name: 'Staff',
meta: {
title: '员工管理',
keepAlive: true,
requireAuth: true,
},
component: () => import('~/views/system/staff.vue'),
},
{
path: 'tenant',
name: 'Tenant',
meta: {
title: '租户管理',
keepAlive: true,
requireAuth: true,
},
component: () => import('~/views/system/tenant.vue'),
},
{
path: 'user',
name: 'SystemUser',
meta: {
title: '系统用户管理',
keepAlive: true,
requireAuth: true,
},
component: () => import('~/views/system/user.vue'),
},
{
path: 'menu',
name: 'SystemMenu',
meta: {
title: '系统菜单管理',
keepAlive: true,
requireAuth: true,
},
component: () => import('~/views/system/menu.vue'),
},
],
},
```
### 2. 添加入境检疫路由
添加了入境检疫认证路由:
```typescript
// 入境检疫路由
{
path: '/entry',
component: LayoutIndex,
meta: {
title: '入境检疫',
keepAlive: true,
requireAuth: true,
},
children: [
{
path: '/entry/details',
name: 'details',
meta: {
title: '详情',
keepAlive: true,
requireAuth: false,
},
component: () => import('~/views/entry/details.vue'),
},
{
path: 'attestation',
name: 'Attestation',
meta: {
title: '入境检疫认证',
keepAlive: true,
requireAuth: true,
},
component: () => import('~/views/entry/attestation.vue'),
},
],
},
```
### 3. 添加运送清单路由
添加了运送清单路由映射到现有的loadingOrder组件
```typescript
// 运送清单路由
{
path: '/shipping',
component: LayoutIndex,
meta: {
title: '运送清单',
keepAlive: true,
requireAuth: true,
},
children: [
{
path: 'loadingOrder',
name: 'LoadingOrder',
meta: {
title: '装车订单',
keepAlive: true,
requireAuth: true,
},
component: () => import('~/views/shipping/loadingOrder.vue'),
},
{
path: 'shippinglist',
name: 'ShippingList',
meta: {
title: '运送清单',
keepAlive: true,
requireAuth: true,
},
component: () => import('~/views/shipping/loadingOrder.vue'),
},
],
},
```
### 4. 添加预警管理路由
添加了预警管理路由:
```typescript
// 预警管理路由
{
path: '/earlywarning',
component: LayoutIndex,
meta: {
title: '预警管理',
keepAlive: true,
requireAuth: true,
},
children: [
{
path: 'earlywarninglist',
name: 'EarlyWarningList',
meta: {
title: '预警列表',
keepAlive: true,
requireAuth: true,
},
component: () => import('~/views/earlywarning/list.vue'),
},
],
},
```
### 5. 硬件管理路由
硬件管理路由已经存在且配置正确,包括:
- `/hardware/collar` - 智能项圈
- `/hardware/eartag` - 智能耳标
- `/hardware/host` - 智能主机
## 修复结果
### 已修复的路由
1.`/system/tenant` - 租户管理
2.`/system/user` - 系统用户管理
3.`/system/menu` - 系统菜单管理
4.`/entry/attestation` - 入境检疫认证
5.`/shipping/shippinglist` - 运送清单
6.`/earlywarning/earlywarninglist` - 预警列表
### 硬件路由状态
硬件管理路由配置正确,包括:
-`/hardware/collar` - 智能项圈
-`/hardware/eartag` - 智能耳标
-`/hardware/host` - 智能主机
## 验证
- ✅ 所有路由配置语法正确
- ✅ 对应的Vue组件文件存在
- ✅ 路由路径格式正确(以/开头)
- ✅ 组件导入路径正确
## 注意事项
1. **路由命名**: 确保每个路由都有唯一的name属性
2. **组件映射**: 所有路由都正确映射到对应的Vue组件
3. **权限控制**: 所有路由都设置了适当的权限要求
4. **向后兼容**: 保持了现有路由配置不变
## 测试建议
建议测试以下路径的导航:
1. `/system/tenant` - 租户管理页面
2. `/system/user` - 系统用户管理页面
3. `/system/menu` - 系统菜单管理页面
4. `/entry/attestation` - 入境检疫认证页面
5. `/shipping/shippinglist` - 运送清单页面
6. `/earlywarning/earlywarninglist` - 预警列表页面
7. `/hardware/collar` - 智能项圈页面
8. `/hardware/eartag` - 智能耳标页面
9. `/hardware/host` - 智能主机页面
所有路由现在都应该能够正确导航,不再出现"No match found"警告。

View File

@@ -1,152 +0,0 @@
# 权限管理机制说明
## 问题描述
用户反映:修改"12.27新增姓名"这个用户的操作权限时,超级管理员的权限也会被修改。
## 根本原因
当前系统使用的是**基于角色的权限管理RBAC - Role-Based Access Control**而不是基于用户的权限管理UBAC - User-Based Access Control
### 权限架构分析
1. **数据库表结构**
- `sys_role_menu` 表:存储角色-菜单权限关系
- `sys_user` 表:存储用户信息,包含 `roleId` 字段
- **没有** `sys_user_menu` 表:不存在用户-菜单权限关系
2. **权限分配机制**
- 权限分配基于 `roleId`角色ID
- 所有使用相同 `roleId` 的用户共享相同的权限
- 修改权限时影响整个角色,不是单个用户
3. **权限获取流程**
```java
// LoginServiceImpl.java 第124行
List<String> permissions = queryUserPermissions(user.getRoleId());
```
- 用户登录时,根据用户的 `roleId` 获取权限
- 不是根据用户的 `userId` 获取权限
## 具体案例分析
### 用户信息对比
| 用户 | 用户ID | 角色ID | 权限来源 |
|------|--------|--------|----------|
| 12.27新增姓名 | 3 | 1 | roleId=1 的权限 |
| 超级管理员 | 11 | 1 | roleId=1 的权限 |
### 权限修改影响
当修改"12.27新增姓名"的权限时:
1. 前端发送请求:`{ roleId: 1, menuIds: [...] }`
2. 后端更新 `sys_role_menu` 表中 `roleId=1` 的记录
3. 所有使用 `roleId=1` 的用户权限都被更新
4. 包括"超级管理员"在内的所有 `roleId=1` 用户都受到影响
## 解决方案
### 方案1修改为基于用户的权限管理推荐但复杂
需要:
1. 创建 `sys_user_menu` 表
2. 修改后端API支持用户级别权限
3. 修改权限查询逻辑
4. 修改前端界面
### 方案2明确显示角色权限管理已实施
已实施的改进:
1. **界面标识**:添加"基于角色权限"标签
2. **角色ID显示**在用户列表中显示角色ID
3. **警告提示**:明确说明影响范围
4. **确认对话框**:保存前确认影响范围
5. **成功提示**:明确说明影响范围
## 修改内容
### 前端界面改进
1. **用户列表**
- 添加"基于角色权限"标签
- 显示角色ID列
2. **权限分配区域**
- 标题改为"角色权限分配"
- 显示当前角色ID
- 添加详细的警告提示
3. **保存确认**
- 添加确认对话框
- 明确说明影响范围
- 成功提示包含影响范围
### 警告信息内容
```
重要提示 - 基于角色的权限管理
• 当前系统使用基于角色的权限管理RBAC
• 修改权限会影响所有使用相同角色ID的用户
• 当前用户角色ID: 1
• 所有角色ID为 1 的用户都会受到影响
• 勾选操作权限后,该角色可以执行相应的按钮操作(新增、编辑、删除等)
```
## 建议
1. **短期解决方案**:使用当前的界面改进,让用户明确知道这是角色权限管理
2. **长期解决方案**:考虑实施基于用户的权限管理,但这需要较大的系统改造
3. **用户培训**:向用户说明权限管理机制,避免误解
## 技术细节
### 权限查询流程
```java
// 用户登录时获取权限
List<String> permissions = queryUserPermissions(user.getRoleId());
// 权限查询方法
private List<String> queryUserPermissions(Integer roleId) {
// 查询角色关联的菜单权限
List<SysMenu> menus = menuMapper.selectMenusByRoleId(roleId);
return menus.stream()
.filter(menu -> StringUtils.isNotEmpty(menu.getAuthority()))
.map(SysMenu::getAuthority)
.distinct()
.collect(Collectors.toList());
}
```
### 权限分配流程
```java
// 分配角色菜单权限
@PostMapping("/assignRoleMenus")
public AjaxResult assignRoleMenus(@RequestBody Map<String, Object> params) {
Integer roleId = (Integer) params.get("roleId");
List<Integer> menuIds = (List<Integer>) params.get("menuIds");
// 删除原有权限
sysRoleMenuMapper.delete(
new LambdaQueryWrapper<SysRoleMenu>()
.eq(SysRoleMenu::getRoleId, roleId)
);
// 添加新权限
for (Integer menuId : menuIds) {
SysRoleMenu roleMenu = new SysRoleMenu();
roleMenu.setRoleId(roleId);
roleMenu.setMenuId(menuId);
sysRoleMenuMapper.insert(roleMenu);
}
return AjaxResult.success("分配成功");
}
```
## 总结
这个问题的根本原因是系统使用基于角色的权限管理,而不是基于用户的权限管理。当修改权限时,影响的是整个角色,而不是单个用户。通过界面改进,现在用户可以清楚地了解权限管理机制和影响范围。

View File

@@ -1,329 +0,0 @@
# 牛只运输管理系统需求文档
## 1. 引言
### 1.1 项目背景
随着畜牧业的快速发展,牛只运输已成为产业链中的重要环节。为提高牛只运输过程的管理效率和安全性,需要建立一套完整的牛只运输管理系统。该系统将实现对牛只运输全过程的数字化管理,包括运输管理、检疫隔离、设备监控、异常预警等功能。
### 1.2 项目目标
本项目旨在开发一套基于Web的牛只运输管理系统为牛只运输企业提供完整的数字化解决方案实现以下目标
- 提高运输过程的可视化程度
- 加强运输过程的安全监控
- 优化运输计划和路线规划
- 完善检疫和隔离管理流程
- 提升异常情况的响应速度
### 1.3 项目范围
本系统主要面向以下用户群体:
- 牛只供应商
- 牛只采购商
- 牛只采购供应链资金提供方
- 牛只运输管理人员
- 检疫和隔离管理人员
- 硬件设备维护人员
- 系统管理员
系统将涵盖运输管理、检疫隔离、设备监控、预警系统等核心功能模块,并提供完整的数据统计和分析功能,支持多角色权限管理和移动端访问。
## 2. 项目概述
### 2.1 产品描述
牛只运输管理系统是一套基于Vue 3 + TypeScript开发的现代化前端应用通过与后端服务配合实现对牛只运输全过程的数字化管理。系统提供友好的用户界面支持多角色权限管理具备实时监控、数据分析、预警提醒等功能。
### 2.2 产品功能概览
- **用户管理**:用户登录/注册、权限管理、用户信息管理
- **运输管理**:运输计划制定、路线规划、状态监控、数据统计
- **检疫和隔离管理**:检疫记录、隔离状态监控、检疫证书管理
- **硬件设备管理**:设备状态监控、数据采集、设备维护
- **预警系统**:实时监控预警、异常情况报警、规则配置
- **系统管理**:配置管理、日志管理、数据备份
- **数据录入管理**:入境检疫数据录入、核验管理
- **用户管理**:司机管理、用户管理
### 2.3 用户特征
1. **运输管理人员**:负责制定运输计划、监控运输过程、查看统计数据
2. **检疫管理人员**:负责检疫记录管理、隔离状态监控、证书管理
3. **设备维护人员**:负责监控设备状态、处理设备异常、维护设备信息
4. **系统管理员**:负责用户管理、权限配置、系统配置、日志管理
5. **司机用户**:查看运输任务、更新运输状态
### 2.4 运行环境
- **客户端**现代浏览器Chrome、Firefox、Safari等
- **服务端**需要与后端API服务配合运行
- **网络环境**:稳定的互联网连接
## 3. 功能需求
### 3.1 用户管理模块
#### 3.1.1 用户登录
- 支持手机号+密码登录
- 支持手机号+验证码登录
- 登录失败次数限制
- 登录状态保持
#### 3.1.2 权限管理
- 基于角色的访问控制RBAC
- 菜单权限控制
- 按钮级别权限控制
- 动态路由生成
#### 3.1.3 用户信息管理
- 个人信息查看和修改
- 密码修改
- 头像上传
#### 3.1.4 系统用户管理
- 用户列表查看
- 用户新增/编辑/删除
- 用户状态管理
#### 3.1.5 司机管理
- 司机列表查看
- 司机新增/编辑/删除
- 司机详情查看
### 3.2 运输管理模块
#### 3.2.1 运输计划制定
- 运输任务创建
- 运输路线规划
- 运输时间安排
- 运输车辆分配
#### 3.2.2 运输路线规划
- 基于百度地图的路线规划
- 路线优化建议
- 实时路线跟踪
#### 3.2.3 运输状态监控
- 实时位置跟踪
- 运输状态更新
- 异常情况记录
#### 3.2.4 运输数据统计
- 运输任务统计
- 运输效率分析
- 成本统计分析
#### 3.2.5 装车管理
- 装车任务分配
- 装车状态跟踪
- 装车数据记录
#### 3.2.6 运单管理
- 运单创建和编辑
- 运单详情查看
- 运单状态更新
### 3.3 检疫和隔离管理模块
#### 3.3.1 检疫记录管理
- 检疫信息录入
- 检疫结果记录
- 检疫证书生成
#### 3.3.2 隔离状态监控
- 隔离牛只信息管理
- 隔离状态跟踪
- 隔离结束处理
#### 3.3.3 检疫证书管理
- 证书模板管理
- 证书生成和下载
- 证书查询和验证
#### 3.3.4 入境检疫管理
- 入境检疫数据录入
- 检疫核验管理
- 检疫文件下载
### 3.4 硬件设备管理模块
#### 3.4.1 设备状态监控
- 设备在线状态监控
- 设备数据实时展示
- 设备异常报警
#### 3.4.2 设备数据采集
- 传感器数据采集
- 数据存储和查询
- 数据可视化展示
#### 3.4.3 设备维护管理
- 设备维护计划制定
- 维护记录管理
- 设备故障处理
#### 3.4.4 项圈设备管理
- 项圈设备列表查看
- 项圈设备分配
- 项圈设备状态监控
#### 3.4.5 耳标设备管理
- 耳标设备列表查看
- 耳标设备分配
- 耳标设备状态监控
#### 3.4.6 主机设备管理
- 主机设备列表查看
- 主机设备状态监控
### 3.5 预警系统模块
#### 3.5.1 实时监控预警
- 运输异常预警
- 设备故障预警
- 环境参数异常预警
#### 3.5.2 异常情况报警
- 多渠道报警通知(短信、邮件、站内信)
- 报警级别分类
- 报警处理跟踪
#### 3.5.3 预警规则配置
- 预警条件设置
- 预警阈值配置
- 预警通知方式配置
### 3.6 系统管理模块
#### 3.6.1 系统配置
- 系统参数配置
- 字典数据管理
- 通知模板配置
#### 3.6.2 日志管理
- 操作日志记录
- 登录日志记录
- 系统日志查看
#### 3.6.3 数据备份
- 数据备份策略配置
- 手动备份功能
- 备份文件管理
#### 3.6.4 岗位管理
- 岗位列表查看
- 岗位新增/编辑/删除
- 岗位权限配置
#### 3.6.5 员工管理
- 员工列表查看
- 员工新增/编辑/删除
- 员工岗位分配
#### 3.6.6 租户管理
- 租户列表查看
- 租户新增/编辑
- 租户设备分配
## 4. 非功能需求
### 4.1 性能需求
- 页面加载时间不超过3秒
- 数据查询响应时间不超过1秒
- 支持至少1000个并发用户访问
### 4.2 可用性需求
- 系统可用性达到99.9%
- 提供友好的用户界面
- 支持主流浏览器
### 4.3 安全性需求
- 用户身份认证和授权
- 数据传输加密
- 敏感信息保护
- 防止SQL注入和XSS攻击
### 4.4 兼容性需求
- 支持Chrome、Firefox、Safari等主流浏览器
- 支持不同分辨率屏幕显示
- 支持移动端访问
### 4.5 可维护性需求
- 代码结构清晰,注释完整
- 模块化设计,便于扩展
- 提供完善的日志记录
## 5. 外部接口需求
### 5.1 用户接口
- 响应式Web界面
- 支持键盘和鼠标操作
- 提供快捷键支持
### 5.2 硬件接口
- GPS设备数据接口
- 传感器数据接口
- 视频监控设备接口
### 5.3 软件接口
- 后端API接口
- 百度地图API
- 短信服务接口
- 邮件服务接口
## 6. 其他需求
### 6.1 国际化需求
- 支持中英文切换
- 日期时间格式本地化
- 数字格式本地化
### 6.2 数据备份和恢复
- 定期自动备份
- 手动备份功能
- 数据恢复功能
### 6.3 技术支持和维护
- 在线帮助文档
- 系统使用培训
- 技术支持服务
## 7. 项目约束
### 7.1 技术约束
- 基于Vue 3 + TypeScript技术栈
- 使用Vite构建工具
- 遵循前端开发规范
### 7.2 数据约束
- 数据存储符合相关法规要求
- 数据传输符合安全标准
- 数据备份符合行业标准
### 7.3 时间约束
- 项目开发周期为6个月
- 分阶段交付功能模块
- 需要预留测试和优化时间
## 8. 验收标准
### 8.1 功能验收标准
- 所有功能模块按需求文档实现
- 功能测试通过率达到100%
- 用户验收测试通过
### 8.2 性能验收标准
- 系统响应时间符合要求
- 并发处理能力达标
- 资源占用率在合理范围内
### 8.3 安全验收标准
- 通过安全测试
- 无高危安全漏洞
- 符合数据保护法规要求
## 9. 附录
### 9.1 术语表
- **牛只运输**:指将牛只从一个地点运输到另一个地点的过程
- **检疫**:指对牛只进行疫病检查的过程
- **隔离**:指对疑似或确诊患病牛只进行隔离观察的过程
- **RFID**:射频识别技术,用于牛只身份识别
### 9.2 参考资料
- 相关行业标准和规范
- 技术文档和API说明
- 法律法规要求

View File

@@ -1,115 +0,0 @@
# Vue Router 路由警告修复报告
## 问题描述
用户登录时出现Vue Router警告
```
[Vue Router warn]: No match found for location with path "/system/post"
```
## 问题原因
1. **路由跳转时机问题**:在动态路由完全生成之前就尝试跳转
2. **路由生成异步性**权限store的路由生成是异步的但跳转是同步的
3. **路由匹配失败**Vue Router找不到匹配 `/system/post` 的路由
## 修复方案
### 1. 添加路由生成等待机制
**修改文件**`pc-cattle-transportation/src/views/login.vue`
**添加内容**
```javascript
// 确保权限store的路由生成完成
const permissionStore = usePermissionStore();
if (!permissionStore.routeFlag) {
console.log('=== 等待路由生成完成 ===');
await permissionStore.generateRoutes();
}
```
### 2. 使用replace替代push
**修改内容**
```javascript
// 使用replace而不是push避免路由警告
await router.replace({ path: targetPath });
```
**原因**
- `replace` 不会在浏览器历史中留下记录
- 避免路由冲突和警告
- 更适合登录后的页面跳转
### 3. 添加必要的导入
**添加导入**
```javascript
import { usePermissionStore } from '~/store/permission.js';
```
## 修复内容
### 文件:`pc-cattle-transportation/src/views/login.vue`
1. **第58行**添加权限store导入
2. **第250-255行**:添加路由生成等待逻辑
3. **第260行**:使用 `router.replace` 替代 `router.push`
## 技术说明
### 路由生成流程
1. 用户登录成功
2. 获取用户菜单和权限
3. 生成动态路由
4. 等待路由完全添加
5. 执行页面跳转
### 修复原理
- **等待机制**:确保动态路由完全生成后再跳转
- **replace方法**:避免路由历史冲突
- **错误处理**:提供降级方案
## 验证结果
### 修复前
- ❌ 出现路由警告
- ❌ 可能跳转失败
- ❌ 用户体验不佳
### 修复后
- ✅ 无路由警告
- ✅ 跳转成功
- ✅ 用户体验良好
## 测试验证
### 测试步骤
1. **清除浏览器缓存**
2. **重新登录系统**
3. **检查控制台**:确认无路由警告
4. **验证跳转**:确认正确跳转到目标页面
### 预期结果
- 超级管理员登录后跳转到 `/system/post`
- 普通用户跳转到第一个有权限的菜单页面
- 无Vue Router警告信息
## 相关文件
- `pc-cattle-transportation/src/views/login.vue` - 登录页面
- `pc-cattle-transportation/src/store/permission.js` - 权限store
- `pc-cattle-transportation/src/router/index.ts` - 路由配置
## 总结
通过添加路由生成等待机制和使用 `replace` 方法成功解决了Vue Router的路由警告问题。修复后的系统能够
1. **正确等待路由生成**:确保动态路由完全添加后再跳转
2. **避免路由冲突**使用replace方法避免历史记录冲突
3. **提供良好体验**:用户登录后能够正确跳转到目标页面
**修复状态**:✅ 已完成
**测试状态**:✅ 已验证
**部署状态**:✅ 可部署

View File

@@ -1,204 +0,0 @@
# 路由和权限问题修复报告
## 问题描述
在登录后出现了两个关键问题:
1. **权限路由生成错误**
```
TypeError: Cannot read properties of null (reading 'replace')
at capitalizeFirstLetter (permission.js:100:21)
```
2. **无限重定向问题**
```
[Vue Router warn]: Detected a possibly infinite redirection in a navigation guard when going from "/login" to "/shipping/loadingOrder"
```
## 根本原因分析
### 1. capitalizeFirstLetter 函数错误
- **原因**:菜单数据中的 `routeUrl` 字段可能为 `null` 或 `undefined`
- **影响**:导致权限路由生成失败,系统无法正常加载
### 2. 无限重定向问题
- **原因**:路由守卫和登录后的导航逻辑产生冲突
- **影响**:用户无法正常进入系统,页面不断重定向
## 修复方案
### 1. 修复 capitalizeFirstLetter 函数
**文件**`src/store/permission.js`
**修复前**
```javascript
function capitalizeFirstLetter(string) {
string = string.replace('/', '');
return string.charAt(0).toUpperCase() + string.toLowerCase().slice(1);
}
```
**修复后**
```javascript
function capitalizeFirstLetter(string) {
// 处理 null 或 undefined 值
if (!string || typeof string !== 'string') {
console.warn('capitalizeFirstLetter: Invalid string input:', string);
return 'Unknown';
}
string = string.replace('/', '');
return string.charAt(0).toUpperCase() + string.toLowerCase().slice(1);
}
```
### 2. 改进菜单数据处理
**修复前**
```javascript
menuList = menuList.map((item) => {
return {
name: capitalizeFirstLetter(item.routeUrl),
path: item.routeUrl,
// ...
};
});
```
**修复后**
```javascript
menuList = menuList.map((item) => {
// 确保 routeUrl 存在且不为空
const routeUrl = item.routeUrl || item.pageUrl || '';
return {
name: capitalizeFirstLetter(routeUrl),
path: routeUrl,
// ...
};
});
```
### 3. 修复登录导航逻辑
**文件**`src/views/login.vue`
**修复前**
```javascript
const generateRoutes = () => {
getUserMenu().then((ret) => {
// 复杂的 Promise 链和错误处理
router.push({ path: targetPath }).catch((error) => {
router.push({ path: '/shipping/loadingOrder' }).catch(() => {
router.push({ path: '/' });
});
});
});
};
```
**修复后**
```javascript
const generateRoutes = async () => {
try {
const ret = await getUserMenu();
// 简化的导航逻辑
try {
await router.push({ path: targetPath });
} catch (error) {
await router.push({ path: '/' });
}
} catch (error) {
await router.push({ path: '/' });
}
};
```
### 4. 改进路由守卫错误处理
**文件**`src/permission.js`
**修复前**
```javascript
usePermissionStore()
.generateRoutes()
.then((accessRoutes) => {
// 处理成功情况
});
```
**修复后**
```javascript
usePermissionStore()
.generateRoutes()
.then((accessRoutes) => {
// 处理成功情况
})
.catch((error) => {
console.error('Failed to generate routes:', error);
next({ path: '/', replace: true });
});
```
## 修复效果
### ✅ 解决的问题
1. **权限路由生成**
- 不再因为 null 值导致崩溃
- 能够正确处理所有菜单数据
- 提供详细的调试信息
2. **导航稳定性**
- 消除了无限重定向问题
- 简化了错误处理逻辑
- 提供了更好的降级方案
3. **用户体验**
- 登录后能够正常进入系统
- 超级管理员正确跳转到系统管理页面
- 普通用户跳转到有权限的菜单页面
### 🔧 技术改进
1. **错误处理**
- 添加了完整的 try-catch 错误处理
- 提供了详细的错误日志
- 实现了优雅的降级方案
2. **代码质量**
- 使用 async/await 替代复杂的 Promise 链
- 改进了函数的可读性和维护性
- 添加了必要的类型检查
3. **调试支持**
- 增加了详细的控制台日志
- 提供了清晰的错误信息
- 便于问题排查和调试
## 测试建议
1. **登录测试**
- 测试超级管理员登录
- 测试普通用户登录
- 测试无权限用户登录
2. **导航测试**
- 验证页面跳转是否正确
- 检查是否还有重定向问题
- 确认错误处理是否有效
3. **权限测试**
- 验证菜单权限是否正确加载
- 检查权限按钮是否正常显示
- 确认权限分配功能是否正常
## 相关文件
- `src/store/permission.js` - 权限存储和路由生成
- `src/views/login.vue` - 登录页面和导航逻辑
- `src/permission.js` - 路由守卫
- `src/directive/permission/hasPermi.js` - 权限指令
修复完成后,系统应该能够正常处理登录和权限管理功能。

View File

@@ -1,179 +0,0 @@
# 运送清单页面详情和下载按钮修复报告
## 问题描述
用户报告运送清单页面缺少"详情"和"下载"按钮。从图片描述中可以看到,当前运送清单页面只有"查看"、"编辑"、"删除"按钮,但缺少"详情"和"下载"功能。
## 问题分析
1. **缺失的按钮**: 运送清单页面缺少"详情"和"下载"按钮
2. **方法调用错误**: 在调用对话框组件时使用了错误的方法名
3. **组件引用问题**: 没有正确引用详情对话框组件
## 修复方案
### 1. 添加详情和下载按钮
**文件**: `pc-cattle-transportation/src/views/shipping/shippingList.vue`
在操作列中添加了"详情"和"下载"按钮:
```vue
<el-table-column label="操作" width="280" fixed="right">
<template #default="scope">
<el-button type="primary" size="small" v-hasPermi="['delivery:view']" @click="showLookDialog(scope.row)">查看</el-button>
<el-button type="info" size="small" v-hasPermi="['delivery:view']" @click="showDetailDialog(scope.row)">详情</el-button>
<el-button type="success" size="small" v-hasPermi="['delivery:edit']" @click="showEditDialog(scope.row)">编辑</el-button>
<el-button type="warning" size="small" v-hasPermi="['delivery:export']" @click="handleDownload(scope.row)">下载</el-button>
<el-button type="danger" size="small" v-hasPermi="['delivery:delete']" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
```
### 2. 添加详情对话框组件
添加了 `DetailDialog` 组件的引用:
```vue
<!-- 对话框 -->
<OrderDialog ref="OrderDialogRef" @success="getDataList" />
<LookDialog ref="LookDialogRef" />
<EditDialog ref="editDialogRef" @success="getDataList" />
<DetailDialog ref="DetailDialogRef" />
```
### 3. 导入详情对话框组件
```javascript
import DetailDialog from './detailDialog.vue';
```
### 4. 添加组件引用
```javascript
const DetailDialogRef = ref();
```
### 5. 实现详情和下载方法
```javascript
// 详情方法
const showDetailDialog = (row) => {
DetailDialogRef.value.onShowDetailDialog(row);
};
// 下载方法
const handleDownload = (row) => {
ElMessageBox.confirm(
`确定要下载运送清单"${row.deliveryTitle || row.deliveryNumber}"的详细信息吗?`,
'下载确认',
{
confirmButtonText: '确定下载',
cancelButtonText: '取消',
type: 'info',
}
).then(() => {
// 这里可以调用下载API或生成PDF
ElMessage.success('下载功能开发中,敬请期待');
console.log('下载运送清单:', row);
});
};
```
### 6. 修复对话框方法调用错误
修正了所有对话框组件的方法调用:
```javascript
// 修正前(错误)
const showLookDialog = (row) => {
LookDialogRef.value.showDialog(row); // 错误:方法不存在
};
// 修正后(正确)
const showLookDialog = (row) => {
LookDialogRef.value.onShowLookDialog(row); // 正确:使用实际的方法名
};
```
## 修复的对话框方法调用
| 组件 | 错误调用 | 正确调用 |
|------|----------|----------|
| `OrderDialog` | `showDialog()` | `onShowDialog()` |
| `LookDialog` | `showDialog()` | `onShowLookDialog()` |
| `EditDialog` | `showDialog()` | `onShowDialog()` |
| `DetailDialog` | `showDialog()` | `onShowDetailDialog()` |
## 按钮功能说明
### 详情按钮
- **功能**: 显示运送清单的详细信息
- **权限**: `delivery:view`
- **实现**: 调用 `detailDialog.vue` 组件显示详细信息
- **样式**: `type="info"` (蓝色)
### 下载按钮
- **功能**: 下载运送清单的详细信息PDF或Excel格式
- **权限**: `delivery:export`
- **实现**: 目前显示"下载功能开发中"提示,可后续扩展
- **样式**: `type="warning"` (橙色)
## 权限控制
所有按钮都配置了相应的权限控制:
- **查看**: `delivery:view`
- **详情**: `delivery:view`
- **编辑**: `delivery:edit`
- **下载**: `delivery:export`
- **删除**: `delivery:delete`
## 修复结果
### ✅ 已修复的问题
1. **添加了详情按钮**: 现在可以查看运送清单的详细信息
2. **添加了下载按钮**: 提供了下载功能入口(待实现具体功能)
3. **修正了方法调用错误**: 所有对话框组件现在都能正确调用
4. **扩展了操作列宽度**: 从200px扩展到280px以容纳更多按钮
5. **完善了权限控制**: 每个按钮都有对应的权限验证
### 🔧 技术实现
1. **组件集成**: 正确集成了 `detailDialog.vue` 组件
2. **方法调用**: 修正了所有对话框组件的方法调用
3. **权限控制**: 添加了完整的权限验证
4. **用户体验**: 提供了确认对话框和友好的提示信息
## 后续扩展建议
### 下载功能实现
可以进一步实现下载功能:
1. **PDF导出**: 使用 `jsPDF``html2pdf` 生成PDF文件
2. **Excel导出**: 使用 `xlsx` 库生成Excel文件
3. **后端API**: 调用后端API生成文件并提供下载链接
### 示例实现
```javascript
// PDF下载示例
const handleDownload = async (row) => {
try {
const response = await fetch(`/api/delivery/export/${row.id}`);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `运送清单_${row.deliveryNumber}.pdf`;
a.click();
window.URL.revokeObjectURL(url);
} catch (error) {
ElMessage.error('下载失败');
}
};
```
现在运送清单页面已经包含了完整的操作按钮:查看、详情、编辑、下载、删除,所有功能都能正常工作。

View File

@@ -1,149 +0,0 @@
# 运送清单与装车订单页面重复问题修复报告
## 问题描述
用户报告装车订单和运送清单两个页面一模一样,运送清单不是正确的页面。经过分析发现:
1. **装车订单页面** (`/shipping/loadingOrder`) 使用的是 `loadingOrder.vue` 组件
2. **运送清单页面** (`/shipping/shippinglist`) 错误地映射到了同一个 `loadingOrder.vue` 组件
3. 两个页面显示相同的内容,但实际上应该是不同的功能
## 根本原因
在之前的路由修复中,我将 `/shipping/shippinglist` 路由错误地映射到了 `loadingOrder.vue` 组件,导致运送清单页面显示装车订单的内容。
## 业务逻辑分析
通过分析后端API和前端代码发现
### 装车订单 (Loading Order)
- **API**: `/delivery/pageDeliveryOrderList`
- **功能**: 管理装车订单,包括创建、编辑、删除装车订单
- **组件**: `loadingOrder.vue`
- **路由**: `/shipping/loadingOrder`
### 运送清单 (Shipping List)
- **API**: `/delivery/pageQueryList`
- **功能**: 管理运送清单,包括查看、管理运送状态
- **组件**: `shippingList.vue` (新创建)
- **路由**: `/shipping/shippinglist`
## 修复方案
### 1. 添加运送清单API函数
**文件**: `pc-cattle-transportation/src/api/shipping.js`
```javascript
// 运送清单 - 列表查询
export function shippingList(data) {
return request({
url: '/delivery/pageQueryList',
method: 'POST',
data,
});
}
```
### 2. 创建运送清单组件
**文件**: `pc-cattle-transportation/src/views/shipping/shippingList.vue`
创建了专门的运送清单组件,具有以下特点:
- **API调用**: 使用 `shippingList` 函数调用 `/delivery/pageQueryList` API
- **界面标题**: "运送清单" 而不是 "装车订单"
- **按钮文本**: "新增运送清单" 而不是 "创建装车订单"
- **表格列**: 包含运送清单相关的字段
- **权限控制**: 使用 `delivery:*` 权限而不是 `loading:*` 权限
### 3. 修正路由配置
**文件**: `pc-cattle-transportation/src/router/index.ts`
```typescript
// 运送清单路由
{
path: '/shipping',
component: LayoutIndex,
meta: {
title: '运送清单',
keepAlive: true,
requireAuth: true,
},
children: [
{
path: 'loadingOrder',
name: 'LoadingOrder',
meta: {
title: '装车订单',
keepAlive: true,
requireAuth: true,
},
component: () => import('~/views/shipping/loadingOrder.vue'),
},
{
path: 'shippinglist',
name: 'ShippingList',
meta: {
title: '运送清单',
keepAlive: true,
requireAuth: true,
},
component: () => import('~/views/shipping/shippingList.vue'),
},
],
},
```
## 功能对比
| 功能 | 装车订单 | 运送清单 |
|------|----------|----------|
| **API端点** | `/delivery/pageDeliveryOrderList` | `/delivery/pageQueryList` |
| **组件文件** | `loadingOrder.vue` | `shippingList.vue` |
| **路由路径** | `/shipping/loadingOrder` | `/shipping/shippinglist` |
| **主要功能** | 创建和管理装车订单 | 查看和管理运送清单 |
| **按钮文本** | "创建装车订单" | "新增运送清单" |
| **权限前缀** | `loading:*` | `delivery:*` |
| **表格标题** | "装车订单编号" | "运送清单编号" |
## 修复结果
### ✅ 已修复的问题
1. **运送清单页面独立**: 现在有专门的 `shippingList.vue` 组件
2. **API调用正确**: 运送清单使用正确的API端点
3. **路由映射正确**: `/shipping/shippinglist` 映射到正确的组件
4. **功能区分明确**: 装车订单和运送清单现在是两个独立的功能
### 🔧 技术实现
1. **API层**: 添加了 `shippingList` 函数调用运送清单API
2. **组件层**: 创建了专门的运送清单组件
3. **路由层**: 修正了路由配置,确保正确的组件映射
4. **权限层**: 使用了正确的权限控制
## 验证建议
建议测试以下功能:
1. **装车订单页面** (`/shipping/loadingOrder`)
- 应该显示装车订单相关功能
- 按钮显示"创建装车订单"
- 使用装车订单API
2. **运送清单页面** (`/shipping/shippinglist`)
- 应该显示运送清单相关功能
- 按钮显示"新增运送清单"
- 使用运送清单API
- 与装车订单页面内容不同
## 注意事项
1. **数据一致性**: 确保两个页面显示的数据来源正确
2. **权限控制**: 确保权限配置正确,避免权限混乱
3. **用户体验**: 两个页面应该有明确的业务区分
4. **API兼容性**: 确保后端API支持两个不同的端点
现在装车订单和运送清单是两个独立的功能页面,不再显示相同的内容。

View File

@@ -1,114 +0,0 @@
# 短期目标任务清单
根据开发计划,短期目标包括:
1. 完善现有功能模块的用户体验
2. 修复已知的Bug和警告信息
3. 优化系统性能和加载速度
4. 完善文档和注释
## 任务列表
### 1. 修复已知的Bug和警告信息
#### 1.1 Vue警告信息修复
- [x] 检查并修复所有"[Vue warn]"相关的警告
- [x] 检查并修复组件属性类型不匹配问题
- [ ] 检查并修复未定义方法或属性的引用问题
- [ ] 检查并修复无效watch源问题
#### 1.2 Element Plus弃用警告修复
- [x] 检查并修复所有Element Plus组件弃用警告
- [x] 替换已弃用的组件属性和方法
- [x] 更新组件使用方式以符合最新版本要求
#### 1.3 其他警告和错误修复
- [x] 修复ESLint配置和相关问题
- [ ] 修复TypeScript类型检查问题
- [ ] 修复控制台中的其他警告信息
### 2. 完善现有功能模块的用户体验
#### 2.1 界面优化
- [ ] 统一各模块界面风格
- [ ] 优化表单布局和交互
- [ ] 改进表格展示效果
- [ ] 优化按钮和操作项的布局
#### 2.2 交互改进
- [ ] 添加必要的加载状态提示
- [ ] 完善错误处理和提示信息
- [ ] 优化表单验证和用户反馈
- [ ] 改进搜索和筛选功能体验
#### 2.3 响应式优化
- [ ] 检查并优化各页面在不同屏幕尺寸下的显示效果
- [ ] 修复可能存在的布局错乱问题
### 3. 优化系统性能和加载速度
#### 3.1 代码优化
- [ ] 检查并优化组件加载策略
- [ ] 实施代码分割和懒加载
- [ ] 减少不必要的重新渲染
- [ ] 优化图片和资源加载
#### 3.2 网络请求优化
- [ ] 检查并优化API请求
- [ ] 实施请求缓存策略
- [ ] 优化数据获取和处理逻辑
#### 3.3 构建优化
- [ ] 检查并优化Vite配置
- [ ] 优化打包和构建过程
### 4. 完善文档和注释
#### 4.1 代码注释
- [ ] 为关键函数和方法添加注释
- [ ] 为复杂业务逻辑添加注释
- [ ] 统一注释风格和格式
#### 4.2 文档更新
- [ ] 更新现有文档中的过时信息
- [ ] 补充缺失的文档内容
- [ ] 优化文档结构和可读性
#### 4.3 开发规范
- [ ] 完善代码规范文档
- [ ] 更新开发指南
- [ ] 补充最佳实践说明
## 实施计划
### 第一周
- [x] 完成所有已知警告和错误的修复
- [x] 修复ESLint配置问题
- [x] 修复Vue相关警告
- [x] 修复Element Plus弃用警告
### 第二周
- 完善用户体验优化
- 统一界面风格
- 优化交互流程
- 改进响应式效果
### 第三周
- 实施性能优化措施
- 优化代码加载策略
- 优化网络请求
- 检查构建配置
### 第四周
- 完善文档和注释
- 补充代码注释
- 更新项目文档
- 完善开发规范
## 验证标准
- [x] 控制台无任何警告信息
- [ ] 所有功能模块正常运行
- [ ] 页面加载速度提升20%以上
- [ ] 用户体验得到明显改善
- [ ] 代码注释覆盖率达到80%以上
- [ ] 文档完整性和准确性达到90%以上

View File

@@ -1,128 +0,0 @@
# 超级管理员权限管理指南
## 概述
本指南说明如何为超级管理员账户手机号15900000000打开全部菜单权限。
## 方法一:通过权限管理界面操作(推荐)
### 步骤:
1. **登录系统**
- 使用超级管理员账户登录系统
- 导航到权限管理页面:`/permission/menu`
2. **选择目标用户**
- 在左侧用户列表中找到手机号为 `15900000000` 的用户
- 点击选择该用户
3. **分配权限**
- **方法A手动全选**
- 在右侧菜单树中,手动勾选所有菜单项
- 点击"保存菜单权限"按钮
- **方法B一键分配新增功能**
- 点击"一键分配全部权限"按钮
- 确认操作
- 系统将自动为该用户分配所有菜单权限
## 方法二:使用工具函数(开发者)
### 导入工具函数
```javascript
import {
assignAllPermissionsToSuperAdmin,
checkSuperAdminPermissions,
quickAssignAllPermissions
} from '@/utils/superAdminHelper.js';
```
### 检查权限状态
```javascript
// 检查超级管理员当前权限状态
const status = await checkSuperAdminPermissions('15900000000');
console.log('权限状态:', status);
```
### 分配所有权限
```javascript
// 为超级管理员分配所有菜单权限
const success = await assignAllPermissionsToSuperAdmin('15900000000');
if (success) {
console.log('权限分配成功');
}
```
### 一键分配(包含确认提示)
```javascript
// 一键分配权限包含UI确认提示
const success = await quickAssignAllPermissions('15900000000');
```
## 方法三通过API直接调用
### 获取所有菜单
```javascript
// GET /sysMenu/list
const menuListRes = await getMenuList();
const allMenuIds = menuListRes.data.map(menu => menu.id);
```
### 分配权限
```javascript
// POST /sysMenu/assignRoleMenus
const assignRes = await assignRoleMenus({
roleId: targetUser.roleId,
menuIds: allMenuIds
});
```
## 权限验证
### 前端验证
系统会自动检查用户权限:
- 超级管理员roleId = 1自动拥有 `*:*:*` 权限
- 普通用户根据分配的菜单权限进行验证
### 后端验证
```java
// 在 LoginServiceImpl.java 中
if (roleId.equals(RoleConstants.SUPER_ADMIN_ROLE_ID)) {
return Collections.singletonList(RoleConstants.ALL_PERMISSION);
}
```
## 注意事项
1. **超级管理员特权**
- 超级管理员roleId = 1在代码层面已经拥有所有权限
- 菜单权限分配主要用于界面显示和权限管理
2. **权限持久化**
- 权限分配会保存到数据库的 `sys_role_menu` 表中
- 重新登录后权限仍然有效
3. **安全考虑**
- 只有拥有 `permission:menu:assign` 权限的用户才能分配权限
- 建议定期检查权限分配情况
## 故障排除
### 常见问题
1. **找不到用户**:确认手机号是否正确
2. **权限分配失败**:检查是否有分配权限的权限
3. **菜单不显示**:确认菜单权限已正确分配
### 调试信息
系统会在控制台输出详细的调试信息:
```
=== 获取用户菜单 ===
=== 用户权限检查 ===
=== 权限路由生成 ===
=== 处理后的菜单列表 ===
```
## 相关文件
- 前端权限管理:`src/views/permission/menuPermission.vue`
- 权限API`src/api/permission.js`
- 工具函数:`src/utils/superAdminHelper.js`
- 后端控制器:`SysMenuController.java`
- 权限验证:`StpInterfaceImpl.java`

View File

@@ -1,114 +0,0 @@
# 超级管理员权限说明
## 问题原因
超级管理员15900000000的操作权限没有全部打开的原因是
### 1. **权限管理基于角色,而非超级管理员特权**
当前系统使用**基于角色的权限管理RBAC**
- 权限存储在 `sys_role_menu` 表中
- 所有使用相同 `roleId` 的用户共享相同的权限
- 即使 `roleId=1`(超级管理员角色),也要遵循数据库中的权限配置
### 2. **超级管理员权限标识**
系统在代码层面为超级管理员提供了特殊处理:
```java
// StpInterfaceImpl.java 第38-42行
if (roleId.equals(RoleConstants.SUPER_ADMIN_ROLE_ID)) {
log.info("用户 {} 是超级管理员,拥有所有权限", loginId);
// 超级管理员返回通配符权限
return Collections.singletonList(RoleConstants.ALL_PERMISSION);
}
```
这意味着:
- **后端验证**:超级管理员拥有 `*:*:*` 权限,后端不会拦截任何操作
- **前端显示**:但前端界面的复选框状态取决于数据库中的 `sys_role_menu`
### 3. **权限界面的作用**
"操作权限管理"界面中的复选框状态:
-**不影响功能权限**:只是用于展示和编辑数据库中的权限配置
-**超级管理员仍然可以操作**:即使复选框未选中,后端也会允许访问
- ⚠️ **前端按钮显示受影响**:如果权限未勾选,前端 `v-hasPermi` 指令会隐藏按钮
## 解决方案
### 方案1为超级管理员角色分配所有权限推荐
在数据库中为 `roleId=1` 分配所有菜单权限:
```sql
-- 查询所有菜单ID
SELECT id FROM sys_menu WHERE is_delete = 0;
-- 为超级管理员角色分配所有菜单权限
-- 假设有 100 个菜单IDs 为 1-100
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT 1, id FROM sys_menu WHERE is_delete = 0
ON DUPLICATE KEY UPDATE role_id = role_id;
```
### 方案2前端特殊处理超级管理员
修改前端的权限检查逻辑,让超级管理员始终显示所有按钮:
```javascript
// src/utils/permission.js 或类似的权限检查文件
const hasPermission = (permission) => {
const userStore = useUserStore();
const isSuperAdmin = userStore.roleId === 1; // 超级管理员 roleId=1
if (isSuperAdmin) {
return true; // 超级管理员直接返回 true
}
// 普通用户的权限检查逻辑
// ...
};
```
### 方案3完全忽略前端权限检查不推荐
对于超级管理员,可以跳过所有前端权限检查,但这可能带来安全隐患。
## 建议
**最佳实践**
1. 在数据库中为超级管理员角色roleId=1分配所有菜单权限
2. 前端保留权限检查逻辑(安全考虑)
3. 后端继续使用 `*:*:*` 特殊处理
这样既保证了超级管理员的功能完整性,又保持了权限管理的规范性。
## 如何判断超级管理员是否有权限
### 前端权限检查(影响按钮显示)
```vue
<!-- 如果权限未勾选按钮会被隐藏 -->
<el-button v-hasPermi="['loading:edit']">编辑</el-button>
```
### 后端权限验证(实际控制)
```java
// 即使前端按钮显示,后端也会验证
@SaCheckPermission("loading:edit")
public AjaxResult editOrder(...) {
// 超级管理员 roleId=1 会被自动放行
}
```
## 总结
**超级管理员的操作权限没有全部打开**是因为:
1. 数据库中的 `sys_role_menu` 表没有为超级管理员角色分配所有菜单
2. 前端的复选框显示基于数据库配置
3. 但这**不影响**后端功能权限:超级管理员仍然可以访问所有接口
**解决方案**:为超级管理员角色在数据库中分配所有菜单权限即可。

View File

@@ -1,169 +0,0 @@
# 超级管理员用户专属权限修复报告
## 问题描述
用户"12.27新增姓名"ID: 3, roleId: 1设置了用户专属权限在权限管理界面中可以看到"装车订单"下的操作按钮(如"编辑"、"分配设备"、"删除"、"装车"等)都是**未选中**状态,表示这些按钮应该被隐藏。但是当用户登录后,这些操作按钮仍然显示。
## 问题原因
### 根本原因
用户"12.27新增姓名"的 `roleId=1`,而 `RoleConstants.SUPER_ADMIN_ROLE_ID = 1`,所以该用户被系统识别为超级管理员。
### 权限查询逻辑问题
`LoginServiceImpl.java``queryUserPermissions` 方法中,存在以下逻辑:
```java
// 原来的逻辑(有问题)
if (roleId.equals(RoleConstants.SUPER_ADMIN_ROLE_ID)) {
log.info("=== 超级管理员用户 {} 使用所有权限", userId);
return Collections.singletonList(RoleConstants.ALL_PERMISSION);
}
// 1. 先查询用户专属权限
List<SysMenu> userMenus = sysUserMenuMapper.selectMenusByUserId(userId);
```
**问题**如果用户是超级管理员角色roleId=1系统会直接返回所有权限 `*:*:*`**完全跳过用户专属权限的检查**。
### 权限检查逻辑
在前端 `hasPermi.js` 中:
```javascript
// 检查是否是超级管理员
const isSuperAdmin = userStore.permissions.includes('*:*:*') || userStore.roles.includes('admin');
// 只有非超级管理员且没有相应权限时才隐藏元素
if (!hasPermissions && !isSuperAdmin) {
el.parentNode && el.parentNode.removeChild(el);
}
```
由于后端返回了 `*:*:*` 权限,前端识别为超级管理员,所以所有按钮都会显示。
## 修复方案
### 修改权限查询优先级
调整 `queryUserPermissions` 方法的逻辑顺序:
1. **优先检查用户专属权限**无论角色ID是什么
2. **如果没有专属权限,再使用角色权限**
3. **超级管理员权限作为最后的fallback**
### 修复后的逻辑
```java
private List<String> queryUserPermissions(Integer userId, Integer roleId) {
if (userId == null || roleId == null) {
return Collections.emptyList();
}
// 1. 先查询用户专属权限(优先于角色权限)
List<SysMenu> userMenus = sysUserMenuMapper.selectMenusByUserId(userId);
if (userMenus != null && !userMenus.isEmpty()) {
log.info("=== 用户 {} 使用专属权限,权限数量: {}", userId, userMenus.size());
return userMenus.stream()
.filter(menu -> StringUtils.isNotEmpty(menu.getAuthority()))
.map(SysMenu::getAuthority)
.distinct()
.collect(Collectors.toList());
}
// 2. 如果没有专属权限,使用角色权限
if (roleId.equals(RoleConstants.SUPER_ADMIN_ROLE_ID)) {
log.info("=== 超级管理员用户 {} 使用所有权限(无专属权限)", userId);
return Collections.singletonList(RoleConstants.ALL_PERMISSION);
}
// 3. 普通角色权限
log.info("=== 用户 {} 使用角色权限roleId: {}", userId, roleId);
List<SysMenu> roleMenus = menuMapper.selectMenusByRoleId(roleId);
return roleMenus.stream()
.filter(menu -> StringUtils.isNotEmpty(menu.getAuthority()))
.map(SysMenu::getAuthority)
.distinct()
.collect(Collectors.toList());
}
```
## 修复内容
### 文件:`tradeCattle/aiotagro-cattle-trade/src/main/java/com/aiotagro/cattletrade/business/service/impl/LoginServiceImpl.java`
**修改位置**第166-196行的 `queryUserPermissions` 方法
**修改内容**
- 将用户专属权限检查提前到角色权限检查之前
- 确保即使超级管理员角色ID的用户也能使用专属权限
- 只有在没有专属权限时才使用超级管理员权限
## 修复效果
### 修复前
- ❌ 超级管理员角色ID的用户无法使用专属权限
- ❌ 用户"12.27新增姓名"设置了专属权限但按钮仍然显示
- ❌ 权限优先级:超级管理员权限 > 用户专属权限
### 修复后
- ✅ 用户专属权限优先于所有角色权限
- ✅ 超级管理员角色ID的用户也能使用专属权限
- ✅ 权限优先级:用户专属权限 > 角色权限 > 超级管理员权限
## 测试验证
### 测试步骤
1. **重新编译后端**`mvn clean compile`
2. **重启后端服务**`mvn spring-boot:run`
3. **清除浏览器缓存**
4. **使用"12.27新增姓名"账号登录**
5. **检查装车订单页面的操作按钮**
### 预期结果
- 用户"12.27新增姓名"登录后,装车订单页面的操作按钮应该根据专属权限设置被隐藏
- 控制台日志应该显示"用户 3 使用专属权限"
- 权限检查应该显示 `isSuperAdmin: false`
## 技术说明
### 权限优先级设计
```
1. 用户专属权限(最高优先级)
2. 角色权限(普通用户)
3. 超级管理员权限fallback
```
### 向后兼容性
- ✅ 没有设置专属权限的超级管理员用户仍然使用所有权限
- ✅ 没有设置专属权限的普通用户仍然使用角色权限
- ✅ 现有功能不受影响
### 日志输出
修复后的日志输出示例:
```
=== 用户 3 使用专属权限,权限数量: 15
```
而不是:
```
=== 超级管理员用户 3 使用所有权限
```
## 相关文件
- `tradeCattle/aiotagro-cattle-trade/src/main/java/com/aiotagro/cattletrade/business/service/impl/LoginServiceImpl.java` - 权限查询逻辑
- `pc-cattle-transportation/src/directive/permission/hasPermi.js` - 前端权限检查
- `pc-cattle-transportation/src/views/permission/operationPermission.vue` - 权限管理界面
## 总结
通过调整权限查询的优先级成功解决了超级管理员角色ID用户无法使用专属权限的问题。修复后的系统能够
1. **正确识别用户专属权限**即使角色ID是超级管理员
2. **按预期隐藏操作按钮**:根据专属权限设置
3. **保持向后兼容性**:不影响现有功能
4. **提供清晰的日志**:便于调试和监控
**修复状态**:✅ 已完成
**测试状态**:⏳ 待验证
**部署状态**:✅ 已部署

View File

@@ -1,144 +0,0 @@
# 用户管理删除功能实现报告
## 概述
实现用户管理页面中的删除按钮功能,可以删除数据库中的用户数据。
## 实现内容
### 1. 前端实现 (`pc-cattle-transportation/src/views/system/user.vue`)
#### 导入必要的依赖
```javascript
import { ElMessage, ElMessageBox } from 'element-plus';
import { sysUserList, sysUserDel, sysUserSave } from '@/api/sys.js';
```
#### 删除用户方法
```92:117:pc-cattle-transportation/src/views/system/user.vue
// 删除用户
const delClick = (row) => {
ElMessageBox.confirm('请确认是否删除该用户?', '提示', {
cancelButtonText: '取消',
confirmButtonText: '确定',
type: 'warning',
})
.then(() => {
sysUserDel(row.id)
.then(() => {
ElMessage.success('删除成功');
getDataList();
})
.catch((error) => {
ElMessage.error('删除失败');
console.error('删除失败:', error);
});
})
.catch(() => {
// 用户取消删除
});
};
```
#### 表格操作列
```20:25:pc-cattle-transportation/src/views/system/user.vue
<el-table-column label="操作" width="150">
<template #default="scope">
<el-button link type="primary" @click="showAddDialog(scope.row)">编辑</el-button>
<el-button link type="primary" @click="delClick(scope.row)" style="color: #f56c6c;">删除</el-button>
</template>
</el-table-column>
```
### 2. API接口 (`pc-cattle-transportation/src/api/sys.js`)
```89:95:pc-cattle-transportation/src/api/sys.js
// 子账号管理-删除
export function sysUserDel(id) {
return request({
url: `/sysUser/delete?id=${id}`,
method: 'GET',
});
}
```
### 3. 后端接口实现
#### Controller (`SysUserController.java`)
```46:49:tradeCattle/aiotagro-cattle-trade/src/main/java/com/aiotagro/cattletrade/business/controller/SysUserController.java
@GetMapping("/delete")
public AjaxResult delete(@RequestParam Integer id) {
return userService.delete(id);
}
```
#### Service (`SysUserServiceImpl.java`)
```80:84:tradeCattle/aiotagro-cattle-trade/src/main/java/com/aiotagro/cattletrade/business/service/impl/SysUserServiceImpl.java
@Override
public AjaxResult delete(Integer id) {
removeById(id);
return AjaxResult.success();
}
```
## 功能特点
1. **二次确认**:点击删除按钮时,会弹出确认对话框
2. **直接删除**:确认后直接调用后端接口删除数据库记录
3. **自动刷新**:删除成功后自动刷新列表
4. **错误处理**:删除失败时显示错误提示
5. **用户友好**:删除按钮使用红色样式,清晰地表示危险操作
## 工作流程
1. 用户点击"删除"按钮
2. 弹出确认对话框:"请确认是否删除该用户?"
3. 用户点击"确定"
4. 前端调用 `/sysUser/delete?id={userId}` 接口
5. 后端执行 `removeById(id)` 删除数据库记录
6. 返回成功响应
7. 前端显示"删除成功"提示
8. 自动刷新用户列表
## 测试建议
1. **功能测试**
- 点击删除按钮,确认对话框正常弹出
- 点击取消,不执行删除
- 点击确定,用户被删除
- 删除后列表自动刷新
2. **边界测试**
- 测试删除不存在的用户ID
- 测试网络异常情况
3. **权限测试**
- 确认用户是否有删除权限
## 修改的文件
### 前端
- ✅ `pc-cattle-transportation/src/views/system/user.vue` - 实现删除功能和完整的数据列表
- ✅ `pc-cattle-transportation/src/api/sys.js` - API接口已存在
### 后端
- ✅ `tradeCattle/aiotagro-cattle-trade/src/main/java/com/aiotagro/cattletrade/business/controller/SysUserController.java` - Controller接口已存在
- ✅ `tradeCattle/aiotagro-cattle-trade/src/main/java/com/aiotagro/cattletrade/business/service/impl/SysUserServiceImpl.java` - Service实现已存在
## 注意事项
1. **数据安全**:删除操作是永久性的,不可恢复(除非有备份)
2. **权限控制**:建议在后端添加权限校验,只允许特定角色删除用户
3. **关联数据**:如果用户有关联数据(如订单、设备等),需要检查是否应该级联删除或阻止删除
## 后续改进建议
1. 添加权限校验
2. 添加关联数据检查(如用户是否有关联的订单或设备)
3. 实现软删除(添加 `isDelete` 标记,而不是物理删除)
4. 添加操作日志记录
## 创建时间
2025-01-16

View File

@@ -1,288 +0,0 @@
# 用户级权限管理系统测试验证指南
## 系统概述
已成功实施基于用户的权限管理系统UBAC与现有角色权限系统RBAC并存。用户专属权限优先于角色权限。
## 实施内容
### 1. 数据库层
- ✅ 创建 `sys_user_menu`
- ✅ 创建 `SysUserMenu` 实体类
- ✅ 创建 `SysUserMenuMapper` 接口和XML
- ✅ 创建 `SysUserMenuController` 控制器
### 2. 后端逻辑
- ✅ 修改 `LoginServiceImpl` 权限查询逻辑
- ✅ 实现用户专属权限优先机制
- ✅ 保持向后兼容性
### 3. 前端界面
- ✅ 重构权限管理页面为标签页结构
- ✅ 角色权限管理标签页(保留原有功能)
- ✅ 用户权限管理标签页(新增功能)
- ✅ 更新API接口文件
## 测试验证步骤
### 步骤1数据库准备
1. **执行SQL脚本**
```sql
-- 执行以下SQL创建用户权限表
source tradeCattle/add_user_menu_table.sql;
```
2. **验证表结构**
```sql
DESCRIBE sys_user_menu;
```
### 步骤2后端API测试
#### 2.1 测试用户权限查询API
**测试用例1检查用户权限状态**
```bash
GET /sysUserMenu/hasUserPermissions?userId=3
```
**预期结果:**
```json
{
"code": 200,
"data": {
"hasUserPermissions": false,
"permissionCount": 0,
"permissionSource": "角色权限"
}
}
```
**测试用例2获取用户权限ID列表**
```bash
GET /sysUserMenu/userMenuIds?userId=3
```
**预期结果:**
```json
{
"code": 200,
"data": []
}
```
#### 2.2 测试用户权限分配API
**测试用例3为用户分配权限**
```bash
POST /sysUserMenu/assignUserMenus
Content-Type: application/json
{
"userId": 3,
"menuIds": [1, 2, 3, 16, 4, 5]
}
```
**预期结果:**
```json
{
"code": 200,
"msg": "分配成功"
}
```
**测试用例4验证权限分配结果**
```bash
GET /sysUserMenu/hasUserPermissions?userId=3
```
**预期结果:**
```json
{
"code": 200,
"data": {
"hasUserPermissions": true,
"permissionCount": 6,
"permissionSource": "用户专属权限"
}
}
```
#### 2.3 测试权限清空API
**测试用例5清空用户专属权限**
```bash
DELETE /sysUserMenu/clearUserMenus?userId=3
```
**预期结果:**
```json
{
"code": 200,
"msg": "清空成功,用户将使用角色权限"
}
```
### 步骤3前端界面测试
#### 3.1 角色权限管理标签页测试
1. **访问权限管理页面**
- 打开浏览器,访问权限管理页面
- 确认显示"角色权限管理"标签页
2. **测试角色权限分配**
- 选择用户"12.27新增姓名"ID: 3, roleId: 1
- 修改权限设置
- 点击"保存角色权限"
- 确认提示信息显示影响范围
3. **验证权限影响**
- 切换到"用户权限管理"标签页
- 查看"超级管理员"ID: 11, roleId: 1的权限
- 确认权限已被修改因为使用相同roleId
#### 3.2 用户权限管理标签页测试
1. **测试用户专属权限分配**
- 在"用户权限管理"标签页选择用户"12.27新增姓名"
- 修改权限设置
- 点击"保存用户权限"
- 确认提示信息
2. **验证权限隔离**
- 选择用户"超级管理员"
- 确认权限未被影响(仍使用角色权限)
- 查看权限来源显示"角色权限"
3. **测试权限清空功能**
- 选择有专属权限的用户
- 点击"清空专属权限"
- 确认权限来源变更为"角色权限"
### 步骤4登录权限验证测试
#### 4.1 用户专属权限测试
1. **设置用户专属权限**
- 为用户"12.27新增姓名"设置专属权限
- 确保权限与角色权限不同
2. **用户登录测试**
- 使用"12.27新增姓名"账号登录
- 检查控制台日志,确认使用专属权限
- 验证页面按钮显示符合专属权限设置
3. **权限覆盖验证**
- 修改角色权限
- 重新登录"12.27新增姓名"账号
- 确认权限未受影响(仍使用专属权限)
#### 4.2 角色权限测试
1. **清空用户专属权限**
- 清空"12.27新增姓名"的专属权限
2. **角色权限验证**
- 重新登录"12.27新增姓名"账号
- 检查控制台日志,确认使用角色权限
- 验证页面按钮显示符合角色权限设置
### 步骤5向后兼容性测试
#### 5.1 现有用户测试
1. **未设置专属权限的用户**
- 使用"超级管理员"账号登录
- 确认权限正常(使用角色权限)
- 验证所有功能正常
2. **权限管理功能**
- 确认角色权限管理功能正常
- 验证权限修改影响所有使用相同角色的用户
#### 5.2 系统稳定性测试
1. **权限查询性能**
- 测试大量用户登录时的权限查询性能
- 确认无性能问题
2. **数据一致性**
- 验证用户权限和角色权限数据一致性
- 确认无数据冲突
## 预期测试结果
### 成功标准
1. **功能完整性**
- ✅ 用户专属权限分配功能正常
- ✅ 用户专属权限清空功能正常
- ✅ 权限优先级正确(用户权限 > 角色权限)
- ✅ 向后兼容性保持
2. **界面友好性**
- ✅ 标签页切换流畅
- ✅ 权限来源显示清晰
- ✅ 操作确认提示完善
3. **数据一致性**
- ✅ 权限数据存储正确
- ✅ 权限查询结果准确
- ✅ 权限更新及时生效
### 测试数据
**测试用户信息:**
- 用户A12.27新增姓名 (ID: 3, roleId: 1)
- 用户B超级管理员 (ID: 11, roleId: 1)
**测试场景:**
1. 用户A设置专属权限用户B使用角色权限
2. 用户A清空专属权限恢复使用角色权限
3. 修改角色权限,影响所有使用该角色的用户
## 问题排查
### 常见问题
1. **权限不生效**
- 检查用户是否重新登录
- 确认权限数据是否正确保存
- 验证权限查询逻辑
2. **界面显示异常**
- 检查API接口是否正常
- 确认前端数据绑定
- 验证权限来源显示
3. **性能问题**
- 检查数据库索引
- 优化权限查询SQL
- 确认缓存机制
### 调试日志
**后端日志关键词:**
- `=== 用户权限查询 ===`
- `=== 用户专属权限优先 ===`
- `=== 分配用户菜单权限 ===`
**前端日志关键词:**
- `=== 用户权限管理 ===`
- `=== 保存用户权限 ===`
- `=== 清空用户权限 ===`
## 总结
用户级权限管理系统已成功实施,实现了:
1. **双权限系统并存**:角色权限 + 用户权限
2. **权限优先级明确**:用户权限覆盖角色权限
3. **向后兼容性**:现有功能不受影响
4. **界面友好性**:标签页切换,操作清晰
5. **功能完整性**:分配、清空、查询功能齐全
系统现在可以满足精细化的权限管理需求,同时保持原有系统的稳定性。

View File

@@ -1,116 +0,0 @@
# Vue组件加载错误修复报告
## 问题描述
用户访问权限管理页面时出现以下错误:
```
TypeError: Failed to fetch dynamically imported module: http://localhost:8080/src/views/permission/operationPermission.vue?t=1761097727669
```
## 问题原因
`operationPermission.vue` 文件中存在**变量名冲突**
1. **导入的API函数**`hasUserPermissions` (来自 `@/api/permission.js`)
2. **声明的ref变量**`const hasUserPermissions = ref(false)`
这导致了JavaScript语法错误
```
[vue/compiler-sfc] Identifier 'hasUserPermissions' has already been declared. (29:6)
```
## 修复方案
### 1. 重命名ref变量
将ref变量从 `hasUserPermissions` 重命名为 `userHasPermissions`
```javascript
// 修复前
const hasUserPermissions = ref(false);
// 修复后
const userHasPermissions = ref(false);
```
### 2. 更新所有引用
更新所有使用该变量的地方:
```javascript
// 模板中的引用
:disabled="!currentUser || !userHasPermissions"
// 脚本中的引用
userHasPermissions.value = hasRes.data.hasUserPermissions;
userHasPermissions.value = true;
userHasPermissions.value = false;
```
## 修复内容
### 文件:`pc-cattle-transportation/src/views/permission/operationPermission.vue`
1. **第277行**:变量声明
```javascript
const userHasPermissions = ref(false);
```
2. **第194行**:模板绑定
```vue
:disabled="!currentUser || !userHasPermissions"
```
3. **第519行**:权限状态更新
```javascript
userHasPermissions.value = hasRes.data.hasUserPermissions;
```
4. **第596行**:保存权限后更新
```javascript
userHasPermissions.value = true;
```
5. **第657行**:清空权限后更新
```javascript
userHasPermissions.value = false;
```
## 验证结果
### 1. 构建测试
```bash
npm run build
```
✅ **构建成功** - 无语法错误
### 2. 开发服务器
```bash
npm run dev
```
✅ **服务器启动成功** - 组件可以正常加载
## 技术说明
### 变量命名冲突
在Vue 3 Composition API中当导入的函数名与本地声明的变量名相同时会导致
- JavaScript解析器报错
- Vue编译器无法正确处理
- 动态导入失败
### 最佳实践
1. **避免命名冲突**:导入的函数和本地变量使用不同的命名
2. **语义化命名**:使用更具描述性的变量名
3. **代码审查**:在重构时检查命名冲突
## 影响范围
- ✅ **修复范围**:仅影响 `operationPermission.vue` 文件
-**功能影响**:无功能影响,仅修复语法错误
-**向后兼容**:完全兼容,不影响现有功能
## 总结
通过重命名冲突的变量成功解决了Vue组件动态导入失败的问题。现在权限管理页面可以正常访问和使用用户级权限管理功能完全可用。
**修复状态**:✅ 已完成
**测试状态**:✅ 已验证
**部署状态**:✅ 可部署

View File

@@ -1,90 +0,0 @@
# Word导出功能实现完成报告
## ✅ 已完成的工作
### 1. 依赖库安装
- ✅ 安装了 `docxtemplater``pizzip``file-saver` 等必要的npm包
### 2. 前端代码实现
- ✅ 更新了 `pc-cattle-transportation/src/views/entry/attestation.vue`
- ✅ 导入了必要的库PizZip、Docxtemplater、saveAs
- ✅ 实现了完整的 `download` 函数,包含:
- 字段计算逻辑(下车总重量、单价、总金额)
- 数据映射和准备
- Word文档生成
- 错误处理和用户反馈
- ✅ 修改了按钮调用传递完整的row对象
- ✅ 添加了详细的调试日志
### 3. 模板文件准备
- ✅ 创建了模板占位符文件
- ✅ 创建了HTML模板参考
- ✅ 创建了详细的模板创建指南
### 4. 字段映射实现
按照要求实现了以下字段映射:
-`supplierName` - 供货单位(供货商姓名)
-`buyerName` - 收货单位(采购商姓名)
-`startLocation` - 发车地点(起始地)
-`createTime` - 发车时间(创建时间)
-`endLocation` - 到达地点(目的地)
-`driverName` - 司机姓名
-`driverMobile` - 司机联系方式
-`licensePlate` - 装车车牌号
-`ratedQuantity` - 下车总数量(头)
-`totalWeight` - 下车总重量(斤)- 计算:(落地装车磅数-空车磅重)/2
-`unitPrice` - 单价(元/斤)- 计算:约定价格/2
-`totalAmount` - 总金额(元)- 计算:下车总重量*单价
### 5. 计算逻辑实现
- ✅ 下车总重量 = (landingEntruckWeight - emptyWeight) / 2
- ✅ 单价 = firmPrice / 2
- ✅ 总金额 = totalWeight * unitPrice
- ✅ 所有计算结果保留2位小数
## 🔄 需要完成的工作
### 1. 创建Word模板文件
**重要**需要手动创建Word模板文件
- 文件位置:`pc-cattle-transportation/public/cattle-delivery-template.docx`
- 参考文件:`pc-cattle-transportation/public/WORD_TEMPLATE_GUIDE.md`
- 模板应包含所有占位符:{supplierName}, {buyerName}, {startLocation}, 等
### 2. 测试和验证
- 测试API返回的数据是否包含所有必需字段
- 验证计算公式的正确性
- 测试Word文档生成功能
- 检查字段映射是否准确
## 📋 测试步骤
1. **检查数据字段**
- 打开浏览器开发者工具
- 查看控制台中的"Word导出字段检查"日志
- 确认所有必需字段都有值
2. **创建Word模板**
- 按照 `WORD_TEMPLATE_GUIDE.md` 创建模板文件
- 确保模板包含所有占位符
- 保存为 `cattle-delivery-template.docx`
3. **测试导出功能**
- 点击"下载文件"按钮
- 检查是否成功生成Word文档
- 验证文档内容是否正确
## 🚨 注意事项
- 订单编号格式字段留空
- 序号、活牛品种、单只体重范围、备注字段留空
- 动物检疫合格证明字段留空
- 计算公式严格按照要求实现
- 单价和总金额保留2位小数
## 🎯 功能特点
- 使用docxtemplater库进行模板处理
- 支持复杂的计算逻辑
- 完整的错误处理和用户反馈
- 详细的调试日志
- 严格按照图片格式要求实现

View File

@@ -0,0 +1,99 @@
/**
* 清理 Vue 文件中的 console.log 调试语句
* 保留 console.error 和 console.warn
*/
const fs = require('fs');
const path = require('path');
// 需要清理的目录
const targetDirs = [
'src/views',
'src/components'
];
// 统计信息
let totalFiles = 0;
let cleanedFiles = 0;
let totalLogsRemoved = 0;
/**
* 递归遍历目录
*/
function walkDir(dir, callback) {
const files = fs.readdirSync(dir);
files.forEach(file => {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
walkDir(filePath, callback);
} else if (file.endsWith('.vue') || file.endsWith('.js') || file.endsWith('.ts')) {
callback(filePath);
}
});
}
/**
* 清理文件中的 console.log
*/
function cleanFile(filePath) {
totalFiles++;
let content = fs.readFileSync(filePath, 'utf-8');
const originalContent = content;
// 计算原有的 console.log 数量
const logCount = (content.match(/console\.log\(/g) || []).length;
if (logCount === 0) {
return;
}
// 移除 console.log 语句(保留 console.error 和 console.warn
// 匹配整行的 console.log
content = content.replace(/^[ \t]*console\.log\([^)]*\);?\s*$/gm, '');
// 移除行内的 console.log可能在其他代码之后
content = content.replace(/console\.log\([^)]*\);?\s*/g, '');
// 清理多余的空行但保留最多2个连续空行
content = content.replace(/\n\n\n+/g, '\n\n');
// 如果内容有变化,写回文件
if (content !== originalContent) {
fs.writeFileSync(filePath, content, 'utf-8');
cleanedFiles++;
totalLogsRemoved += logCount;
console.log(`✓ 清理 ${path.relative(process.cwd(), filePath)} (移除 ${logCount} 个 console.log)`);
}
}
// 主函数
console.log('='.repeat(60));
console.log('开始清理前端 console.log 调试语句');
console.log('='.repeat(60));
console.log();
targetDirs.forEach(dir => {
const fullPath = path.join(__dirname, dir);
if (fs.existsSync(fullPath)) {
console.log(`正在扫描目录: ${dir}`);
walkDir(fullPath, cleanFile);
} else {
console.log(`⚠ 目录不存在: ${dir}`);
}
});
console.log();
console.log('='.repeat(60));
console.log('清理完成!');
console.log('='.repeat(60));
console.log(`总文件数: ${totalFiles}`);
console.log(`清理文件数: ${cleanedFiles}`);
console.log(`移除的 console.log 总数: ${totalLogsRemoved}`);
console.log();
console.log('注意console.error 和 console.warn 已保留');
console.log('='.repeat(60));

View File

@@ -70,7 +70,7 @@ export function jbqServerList(data) {
data,
});
}
// 预警记录
// 预警记录列表
export function warningLogList(data) {
return request({
url: '/warningLog/pageQuery',
@@ -79,6 +79,15 @@ export function warningLogList(data) {
});
}
// 预警详情
export function warningDetail(id) {
return request({
url: '/warningLog/warningDetail',
method: 'POST',
params: { id },
});
}
// 智能项圈 -列表
export function collarList(data) {
return request({

View File

@@ -86,12 +86,9 @@ const handleChange = (editor) => {
emits('update:html', valueHtml.value);
};
const handleFocus = (editor) => {
// console.log('focus', editor);
};
// };
const handleBlur = (editor) => {
// console.log('blur', editor);
// console.log(valueHtml.value);
};
// // };
const customAlert = (info, type) => {
// alert(`【自定义提示】${type} - ${info}`);
};
@@ -103,8 +100,7 @@ const customPaste = (editor, event, callback) => {
};
const handleDestroyed = (editor) => {
// console.log('destroyed', editor);
};
// };
// 组件销毁时,也及时销毁编辑器
onBeforeUnmount(() => {
const editor = editorRef.value;

View File

@@ -169,8 +169,7 @@ export default defineComponent({
imgUploading.value = false;
})
.catch((err) => {
// console.log(err);
imgUploading.value = false;
// imgUploading.value = false;
});
} else {
alert(`文件过大,请选择不超过${(maxFileSizeType.value / 1024 / 1024).toFixed(2)}MB的文件`);
@@ -189,9 +188,7 @@ export default defineComponent({
if (acceptType.value == 'video/*') {
dialogVisible.value = true;
}
// console.log(acceptType.value);
// console.log(imgUrl.value);
},
// // },
imgPreviewClose: () => {
showImageViewer.value = false;
},

View File

@@ -38,9 +38,7 @@ const handlers = reactive({
};
});
data.options = handleTree(list);
// console.log('====================================');
// console.log(data.options);
});
// // });
},
});

View File

@@ -172,7 +172,7 @@ watch(
() => props.emitSearch,
(newVal, oldVal) => {
if (newVal) {
console.log('此时触发--立即执行搜索');
emit('search', formInline);
}
},
@@ -205,7 +205,6 @@ const change = (e, param) => {
emit('change', { e, param });
};
const onSubmit = () => {
// console.log('submit!',formInline);
emit('search', formInline);
};

View File

@@ -167,7 +167,7 @@ const toggleSelection = (rows) => {
}
};
const handleClick = (type, e) => {
console.log(e);
if (type == 'select') {
emit('select', e);
} else if (type == 'pageSize') {

View File

@@ -24,13 +24,13 @@
</template>
<template v-for="cItem in route.children">
<template v-if="cItem.children && cItem.children.length > 0">
<el-sub-menu :index="route.path + '/' + cItem.path">
<el-sub-menu :index="joinPath(route.path, cItem.path)">
<template #title>
<!-- <svg-icon :icon-class="cItem.meta.icon" /> -->
<span class="pl-3">{{ cItem.meta.title }}</span>
</template>
<template v-for="subItem in cItem.children">
<el-menu-item :index="route.path + '/' + cItem.path + '/' + subItem.path">
<el-menu-item :index="joinPath(joinPath(route.path, cItem.path), subItem.path)">
<svg-icon :icon-class="subItem.meta.icon" />
<span class="pl-3">{{ subItem.meta.title }}</span>
</el-menu-item>
@@ -38,7 +38,7 @@
</el-sub-menu>
</template>
<template v-else>
<el-menu-item :index="route.path + '/' + cItem.path">
<el-menu-item :index="joinPath(route.path, cItem.path)">
<svg-icon :icon-class="cItem.meta.icon" />
<span class="pl-3">{{ cItem.meta.title }}</span>
</el-menu-item>
@@ -72,6 +72,21 @@ const data = reactive({
const sidebarRouters = computed(() => permissionStore.sidebarRouters);
const route = useRoute();
// 安全拼接路径,避免双斜杠
const joinPath = (parentPath, childPath) => {
if (!parentPath || !childPath) return parentPath || childPath || '/';
// 移除父路径末尾的斜杠
parentPath = parentPath.replace(/\/+$/, '');
// 移除子路径开头的斜杠
childPath = childPath.replace(/^\/+/, '');
// 拼接并规范化
const joined = `${parentPath}/${childPath}`;
// 移除重复的斜杠
return joined.replace(/\/+/g, '/');
};
const currentMenu = ref('/');
const isCollapse = ref(false);
const userStore = JSON.parse(localStorage.getItem('userStore'));

View File

@@ -12,7 +12,6 @@ import { useUserStore } from '~/store/user';
const userStore = useUserStore();
// 打印useUserStore里 state的信息
console.log(userStore.$state);
const updateUserName = () => {
userStore.updateUserName('嗨!');

View File

@@ -26,14 +26,6 @@ export default {
// 检查是否是超级管理员 - 只检查用户store中的权限避免权限数据不一致
const isSuperAdmin = userStore.permissions.includes('*:*:*') || userStore.roles.includes('admin');
console.log('=== 权限检查调试 ===', {
permissionStorePermissions: permissionStore.userPermission,
userStorePermissions: userStore.permissions,
finalPermissions: permissions,
isSuperAdmin: isSuperAdmin,
requiredPermissions: value
});
if (value && value instanceof Array && value.length > 0) {
const permissionFlag = value;
@@ -42,15 +34,8 @@ export default {
return all_permission === permission || permissionFlag.includes(permission);
});
console.log('=== 权限检查结果 ===', {
hasPermissions: hasPermissions,
isSuperAdmin: isSuperAdmin,
shouldShow: hasPermissions || isSuperAdmin
});
// 只有非超级管理员且没有相应权限时才隐藏元素
if (!hasPermissions && !isSuperAdmin) {
console.log('=== 隐藏元素 ===', permissionFlag);
// 安全地隐藏元素避免DOM操作错误
try {
// 检查元素是否还在DOM中

View File

@@ -13,14 +13,6 @@ const whiteList = ['/login', '/register'];
router.beforeEach((to, from, next) => {
NProgress.start();
// 修复双斜杠路径问题
if (to.path && to.path.includes('//')) {
const fixedPath = to.path.replace(/\/+/g, '/');
console.warn('检测到双斜杠路径,已修复:', to.path, '->', fixedPath);
next({ path: fixedPath, query: to.query, hash: to.hash, replace: true });
return;
}
if (getToken()) {
if (to.path === '/login') {
usePermissionStore().setRoutes([]);
@@ -41,6 +33,16 @@ router.beforeEach((to, from, next) => {
usePermissionStore()
.generateRoutes()
.then((accessRoutes) => {
// 递归修复所有路由(包括子路由)的双斜杠
const fixRouteSlashes = (route) => {
if (route.path && route.path.includes('//')) {
route.path = route.path.replace(/\/+/g, '/');
}
if (route.children && Array.isArray(route.children)) {
route.children.forEach(child => fixRouteSlashes(child));
}
};
// 根据roles权限生成可访问的路由表
accessRoutes.forEach((route) => {
// 验证路由路径
@@ -49,11 +51,8 @@ router.beforeEach((to, from, next) => {
return;
}
// 修复双斜杠路径
if (route.path && route.path.includes('//')) {
console.warn('修复路由双斜杠路径:', route.path, '->', route.path.replace(/\/+/g, '/'));
route.path = route.path.replace(/\/+/g, '/');
}
// 递归修复路由及其所有子路由的双斜杠
fixRouteSlashes(route);
router.addRoute(route); // 动态添加可访问路由表
});

View File

@@ -27,11 +27,9 @@ const usePermissionStore = defineStore('permission', {
},
setUserPermission(arr) {
this.userPermission = arr;
console.log('=== 权限store更新 ===', arr);
},
// 强制刷新权限数据
async refreshPermissions() {
console.log('=== 强制刷新权限数据 ===');
this.routeFlag = false; // 重置路由标志,强制重新生成路由
return this.generateRoutes();
},
@@ -40,11 +38,9 @@ const usePermissionStore = defineStore('permission', {
// 向后端请求路由数据
getUserMenu().then((res) => {
const { code, data } = res;
console.log('=== 权限路由生成 ===', { code, data });
const btnList = data.filter((i) => i.type === 2);
const permissionList = btnList.map((i) => i.authority).filter(auth => auth); // 过滤掉空权限
console.log('=== 设置用户权限列表 ===', permissionList);
this.setUserPermission(permissionList);
let menuList = data.filter((i) => i.type !== 2);
@@ -52,8 +48,15 @@ const usePermissionStore = defineStore('permission', {
// 确保 routeUrl 存在且不为空
let routeUrl = item.routeUrl || item.pageUrl || '';
// 规范化路径
routeUrl = normalizeRoutePath(routeUrl);
// 对于顶级菜单parentId === 0添加前导斜杠
// 对于子菜单,保持为相对路径(不带前导斜杠)
if (item.parentId === 0 || item.parentId === '0') {
// 顶级菜单:确保以 / 开头
routeUrl = normalizeRoutePath(routeUrl);
} else {
// 子菜单:移除前导斜杠,使用相对路径
routeUrl = routeUrl.replace(/^\/+/, '');
}
return {
id: item.id,
@@ -75,22 +78,21 @@ const usePermissionStore = defineStore('permission', {
JSON.parse(JSON.stringify(menuList));
const sdata = JSON.parse(JSON.stringify(menuList));
console.log('=== 处理后的菜单列表 ===', menuList);
console.log('=== 路径检查 ===', menuList.map(item => ({ name: item.name, path: item.path })));
// 检查并修复双斜杠路径
const doubleSlashPaths = menuList.filter(item => item.path && item.path.includes('//'));
if (doubleSlashPaths.length > 0) {
console.error('=== 发现双斜杠路径 ===', doubleSlashPaths);
// 修复双斜杠路径
menuList.forEach(item => {
// 递归修复所有路径中的双斜杠
const fixDoubleSlashes = (items) => {
if (!items || !Array.isArray(items)) return;
items.forEach(item => {
if (item.path && item.path.includes('//')) {
const originalPath = item.path;
item.path = item.path.replace(/\/+/g, '/');
console.warn('修复菜单路径:', originalPath, '->', item.path);
}
if (item.children && item.children.length > 0) {
fixDoubleSlashes(item.children);
}
});
}
};
fixDoubleSlashes(menuList);
fixDoubleSlashes(sdata);
// eslint-disable-next-line no-use-before-define
const rewriteRoutes = filterAsyncRouter(menuList, false, true);
@@ -99,12 +101,6 @@ const usePermissionStore = defineStore('permission', {
// eslint-disable-next-line no-use-before-define
const asyncRoutes = filterDynamicRoutes(dynamicRoutes);
console.log('=== 最终路由配置 ===', {
rewriteRoutes,
sidebarRoutes,
asyncRoutes
});
asyncRoutes.forEach((route) => {
router.addRoute(route);
});
@@ -113,7 +109,7 @@ const usePermissionStore = defineStore('permission', {
resolve(rewriteRoutes);
}).catch((error) => {
console.error('=== 获取用户菜单失败 ===', error);
console.error('获取用户菜单失败', error);
// 如果获取菜单失败,返回空路由数组
this.setSidebarRouters([], 500);
this.setRoutes([]);
@@ -127,7 +123,6 @@ const usePermissionStore = defineStore('permission', {
function capitalizeFirstLetter(string) {
// 处理 null 或 undefined 值
if (!string || typeof string !== 'string') {
console.warn('capitalizeFirstLetter: Invalid string input:', string);
return 'Unknown';
}
@@ -281,18 +276,18 @@ export const loadView = (view) => {
const defaultView = () => import('~/views/entry/details.vue');
if (!view) {
console.warn('loadView: view parameter is empty, using default view');
console.error('loadView: view parameter is empty, using default view');
return defaultView;
}
console.log('loadView: Loading view:', view);
// 规范化 view 路径:移除开头的斜杠(如果有)
const normalizedView = view.startsWith('/') ? view.slice(1) : view;
let res;
// eslint-disable-next-line guard-for-in,no-restricted-syntax
for (const path in modules) {
const dir = path.split('views/')[1].split('.vue')[0];
if (dir === view) {
console.log('loadView: Found matching module:', path);
if (dir === normalizedView) {
// 使用函数包装导入过程,添加错误处理
res = () =>
modules[path]().catch((error) => {
@@ -306,7 +301,7 @@ export const loadView = (view) => {
// 如果没有找到匹配的视图,返回默认视图
if (!res) {
console.warn('loadView: View not found:', view, 'Available modules:', Object.keys(modules));
console.error('loadView: View not found:', normalizedView);
return defaultView;
}

View File

@@ -61,18 +61,29 @@
</template>
</el-table-column>
<el-table-column prop="warningTime" label="预警时间" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="scope">
<el-button type="primary" link @click="viewDetail(scope.row)">查看详情</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-model:limit="form.pageSize" v-model:page="form.pageNum" :total="form1.total" @pagination="getList" />
</div>
<!-- 预警详情对话框 -->
<warning-detail-dialog ref="detailDialogRef" />
</div>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue';
import { ElMessage } from 'element-plus';
import baseSearch from '@/components/common/searchCustom/index.vue';
import { warningLogList } from '~/api/hardware.js';
import warningDetailDialog from './warningDetailDialog.vue';
import { warningLogList, warningDetail } from '~/api/hardware.js';
const dataListLoading = ref(false);
const baseSearchRef = ref();
const detailDialogRef = ref();
const form = reactive({
pageSize: 10,
pageNum: 1,
@@ -92,7 +103,7 @@ const searchFrom = () => {
};
const searchChange = (val) => {
console.log('Search change:', val);
// 在这里可以处理搜索条件变化的逻辑
};
@@ -160,6 +171,50 @@ const getList = () => {
dataListLoading.value = false;
});
};
// 查看预警详情
const viewDetail = async (row) => {
try {
// ✅ 调用后端接口获取完整的预警详情(包括设备位置信息)
const res = await warningDetail(row.id);
if (res.code === 200 && res.data) {
const detailData = res.data;
// ✅ 修复使用列表中的预警类型而不是后端API返回的类型
// 因为后端API可能返回的是旧数据列表中的类型才是用户看到的
if (row.warningType && row.warningType !== detailData.warningType) {
console.warn('[WARNING-LIST] ⚠️ 预警类型不一致!列表中:', row.warningType, '后端返回:', detailData.warningType);
console.warn('[WARNING-LIST] 使用列表中的预警类型:', row.warningType);
detailData.warningType = row.warningType;
}
// 补充预警类型描述
const warningTypeMap = {
2: '数量盘单预警',
3: '运输距离预警',
4: '设备停留预警',
5: '高温预警',
6: '低温预警',
7: '位置偏离预警',
8: '延误预警',
9: '超前到达预警'
};
detailData.warningTypeDesc = warningTypeMap[detailData.warningType] || detailData.warningReason || '未知预警';
// 打开详情对话框
detailDialogRef.value.open(detailData);
} else {
ElMessage.error(res.msg || '获取预警详情失败');
}
} catch (error) {
console.error('[WARNING-LIST] 获取预警详情失败:', error);
ElMessage.error('获取预警详情失败,请稍后重试');
}
};
onMounted(() => {
getList();
});

View File

@@ -0,0 +1,725 @@
<template>
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="900px"
:close-on-click-modal="false"
@close="handleClose"
>
<!-- 温度预警 - 只显示设备信息不显示地图 -->
<div v-if="isTemperatureWarning" class="warning-content temperature-warning">
<!-- 预警基本信息 -->
<el-descriptions title="温度预警基本信息" :column="2" border>
<el-descriptions-item label="预警时间">
<span style="font-weight: 600;">{{ warningData.warningTime }}</span>
</el-descriptions-item>
<el-descriptions-item label="预警类型">
<el-tag :type="warningData.warningType == 5 ? 'danger' : 'primary'" size="large">
{{ warningData.warningTypeDesc }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="预警温度">
<span :style="{
color: getTemperatureColor(parseFloat(warningData.deviceTemp)),
fontWeight: 'bold',
fontSize: '18px'
}">
{{ warningData.deviceTemp || '--' }}°C
</span>
</el-descriptions-item>
<el-descriptions-item label="设备ID">
{{ warningData.serverDeviceSn || warningData.deviceId || '未知' }}
</el-descriptions-item>
<el-descriptions-item label="运单号">{{ warningData.deliveryNumber }}</el-descriptions-item>
<el-descriptions-item label="车牌号">{{ warningData.licensePlate }}</el-descriptions-item>
<el-descriptions-item label="司机姓名">{{ warningData.driverName }}</el-descriptions-item>
<el-descriptions-item label="创建人">{{ warningData.createByName || '--' }}</el-descriptions-item>
</el-descriptions>
<!-- 预警详情描述 -->
<div v-if="warningData.warningDetail" class="warning-description">
<el-alert
:title="warningData.warningDetail"
:type="warningData.warningType == 5 ? 'error' : 'warning'"
:closable="false"
show-icon
style="margin-top: 20px;"
/>
</div>
<el-divider content-position="left">
<el-icon><InfoFilled /></el-icon>
<span style="margin-left: 5px;">设备详细信息</span>
</el-divider>
<!-- 绑定设备列表 -->
<div v-if="deviceList.length > 0" class="device-list-section">
<div class="section-header">
<h4>
<el-icon style="vertical-align: middle;"><Connection /></el-icon>
绑定设备列表
<el-tag type="info" size="small" style="margin-left: 10px;">{{ deviceList.length }}</el-tag>
</h4>
</div>
<el-table :data="deviceList" border style="width: 100%" size="small">
<el-table-column prop="deviceId" label="设备ID" width="150" />
<el-table-column prop="deviceTypeName" label="设备类型" width="120">
<template #default="scope">
<el-tag
:type="scope.row.deviceType == 1 || scope.row.deviceType == 4 ? 'primary' : (scope.row.deviceType == 2 ? 'success' : 'warning')"
>
{{ scope.row.deviceTypeName || '未知' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="sn" label="设备SN" min-width="150" />
<el-table-column prop="battery" label="电量" width="100">
<template #default="scope">
<span v-if="scope.row.battery || scope.row.batteryPercentage">
{{ scope.row.battery || scope.row.batteryPercentage }}%
</span>
<span v-else>--</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="scope">
<el-tag :type="scope.row.status == 1 ? 'success' : 'info'">
{{ scope.row.status == 1 ? '在线' : '离线' }}
</el-tag>
</template>
</el-table-column>
</el-table>
</div>
<div v-else class="no-data-tip">
<el-empty description="暂无绑定设备信息" :image-size="80" />
</div>
<!-- 设备温度日志重点显示温度数据 -->
<div v-if="deviceLogs.length > 0" class="device-logs-section">
<div class="section-header">
<h4>
<el-icon style="vertical-align: middle;"><DataLine /></el-icon>
设备温度记录
<el-tag type="info" size="small" style="margin-left: 10px;">{{ deviceLogs.length }}</el-tag>
</h4>
<p class="section-desc">显示设备的温度历史记录可以查看温度变化趋势</p>
</div>
<el-table
:data="deviceLogs"
border
style="width: 100%"
size="small"
max-height="350"
v-loading="loadingLogs"
:default-sort="{ prop: 'createTime', order: 'descending' }"
>
<el-table-column label="记录时间" width="170" sortable>
<template #default="scope">
{{ scope.row.hourTime || scope.row.createTime || '--' }}
</template>
</el-table-column>
<el-table-column prop="deviceTypeName" label="设备类型" width="110">
<template #default="scope">
<el-tag
size="small"
:type="scope.row.deviceType == 1 || scope.row.deviceType == 4 ? 'primary' : (scope.row.deviceType == 2 ? 'success' : 'warning')"
>
{{ scope.row.deviceTypeName }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="deviceId" label="设备ID" width="140" />
<el-table-column label="温度°C" width="120" sortable>
<template #default="scope">
<span v-if="scope.row.deviceTemp"
:style="{
color: getTemperatureColor(parseFloat(scope.row.deviceTemp)),
fontWeight: '600',
fontSize: '14px'
}">
{{ scope.row.deviceTemp }}°C
</span>
<span v-else style="color: #909399;">--</span>
</template>
</el-table-column>
<el-table-column prop="heartRate" label="心率" width="80" />
<el-table-column label="步数" width="90">
<template #default="scope">
{{ scope.row.stepCount || scope.row.steps || '--' }}
</template>
</el-table-column>
<el-table-column prop="latitude" label="纬度" width="100" />
<el-table-column prop="longitude" label="经度" width="100" />
</el-table>
</div>
<div v-else-if="!loadingLogs" class="no-data-tip">
<el-empty description="暂无设备日志记录" :image-size="80" />
</div>
</div>
<!-- 停留预警/位置偏离预警 - 显示地图 -->
<div v-else-if="isLocationWarning" class="warning-content">
<el-descriptions title="位置预警详情" :column="2" border>
<el-descriptions-item label="预警时间">{{ warningData.warningTime }}</el-descriptions-item>
<el-descriptions-item label="预警类型">
<el-tag :type="getWarningTagType(warningData.warningType)">
{{ warningData.warningTypeDesc }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="预警经度">{{ warningData.longitude || '未知' }}</el-descriptions-item>
<el-descriptions-item label="预警纬度">{{ warningData.latitude || '未知' }}</el-descriptions-item>
<el-descriptions-item label="运单号">{{ warningData.deliveryNumber }}</el-descriptions-item>
<el-descriptions-item label="车牌号">{{ warningData.licensePlate }}</el-descriptions-item>
<el-descriptions-item label="司机姓名">{{ warningData.driverName }}</el-descriptions-item>
<el-descriptions-item label="创建人">{{ warningData.createByName }}</el-descriptions-item>
</el-descriptions>
<el-divider />
<!-- 百度地图显示 -->
<div class="map-container">
<h4>预警位置地图</h4>
<div id="warningMap" style="width: 100%; height: 400px;"></div>
</div>
<!-- 预警详情描述 -->
<div v-if="warningData.warningDetail" class="warning-description">
<h4>预警详情</h4>
<p>{{ warningData.warningDetail }}</p>
</div>
<!-- 新增绑定设备列表 -->
<div v-if="deviceList.length > 0" class="device-list-section">
<h4>绑定设备列表{{ deviceList.length }}</h4>
<el-table :data="deviceList" border style="width: 100%" size="small">
<el-table-column prop="deviceId" label="设备ID" width="150" />
<el-table-column prop="deviceTypeName" label="设备类型" width="120">
<template #default="scope">
<el-tag :type="scope.row.deviceType == 1 ? 'primary' : (scope.row.deviceType == 2 ? 'success' : 'warning')">
{{ scope.row.deviceTypeName || '未知' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="sn" label="设备SN" min-width="150" />
<el-table-column prop="status" label="状态" width="100">
<template #default="scope">
<el-tag :type="scope.row.status == 1 ? 'success' : 'info'">
{{ scope.row.status == 1 ? '在线' : '离线' }}
</el-tag>
</template>
</el-table-column>
</el-table>
</div>
<!-- 新增设备日志列表 -->
<div v-if="deviceLogs.length > 0" class="device-logs-section">
<h4>设备日志记录{{ deviceLogs.length }}</h4>
<el-table
:data="deviceLogs"
border
style="width: 100%"
size="small"
max-height="300"
v-loading="loadingLogs"
>
<el-table-column label="时间" width="160">
<template #default="scope">
{{ scope.row.hourTime || scope.row.createTime || '--' }}
</template>
</el-table-column>
<el-table-column prop="deviceTypeName" label="设备类型" width="110">
<template #default="scope">
<el-tag size="small" :type="scope.row.deviceType == 1 ? 'primary' : (scope.row.deviceType == 2 ? 'success' : 'warning')">
{{ scope.row.deviceTypeName }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="deviceId" label="设备ID" width="130" />
<el-table-column prop="latitude" label="纬度" width="90" />
<el-table-column prop="longitude" label="经度" width="90" />
<el-table-column prop="deviceTemp" label="温度°C" width="100">
<template #default="scope">
<span v-if="scope.row.deviceTemp" :style="{ color: getTemperatureColor(parseFloat(scope.row.deviceTemp)) }">
{{ scope.row.deviceTemp }}°C
</span>
<span v-else>--</span>
</template>
</el-table-column>
<el-table-column prop="heartRate" label="心率" width="80" />
<el-table-column label="步数" width="80">
<template #default="scope">
{{ scope.row.stepCount || scope.row.steps || '--' }}
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 其他类型预警 -->
<div v-else class="warning-content">
<el-descriptions title="预警详情" :column="2" border>
<el-descriptions-item label="预警时间">{{ warningData.warningTime }}</el-descriptions-item>
<el-descriptions-item label="预警类型">
<el-tag :type="getWarningTagType(warningData.warningType)">
{{ warningData.warningTypeDesc }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="运单号">{{ warningData.deliveryNumber }}</el-descriptions-item>
<el-descriptions-item label="车牌号">{{ warningData.licensePlate }}</el-descriptions-item>
<el-descriptions-item label="司机姓名">{{ warningData.driverName }}</el-descriptions-item>
<el-descriptions-item label="创建人">{{ warningData.createByName }}</el-descriptions-item>
</el-descriptions>
<el-divider />
<div v-if="warningData.warningDetail" class="warning-description">
<h4>预警详情</h4>
<p>{{ warningData.warningDetail }}</p>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">关闭</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, computed, nextTick } from 'vue';
import { ElMessage } from 'element-plus';
import { BMPGL } from '@/utils/loadBmap.js';
import { pageDeviceList, getCollarLogs, getEarTagLogs, getHostLogs } from '@/api/abroad.js';
const dialogVisible = ref(false);
const warningData = reactive({
id: null,
warningType: null,
warningTypeDesc: '',
warningTime: '',
latitude: '',
longitude: '',
deviceId: '',
deviceName: '',
deviceTemp: '', // 修改:使用 deviceTemp
temperature: null,
warningDetail: '',
deliveryNumber: '',
licensePlate: '',
driverName: '',
createByName: '',
deliveryId: null, // 新增运单ID
serverDeviceSn: '', // 新增主机设备SN
});
const temperatureHistory = ref([]);
const deviceList = ref([]); // 新增:设备列表
const deviceLogs = ref([]); // 新增:设备日志列表
const loadingDevices = ref(false); // 新增:加载设备列表状态
const loadingLogs = ref(false); // 新增:加载日志状态
let mapInstance = null;
let markerInstance = null;
// 计算属性:判断预警类型
const isTemperatureWarning = computed(() => {
// 5-高温预警6-低温预警
const type = parseInt(warningData.warningType);
const isTempWarning = type === 5 || type === 6;
return isTempWarning;
});
const isLocationWarning = computed(() => {
// 4-设备停留预警7-位置偏离预警8-延误预警
const type = parseInt(warningData.warningType);
const isLocWarning = type === 4 || type === 7 || type === 8;
return isLocWarning;
});
const dialogTitle = computed(() => {
return `${warningData.warningTypeDesc || '预警'}详情`;
});
// 打开对话框
const open = async (row) => {
// 填充数据
Object.keys(warningData).forEach(key => {
if (row[key] !== undefined) {
warningData[key] = row[key];
}
});
dialogVisible.value = true;
// ✅ 查询运单绑定的设备列表
if (warningData.deliveryId) {
await loadDeviceList(warningData.deliveryId);
}
// 如果是位置相关预警,加载地图
if (isLocationWarning.value && warningData.latitude && warningData.longitude) {
await nextTick();
initMap();
}
// 注意:温度预警的日志已经通过 loadDeviceList 自动加载,无需单独调用
// 设备列表加载后会自动调用 loadAllDeviceLogs()
};
// ✅ 新增:加载运单绑定的设备列表
const loadDeviceList = async (deliveryId) => {
if (!deliveryId) {
console.warn('[WARNING-DETAIL] 运单ID为空无法加载设备列表');
return;
}
loadingDevices.value = true;
try {
const res = await pageDeviceList({
deliveryId: deliveryId,
pageNum: 1,
pageSize: 100, // 一次性加载所有设备
});
if (res.code === 200 && res.data) {
// ✅ 修复:后端直接返回数组,不是嵌套在 list 或 rows 中
if (Array.isArray(res.data)) {
deviceList.value = res.data;
} else {
deviceList.value = res.data.list || res.data.rows || [];
}
// 自动加载所有设备的日志
await loadAllDeviceLogs();
} else {
ElMessage.warning('加载设备列表失败:' + (res.msg || '未知错误'));
}
} catch (error) {
console.error('[WARNING-DETAIL] 加载设备列表失败:', error);
ElMessage.error('加载设备列表失败');
} finally {
loadingDevices.value = false;
}
};
// ✅ 新增:加载所有设备的日志数据
const loadAllDeviceLogs = async () => {
if (deviceList.value.length === 0) {
console.warn('[WARNING-DETAIL] 设备列表为空,无法加载日志');
return;
}
loadingLogs.value = true;
deviceLogs.value = []; // 清空之前的日志
try {
// 并行加载所有设备的日志
const logPromises = deviceList.value.map(device => {
return loadDeviceLogs(device.deviceId || device.sn, device.deviceType, warningData.deliveryId);
});
await Promise.all(logPromises);
} catch (error) {
console.error('[WARNING-DETAIL] 加载设备日志失败:', error);
ElMessage.error('加载设备日志失败');
} finally {
loadingLogs.value = false;
}
};
// ✅ 新增:加载单个设备的日志数据
const loadDeviceLogs = async (deviceId, deviceType, deliveryId) => {
if (!deviceId) {
console.warn('[WARNING-DETAIL] 设备ID为空无法加载日志');
return;
}
if (!deliveryId) {
console.warn('[WARNING-DETAIL] 运单ID为空无法加载日志');
return;
}
// 确保 deviceType 是数字
const typeNum = parseInt(deviceType);
try {
// 根据设备类型选择不同的API
let apiFunc;
let deviceTypeName;
switch (typeNum) {
case 1: // 智能主机
apiFunc = getHostLogs;
deviceTypeName = '智能主机';
break;
case 2: // 智能耳标
apiFunc = getEarTagLogs;
deviceTypeName = '智能耳标';
break;
case 3: // 智能项圈
case 4: // 也可能是4
apiFunc = getCollarLogs;
deviceTypeName = '智能项圈';
break;
default:
console.warn(`[WARNING-DETAIL] 未知的设备类型: ${typeNum} (原始值: ${deviceType})`);
return;
}
// 调用对应的日志查询API必须传入 deliveryId
const res = await apiFunc({
deviceId: deviceId,
deliveryId: deliveryId, // ✅ 新增:后端必需参数
pageNum: 1,
pageSize: 50, // 查询最近50条日志
// 可选添加时间范围过滤预警时间前后1小时
// startTime: getStartTime(warningData.warningTime),
// endTime: getEndTime(warningData.warningTime),
});
if (res.code === 200 && res.data) {
// ✅ 修复:后端可能直接返回数组,也可能嵌套在 list/rows 中
let logs = [];
if (Array.isArray(res.data)) {
logs = res.data;
} else {
logs = res.data.list || res.data.rows || [];
}
console.log('[WARNING-DETAIL] 原始日志数据:', logs);
// 为每条日志添加设备信息
const logsWithDeviceInfo = logs.map(log => ({
...log,
deviceId: deviceId,
deviceType: typeNum, // 使用转换后的数字类型
deviceTypeName: deviceTypeName,
}));
deviceLogs.value.push(...logsWithDeviceInfo);
} else {
console.warn('[WARNING-DETAIL] 加载' + deviceTypeName + '日志失败:', res.msg);
}
} catch (error) {
console.error('[WARNING-DETAIL] 加载设备(' + deviceId + ')日志失败:', error);
}
};
// 初始化地图
const initMap = async () => {
try {
// 使用百度地图 API Key
const BMapGL = await BMPGL('SOawZTeQbxdgrKYYx0o2hn34G0DyU2uo');
const lat = parseFloat(warningData.latitude);
const lon = parseFloat(warningData.longitude);
if (isNaN(lat) || isNaN(lon)) {
ElMessage.warning('经纬度数据无效');
return;
}
// 创建地图实例(使用 BMapGL
mapInstance = new BMapGL.Map('warningMap');
const point = new BMapGL.Point(lon, lat);
mapInstance.centerAndZoom(point, 15);
mapInstance.enableScrollWheelZoom(true);
// 添加标注
markerInstance = new BMapGL.Marker(point);
mapInstance.addOverlay(markerInstance);
// 添加信息窗口
const warningTypeText = warningData.warningTypeDesc || '预警位置';
const infoWindow = new BMapGL.InfoWindow(
'<div style="padding: 10px;">' +
'<p style="margin: 0; font-weight: bold; color: #f56c6c;">' + warningTypeText + '</p>' +
'<p style="margin: 5px 0 0 0;">时间: ' + warningData.warningTime + '</p>' +
'<p style="margin: 5px 0 0 0;">经度: ' + lon + '</p>' +
'<p style="margin: 5px 0 0 0;">纬度: ' + lat + '</p>' +
'</div>',
{ width: 250, height: 120 }
);
markerInstance.addEventListener('click', function () {
mapInstance.openInfoWindow(infoWindow, point);
});
// 默认打开信息窗口
mapInstance.openInfoWindow(infoWindow, point);
} catch (error) {
console.error('[WARNING-DETAIL] 地图初始化失败:', error);
ElMessage.error('地图加载失败');
}
};
// 根据温度值返回颜色
const getTemperatureColor = (temp) => {
if (temp == null) return '#909399';
if (temp >= 35) return '#f56c6c'; // 高温-红色
if (temp <= 5) return '#409eff'; // 低温-蓝色
return '#67c23a'; // 正常-绿色
};
// 根据预警类型返回标签类型
const getWarningTagType = (type) => {
const typeNum = parseInt(type);
switch (typeNum) {
case 2: return 'danger'; // 数量盘单预警
case 3: return 'warning'; // 运输距离预警
case 4: return 'info'; // 设备停留预警
case 5: return 'danger'; // 高温预警
case 6: return 'info'; // 低温预警
case 7: return 'warning'; // 位置偏离预警
case 8: return 'danger'; // 延误预警
case 9: return 'success'; // 超前到达预警
default: return 'info';
}
};
// 关闭对话框
const handleClose = () => {
// 清理地图实例
if (mapInstance) {
mapInstance.clearOverlays();
mapInstance = null;
markerInstance = null;
}
// 清空温度历史数据
temperatureHistory.value = [];
// ✅ 清空设备列表和日志数据
deviceList.value = [];
deviceLogs.value = [];
loadingDevices.value = false;
loadingLogs.value = false;
// 重置数据
Object.keys(warningData).forEach(key => {
if (typeof warningData[key] === 'number') {
warningData[key] = null;
} else {
warningData[key] = '';
}
});
};
// 导出方法
defineExpose({
open
});
</script>
<style scoped lang="less">
.warning-content {
padding: 10px 0;
}
// 温度预警专用样式
.temperature-warning {
.warning-description {
margin-top: 20px;
}
.section-header {
margin-bottom: 15px;
h4 {
margin: 0 0 8px 0;
color: #303133;
font-size: 16px;
font-weight: 600;
display: flex;
align-items: center;
}
.section-desc {
margin: 0;
font-size: 13px;
color: #909399;
}
}
.no-data-tip {
padding: 40px 0;
text-align: center;
}
}
.warning-description {
margin-top: 20px;
h4 {
margin: 0 0 10px 0;
color: #303133;
font-size: 14px;
}
p {
margin: 0;
padding: 10px;
background-color: #f5f7fa;
border-left: 3px solid #409eff;
border-radius: 4px;
line-height: 1.6;
color: #606266;
}
}
.map-container {
margin-top: 20px;
h4 {
margin: 0 0 10px 0;
color: #303133;
font-size: 14px;
}
}
.temperature-chart {
margin-top: 20px;
h4 {
margin: 0 0 10px 0;
color: #303133;
font-size: 14px;
}
}
.device-list-section {
margin-top: 20px;
h4 {
margin: 0 0 10px 0;
color: #303133;
font-size: 14px;
font-weight: 600;
}
}
.device-logs-section {
margin-top: 20px;
h4 {
margin: 0 0 10px 0;
color: #303133;
font-size: 14px;
font-weight: 600;
}
:deep(.el-table) {
font-size: 12px;
}
}
</style>

View File

@@ -168,8 +168,7 @@ const formItemList = reactive([
// 获取指定订单的设备数量
const getDeviceCounts = async (deliveryId) => {
try {
console.log('=== 获取订单设备数量deliveryId:', deliveryId);
// 获取所有设备类型的数据
const [hostRes, earRes, collarRes] = await Promise.all([
pageDeviceList({ pageNum: 1, pageSize: 100, deliveryId: parseInt(deliveryId), deviceType: 1 }),
@@ -182,14 +181,7 @@ const getDeviceCounts = async (deliveryId) => {
const collarCount = collarRes.code === 200 ? collarRes.data.length : 0;
const totalCount = hostCount + earCount + collarCount;
console.log('=== 设备数量统计:', {
deliveryId,
hostCount,
earCount,
collarCount,
totalCount
});
// 存储设备数量
data.deviceCounts[deliveryId] = {
host: hostCount,
@@ -244,63 +236,40 @@ const getDataList = () => {
// 处理精确的创建时间查询
if (searchParams.createTime) {
params.createTime = searchParams.createTime;
console.log('精确创建时间查询:', searchParams.createTime);
}
// 处理精确的车牌号查询
if (searchParams.licensePlate) {
params.licensePlate = searchParams.licensePlate.trim();
console.log('精确车牌号查询:', params.licensePlate);
}
console.log('查询参数:', params);
inspectionList(params)
.then(async (ret) => {
console.log('入境检疫列表返回结果:', ret);
data.rows = ret.data.rows;
data.total = ret.data.total;
dataListLoading.value = false;
// 为每个订单获取设备数量
if (ret.data.rows && ret.data.rows.length > 0) {
console.log('=== 开始为每个订单获取设备数量');
for (const row of ret.data.rows) {
if (row.id) {
await getDeviceCounts(row.id);
}
}
console.log('=== 所有订单设备数量获取完成');
}
// 调试:检查第一行数据的字段
if (ret.data.rows && ret.data.rows.length > 0) {
const firstRow = ret.data.rows[0];
console.log('入境检疫第一行数据完整字段:', firstRow);
console.log('入境检疫关键字段检查:', {
status: firstRow.status,
statusDesc: firstRow.statusDesc,
registeredJbqCount: firstRow.registeredJbqCount,
earTagCount: firstRow.earTagCount,
driverName: firstRow.driverName,
licensePlate: firstRow.licensePlate
});
// 检查Word导出所需字段
console.log('Word导出字段检查:', {
supplierName: firstRow.supplierName,
buyerName: firstRow.buyerName,
startLocation: firstRow.startLocation,
createTime: firstRow.createTime,
endLocation: firstRow.endLocation,
driverName: firstRow.driverName,
driverMobile: firstRow.driverMobile,
licensePlate: firstRow.licensePlate,
ratedQuantity: firstRow.ratedQuantity,
landingEntruckWeight: firstRow.landingEntruckWeight,
emptyWeight: firstRow.emptyWeight,
firmPrice: firstRow.firmPrice
});
}
})
.catch(() => {
@@ -346,16 +315,7 @@ const download = async (row) => {
totalAmount: totalAmount
};
console.log('生成Word文档数据:', data);
console.log('原始数据字段检查:', {
supplierName: row.supplierName,
buyerName: row.buyerName,
supplierMobile: row.supplierMobile,
buyerMobile: row.buyerMobile,
fundName: row.fundName,
fundMobile: row.fundMobile
});
// 生成HTML内容
const htmlContent = `
<!DOCTYPE html>
@@ -601,8 +561,7 @@ const viewDevices = (row) => {
// 编辑运送清单
const editDelivery = async (row) => {
try {
console.log('[EDIT-DELIVERY] 准备编辑运送清单, ID:', row.id);
// 检查编辑对话框组件是否已加载
if (!editDialogRef.value || !editDialogRef.value.open) {
ElMessage.warning('编辑功能暂不可用,请刷新页面重试');
@@ -611,8 +570,7 @@ const editDelivery = async (row) => {
// 调用 detail 接口获取完整数据(包含 supplierId, buyerId, 设备信息等)
const detailRes = await getDeliveryDetail(row.id);
console.log('[EDIT-DELIVERY] 获取到详情数据:', detailRes);
if (detailRes.code === 200 && detailRes.data) {
// 传入完整的 detail 数据给 open() 方法
editDialogRef.value.open(detailRes.data);

View File

@@ -638,8 +638,7 @@ const collarLogForm = reactive({
});
// 查详情
const getDetail = () => {
console.log('查询运单详情, deliveryId:', route.query.id);
if (!route.query.id) {
console.warn('=== 警告deliveryId为空跳过运单详情查询');
return;
@@ -647,19 +646,12 @@ const getDetail = () => {
waybillDetail(route.query.id)
.then((res) => {
console.log('运单详情返回结果:', res);
if (res.code === 200) {
data.baseInfo = res.data.delivery ? res.data.delivery : {};
data.warnInfo = res.data.warningLog ? res.data.warningLog : {};
data.serverIds = res.data.serverIds ? res.data.serverIds : [];
console.log('基础信息:', {
driverName: data.baseInfo.driverName,
licensePlate: data.baseInfo.licensePlate,
carFrontPhoto: data.baseInfo.carFrontPhoto,
carBehindPhoto: data.baseInfo.carBehindPhoto,
driverId: data.baseInfo.driverId
});
// 查询车辆照片
if (data.baseInfo.licensePlate) {
loadVehiclePhotos();
@@ -678,10 +670,7 @@ const getDetail = () => {
const loadVehiclePhotos = () => {
// 后端已经从delivery/driver信息中获取了车身照片无需额外前端查询
// carFrontPhoto和carBehindPhoto应该已经由后端的DeliveryServiceImpl填充
console.log('车身照片信息:', {
carFrontPhoto: data.baseInfo.carFrontPhoto,
carBehindPhoto: data.baseInfo.carBehindPhoto
});
};
// 智能主机列表查询
@@ -700,10 +689,10 @@ const getHostList = () => {
deviceType: 1, // 智能主机设备类型
})
.then((res) => {
console.log('=== 主机设备API返回结果:', res);
data.hostDataListLoading = false;
if (res.code === 200) {
console.log('=== 主机设备数据:', res.data);
// 新API返回的是数组格式过滤出智能主机设备
const hostDevices = res.data.filter(device => device.deviceType === 1 || device.deviceType === '1');
data.hostRows = hostDevices || [];
@@ -712,13 +701,12 @@ const getHostList = () => {
if (hostDevices.length > 0) {
// 如果有主机设备,取第一个作为主要主机
data.serverIds = hostDevices[0].deviceId || hostDevices[0].sn || '';
console.log('=== 设置后的serverIds:', data.serverIds);
} else {
data.serverIds = '';
}
console.log('=== 设置后的hostRows:', data.hostRows);
console.log('=== 设置后的hostTotal:', data.hostTotal);
} else {
console.warn('获取主机设备信息失败:', res.msg);
data.hostRows = [];
@@ -819,16 +807,15 @@ const getEarList = () => {
deviceType: 2, // 智能耳标设备类型
})
.then((res) => {
console.log('=== 耳标设备API返回结果:', res);
data.dataListLoading = false;
if (res.code === 200) {
console.log('=== 耳标设备数据:', res.data);
// 新API返回的是数组格式需要过滤出智能耳标设备
const earDevices = res.data.filter(device => device.deviceType === 2 || device.deviceType === '2');
data.rows = earDevices || [];
data.total = earDevices.length || 0;
console.log('=== 设置后的rows:', data.rows);
console.log('=== 设置后的total:', data.total);
} else {
ElMessage.error(res.msg);
data.total = 0;
@@ -839,9 +826,7 @@ const getEarList = () => {
});
};
const earLogClick = (row) => {
console.log('=== 智能耳标日志点击 ===');
console.log('设备信息:', row);
data.deviceId = row.deviceId || row.sn || '';
data.earLogDialogVisible = true;
@@ -850,13 +835,12 @@ const earLogClick = (row) => {
deviceId: data.deviceId,
deliveryId: parseInt(route.query.id)
}).then((res) => {
console.log('=== 智能耳标日志API返回结果:', res);
if (res.code === 200) {
// 新API返回的是按60分钟分组的日志数据
data.earLogRows = res.data || [];
data.earLogTotal = res.data.length || 0;
console.log('=== 设置后的earLogRows:', data.earLogRows);
console.log('=== 设置后的earLogTotal:', data.earLogTotal);
} else {
ElMessage.error(res.msg || '获取智能耳标日志失败');
data.earLogRows = [];
@@ -872,20 +856,17 @@ const earLogClick = (row) => {
// 智能耳标运动轨迹
const earTrackClick = (row) => {
console.log('=== 智能耳标运动轨迹点击 ===');
console.log('设备信息:', row);
// 调用新的API获取60分钟间隔的轨迹数据
getEarTagTrajectory({
deviceId: row.deviceId || row.sn || '',
deliveryId: parseInt(route.query.id)
}).then((res) => {
console.log('=== 智能耳标轨迹API返回结果:', res);
if (res.code === 200 && res.data && res.data.length > 0) {
// 新API返回的是按60分钟分组的轨迹点数据
const trajectoryPoints = res.data;
console.log('=== 轨迹点数据:', trajectoryPoints);
// 使用TrackDialog显示轨迹
if (TrackDialogRef.value) {
const info = {
@@ -921,16 +902,15 @@ const getCollarList = () => {
deviceType: 4, // 智能项圈设备类型
})
.then((res) => {
console.log('=== 项圈设备API返回结果:', res);
data.collarDataListLoading = false;
if (res.code === 200) {
console.log('=== 项圈设备数据:', res.data);
// 新API返回的是数组格式需要过滤出智能项圈设备
const collarDevices = res.data.filter(device => device.deviceType === 4 || device.deviceType === '4');
data.collarRows = collarDevices || [];
data.collarTotal = collarDevices.length || 0;
console.log('=== 设置后的collarRows:', data.collarRows);
console.log('=== 设置后的collarTotal:', data.collarTotal);
} else {
ElMessage.error(res.msg);
data.collarTotal = 0;
@@ -942,9 +922,7 @@ const getCollarList = () => {
});
};
const collarLogClick = (row) => {
console.log('=== 智能项圈日志点击 ===');
console.log('设备信息:', row);
data.sn = row.sn || row.deviceId || '';
data.collarDialogVisible = true;
@@ -953,13 +931,12 @@ const collarLogClick = (row) => {
deviceId: data.sn,
deliveryId: parseInt(route.query.id)
}).then((res) => {
console.log('=== 智能项圈日志API返回结果:', res);
if (res.code === 200) {
// 新API返回的是按60分钟分组的日志数据
data.collarLogRows = res.data || [];
data.collarLogTotal = res.data.length || 0;
console.log('=== 设置后的collarLogRows:', data.collarLogRows);
console.log('=== 设置后的collarLogTotal:', data.collarLogTotal);
} else {
ElMessage.error(res.msg || '获取智能项圈日志失败');
data.collarLogRows = [];
@@ -1054,20 +1031,17 @@ const getCollarLogList = () => {
};
// 查看运动轨迹
const collarTrackClick = (row) => {
console.log('=== 智能项圈运动轨迹点击 ===');
console.log('设备信息:', row);
// 调用新的API获取60分钟间隔的轨迹数据
getCollarTrajectory({
deviceId: row.sn || row.deviceId || '',
deliveryId: parseInt(route.query.id)
}).then((res) => {
console.log('=== 智能项圈轨迹API返回结果:', res);
if (res.code === 200 && res.data && res.data.length > 0) {
// 新API返回的是按60分钟分组的轨迹点数据
const trajectoryPoints = res.data;
console.log('=== 轨迹点数据:', trajectoryPoints);
// 使用TrackDialog显示轨迹
if (TrackDialogRef.value) {
const info = {
@@ -1089,9 +1063,7 @@ const collarTrackClick = (row) => {
// 智能主机操作函数
const hostLogClick = (row) => {
console.log('=== 智能主机日志点击 ===');
console.log('设备信息:', row);
data.deviceId = row.deviceId || row.sn || '';
data.hostLogDialogVisible = true;
@@ -1100,13 +1072,12 @@ const hostLogClick = (row) => {
deviceId: data.deviceId,
deliveryId: parseInt(route.query.id)
}).then((res) => {
console.log('=== 智能主机日志API返回结果:', res);
if (res.code === 200) {
// 新API返回的是按60分钟分组的日志数据
data.hostLogRows = res.data || [];
data.hostLogTotal = res.data.length || 0;
console.log('=== 设置后的hostLogRows:', data.hostLogRows);
console.log('=== 设置后的hostLogTotal:', data.hostLogTotal);
} else {
ElMessage.error(res.msg || '获取智能主机日志失败');
data.hostLogRows = [];
@@ -1121,20 +1092,17 @@ const hostLogClick = (row) => {
};
const hostTrackClick = (row) => {
console.log('=== 智能主机运动轨迹点击 ===');
console.log('设备信息:', row);
// 调用新的API获取60分钟间隔的轨迹数据
getHostTrajectory({
deviceId: row.deviceId || row.sn || '',
deliveryId: parseInt(route.query.id)
}).then((res) => {
console.log('=== 智能主机轨迹API返回结果:', res);
if (res.code === 200 && res.data && res.data.length > 0) {
// 新API返回的是按60分钟分组的轨迹点数据
const trajectoryPoints = res.data;
console.log('=== 轨迹点数据:', trajectoryPoints);
// 使用TrackDialog显示轨迹
if (TrackDialogRef.value) {
const info = {
@@ -1167,7 +1135,7 @@ const totalRegisteredDevices = computed(() => {
const earCount = data.total || 0;
const collarCount = data.collarTotal || 0;
const total = hostCount + earCount + collarCount;
console.log('=== 计算设备总数 - 主机:', hostCount, '耳标:', earCount, '项圈:', collarCount, '总计:', total);
return total;
});
@@ -1195,9 +1163,7 @@ onMounted(() => {
data.status = route.query.status;
data.length = route.query.length;
console.log('=== 详情页面初始化deliveryId:', route.query.id);
console.log('=== 路由参数:', route.query);
// 检查deliveryId是否存在
if (!route.query.id) {
console.warn('=== 警告deliveryId为空无法加载详情页面');
@@ -1208,7 +1174,7 @@ onMounted(() => {
// 检查deliveryId是否存在存在时才测试设备关联情况
testDeliveryDevices({ deliveryId: route.query.id })
.then(res => {
console.log('=== 测试设备关联结果:', res);
})
.catch(err => {
console.error('=== 测试设备关联失败:', err);

View File

@@ -172,7 +172,7 @@ const goBack = () => {
// Tab切换
const handleTabChange = (tabName) => {
console.log('切换到Tab:', tabName);
};
// 获取智能主机列表
@@ -191,7 +191,7 @@ const getHostList = async () => {
deviceType: 1,
});
console.log('主机设备API返回:', res);
if (res.code === 200) {
const hostDevices = res.data.filter(device => device.deviceType === 1 || device.deviceType === '1');
hostList.value = hostDevices || [];
@@ -223,7 +223,7 @@ const getEarList = async () => {
deviceType: 2,
});
console.log('耳标设备API返回:', res);
if (res.code === 200) {
const earDevices = res.data.filter(device => device.deviceType === 2 || device.deviceType === '2');
earList.value = earDevices || [];
@@ -255,7 +255,7 @@ const getCollarList = async () => {
deviceType: 4,
});
console.log('项圈设备API返回:', res);
if (res.code === 200) {
const collarDevices = res.data.filter(device => device.deviceType === 4 || device.deviceType === '4');
collarList.value = collarDevices || [];
@@ -310,8 +310,6 @@ const unbindDevice = (device, deviceType) => {
};
onMounted(() => {
console.log('设备管理页面初始化deliveryId:', route.query.deliveryId);
console.log('运单号:', route.query.deliveryNumber);
if (!route.query.deliveryId) {
console.warn('deliveryId为空无法加载设备列表');

View File

@@ -112,7 +112,7 @@ const getList = async () => {
const searchClick = async () => {
form.pageNum = 1;
await getList();
console.log('searchClick');
};
const resetClick = async (el) => {
form.pageNum = 1;

View File

@@ -103,7 +103,7 @@ const getList = async () => {
const searchClick = async () => {
form.pageNum = 1;
await getList();
console.log('searchClick');
};
const resetClick = async (el) => {
form.pageNum = 1;

View File

@@ -185,8 +185,7 @@ const getTrack = () => {
})
.then((res) => {
data.trackLoading = false;
console.log('=== 查询轨迹API返回结果:', res);
if (res.code === 200 && res.data && res.data.length > 0) {
data.mapShow = true;
data.path = [];
@@ -206,14 +205,14 @@ const getTrack = () => {
if (data.path.length > 0) {
data.startMark = data.path[0];
data.endMark = data.path[data.path.length - 1];
console.log('轨迹查询成功,共', data.path.length, '个轨迹点');
} else {
console.log('没有有效的轨迹点');
data.noTrack = true;
ElMessage.warning('该时间范围内暂无有效轨迹点');
}
} else {
console.log('没有轨迹数据');
ElMessage.warning('该时间范围内暂无轨迹数据');
data.noTrack = true;
}
@@ -305,18 +304,14 @@ const onShowTrackDialog = (row) => {
// 如果传入了trajectoryPoints直接使用这些轨迹点
if (row.trajectoryPoints && row.trajectoryPoints.length > 0) {
console.log('=== trackDialog: 直接使用传入的轨迹点 ===');
console.log('轨迹点数量:', row.trajectoryPoints.length);
console.log('轨迹点数据:', row.trajectoryPoints);
data.mapShow = true;
data.path = [];
row.trajectoryPoints.forEach((item, index) => {
const lng = parseFloat(item.longitude || item.lng || 0);
const lat = parseFloat(item.latitude || item.lat || 0);
console.log(`轨迹点${index}: latitude=${item.latitude}, longitude=${item.longitude}, lng=${lng}, lat=${lat}`);
// 检查经纬度是否有效
if (lng !== 0 && lat !== 0 && !isNaN(lng) && !isNaN(lat)) {
data.path.push({
@@ -328,13 +323,11 @@ const onShowTrackDialog = (row) => {
}
});
console.log('最终path数据:', data.path);
if (data.path.length > 0) {
data.startMark = data.path[0]; // 起点
data.endMark = data.path[data.path.length - 1]; // 终点
console.log('起点:', data.startMark);
console.log('终点:', data.endMark);
} else {
console.error('没有有效的轨迹点,显示空状态');
data.noTrack = true;

View File

@@ -200,17 +200,11 @@ const onSubmit = async (val) => {
const generateRoutes = async () => {
try {
const ret = await getUserMenu();
console.log('=== 获取用户菜单 ===', ret.data);
// 检查用户权限
const userStore = useUserStore();
const isSuperAdmin = userStore.permissions.includes('*:*:*') || userStore.roles.includes('admin');
console.log('=== 用户权限检查 ===', {
permissions: userStore.permissions,
roles: userStore.roles,
isSuperAdmin: isSuperAdmin
});
// 查找第一个有pageUrl的菜单项type=1表示菜单
const findFirstMenuWithUrl = (menus) => {
// 按sort排序确保按顺序查找
@@ -219,7 +213,7 @@ const generateRoutes = async () => {
for (const menu of sortedMenus) {
// 查找type=1菜单且有pageUrl的项目
if (menu.type === 1 && menu.pageUrl) {
console.log('=== 找到第一个菜单页面 ===', menu);
return menu;
}
}
@@ -234,15 +228,15 @@ const generateRoutes = async () => {
if (isSuperAdmin) {
// 超级管理员优先跳转到系统管理页面
targetPath = '/system/post';
console.log('=== 超级管理员,跳转到系统管理页面 ===', targetPath);
} else if (firstMenu && firstMenu.pageUrl) {
// 普通用户跳转到第一个有权限的菜单页面
targetPath = firstMenu.pageUrl;
console.log('=== 普通用户,跳转到第一个菜单页面 ===', targetPath);
} else {
// 默认跳转到装车订单页面
targetPath = '/shipping/loadingOrder';
console.log('=== 没有找到有效菜单,跳转到默认页面 ===', targetPath);
}
// 等待路由完全生成后再执行跳转
@@ -251,7 +245,7 @@ const generateRoutes = async () => {
// 确保权限store的路由生成完成
const permissionStore = usePermissionStore();
if (!permissionStore.routeFlag) {
console.log('=== 等待路由生成完成 ===');
await permissionStore.generateRoutes();
}
@@ -263,13 +257,13 @@ const generateRoutes = async () => {
// 使用replace而不是push避免路由警告
try {
await router.replace({ path: targetPath });
console.log('=== 成功跳转到目标页面 ===', targetPath);
} catch (error) {
console.warn('Failed to navigate to', targetPath, 'error:', error);
// 如果跳转失败,尝试跳转到首页
try {
await router.replace({ path: '/' });
console.log('=== 跳转到首页 ===');
} catch (homeError) {
console.error('Failed to navigate to home:', homeError);
}
@@ -280,7 +274,7 @@ const generateRoutes = async () => {
// 获取菜单失败时跳转到首页
try {
await router.push({ path: '/' });
console.log('=== 获取菜单失败,跳转到首页 ===');
} catch (navError) {
console.error('Failed to navigate to home:', navError);
}

View File

@@ -226,10 +226,7 @@ const loadUserList = async () => {
const handleUserChange = async (row) => {
if (!row) return;
console.log('=== 菜单权限管理 - 用户选择改变 ===');
console.log('选择的用户:', row);
console.log('用户ID:', row.id);
currentUser.value = row;
await loadMenuTree();
await loadUserMenus(row.id);
@@ -244,7 +241,7 @@ const loadMenuTree = async () => {
// 过滤掉按钮权限type=2只保留菜单type=0,1
const filteredTree = filterMenuTree(res.data || []);
menuTree.value = filteredTree;
console.log('=== 菜单权限管理 - 过滤后的菜单树 ===', filteredTree);
}
} catch (error) {
console.error('加载菜单树失败:', error);
@@ -285,12 +282,7 @@ const loadUserMenus = async (userId) => {
const menuOnlyIds = await filterMenuOnlyIds(allMenuIds);
checkedMenuIds.value = menuOnlyIds;
console.log('=== 菜单权限管理 - 过滤后的菜单权限 ===', {
userId: userId,
allMenuIds: allMenuIds,
menuOnlyIds: menuOnlyIds
});
await nextTick();
if (menuTreeRef.value) {
menuTreeRef.value.setCheckedKeys(checkedMenuIds.value);
@@ -369,11 +361,6 @@ const handleSaveMenuPermissions = async () => {
// 过滤掉按钮权限,只保留菜单权限
const menuOnlyIds = await filterMenuOnlyIds(allKeys);
console.log('=== 保存菜单权限 ===', {
user: currentUser.value,
allKeys: allKeys,
menuOnlyIds: menuOnlyIds
});
saveLoading.value = true;
try {
@@ -390,7 +377,7 @@ const handleSaveMenuPermissions = async () => {
const permissionStore = usePermissionStore();
await permissionStore.refreshPermissions();
ElMessage.success('权限已保存并刷新成功!');
console.log('权限数据已刷新');
} catch (error) {
console.error('刷新权限失败:', error);
ElMessage.warning('权限已保存,但刷新失败,请手动刷新页面');
@@ -443,12 +430,6 @@ const handleQuickAssignAll = async () => {
const menuOnlyMenus = allMenus.filter(menu => menu.type !== 2);
const menuOnlyIds = menuOnlyMenus.map(menu => menu.id);
console.log('=== 一键分配全部菜单权限 ===', {
user: currentUser.value,
totalMenus: allMenus.length,
menuOnlyMenus: menuOnlyMenus.length,
menuOnlyIds: menuOnlyIds
});
// 分配所有菜单权限
const res = await assignUserMenus({
@@ -467,7 +448,7 @@ const handleQuickAssignAll = async () => {
const permissionStore = usePermissionStore();
await permissionStore.refreshPermissions();
ElMessage.success('权限已保存并刷新成功!');
console.log('权限数据已刷新');
} catch (error) {
console.error('刷新权限失败:', error);
ElMessage.warning('权限已保存,但刷新失败,请手动刷新页面');
@@ -525,7 +506,7 @@ const handleClearUserPermissions = async () => {
const permissionStore = usePermissionStore();
await permissionStore.refreshPermissions();
ElMessage.success('权限已清空并刷新成功!');
console.log('权限数据已刷新');
} catch (error) {
console.error('刷新权限失败:', error);
ElMessage.warning('权限已清空,但刷新失败,请手动刷新页面');

View File

@@ -184,10 +184,7 @@ const loadUserList = async () => {
const handleUserChange = async (row) => {
if (!row) return;
console.log('=== 操作权限管理 - 用户选择改变 ===');
console.log('选择的用户:', row);
console.log('用户ID:', row.id);
currentUser.value = row;
await loadPermissionTree();
await loadUserPermissions(row.id);
@@ -195,22 +192,18 @@ const handleUserChange = async (row) => {
// 加载用户已分配的权限
const loadUserPermissions = async (userId) => {
console.log('=== 加载用户权限 ===');
console.log('userId:', userId);
try {
const res = await getUserMenuIds(userId);
console.log('用户权限API响应:', res);
if (res.code === 200) {
const menuIds = res.data || [];
console.log('已分配的用户权限IDs:', menuIds);
// 设置权限树选中状态
await nextTick();
if (userPermissionTreeRef.value) {
userPermissionTreeRef.value.setCheckedKeys(menuIds);
console.log('用户权限树已设置选中状态');
}
}
} catch (error) {
@@ -242,39 +235,29 @@ const handleSaveUserPermissions = async () => {
return;
}
console.log('=== 保存用户权限 ===');
console.log('当前用户:', currentUser.value);
// 获取选中的节点
const checkedKeys = userPermissionTreeRef.value.getCheckedKeys();
const halfCheckedKeys = userPermissionTreeRef.value.getHalfCheckedKeys();
const allKeys = [...checkedKeys, ...halfCheckedKeys];
console.log('选中的权限IDs:', checkedKeys);
console.log('半选中的权限IDs:', halfCheckedKeys);
console.log('所有权限IDs:', allKeys);
const saveData = {
userId: currentUser.value.id,
menuIds: allKeys,
};
console.log('保存数据:', saveData);
saveLoading.value = true;
try {
const res = await assignUserMenus(saveData);
console.log('保存API响应:', res);
if (res.code === 200) {
ElMessage.success(`用户 ${currentUser.value.name} 的操作权限保存成功!`);
console.log('用户权限保存成功');
// 权限保存成功后,刷新权限数据
try {
const permissionStore = usePermissionStore();
await permissionStore.refreshPermissions();
ElMessage.success('权限已保存并刷新成功!');
console.log('权限数据已刷新');
} catch (error) {
console.error('刷新权限失败:', error);
ElMessage.warning('权限已保存,但刷新失败,请手动刷新页面');
@@ -314,18 +297,13 @@ const handleClearUserPermissions = async () => {
return;
}
console.log('=== 清空用户权限 ===');
console.log('当前用户:', currentUser.value);
clearLoading.value = true;
try {
const res = await clearUserMenus(currentUser.value.id);
console.log('清空API响应:', res);
if (res.code === 200) {
ElMessage.success(`用户 ${currentUser.value.name} 的权限已清空!`);
console.log('用户权限清空成功');
// 重新加载用户权限(显示空权限)
await loadUserPermissions(currentUser.value.id);
@@ -334,7 +312,7 @@ const handleClearUserPermissions = async () => {
const permissionStore = usePermissionStore();
await permissionStore.refreshPermissions();
ElMessage.success('权限已清空并刷新成功!');
console.log('权限数据已刷新');
} catch (error) {
console.error('刷新权限失败:', error);
ElMessage.warning('权限已清空,但刷新失败,请手动刷新页面');

View File

@@ -106,8 +106,7 @@ const getDataList = async () => {
data.dataListLoading = true;
try {
console.log('开始查询可分配设备列表...');
const params = {
pageNum: form.pageNum,
pageSize: form.pageSize,
@@ -115,11 +114,9 @@ const getDataList = async () => {
deviceType: data.deviceType ? parseInt(data.deviceType) : null,
};
console.log('查询参数:', params);
const res = await iotDeviceAssignableList(params);
console.log('API返回结果:', res);
if (res.code === 200) {
const rawData = res.data?.rows || [];
const total = res.data?.total || 0;
@@ -144,21 +141,14 @@ const getDataList = async () => {
break;
}
console.log(`处理设备 ${item.deviceId}:`, {
deviceType: item.deviceType,
deviceTypeName: processedItem.deviceTypeName,
isAssigned: item.isAssigned
});
return processedItem;
});
data.total = total;
data.dataListLoading = false;
console.log('最终可分配设备列表:', data.rows);
console.log('总设备数量:', data.total);
} else {
console.error('API返回错误:', res);
data.dataListLoading = false;
@@ -199,13 +189,12 @@ const onClickSave = () => {
carNumber: data.licensePlate, // 车牌号
};
console.log('设备分配参数:', params);
data.saveLoading = true;
iotDeviceAssign(params)
.then((res) => {
data.saveLoading = false;
console.log('设备分配结果:', res);
if (res.code === 200) {
ElMessage({
message: res.msg,

View File

@@ -856,8 +856,7 @@ const buildSubmitData = () => {
}
});
console.log('[buildSubmitData] 最终提交数据已处理undefined:', data);
return data;
};
@@ -874,9 +873,7 @@ const open = async (editData = null) => {
loadOrderList()
]);
console.log('[OPEN-DIALOG] 所有下拉列表加载完成');
console.log('[OPEN-DIALOG] 车辆列表:', vehicleOptions.value);
// 如果传入了编辑数据,则填充表单
if (editData) {
fillFormWithEditData(editData);
@@ -885,8 +882,7 @@ const open = async (editData = null) => {
// 填充编辑数据到表单
const fillFormWithEditData = (editData) => {
console.log('[EDIT-FILL] 开始填充编辑数据:', editData);
// editData 包含两个部分:
// 1. editData.delivery - 运单基本信息
// 2. editData 的根级字段 - supplierId, buyerId, eartagIds, collarIds, serverIds
@@ -899,13 +895,10 @@ const fillFormWithEditData = (editData) => {
// 发货方和采购方:优先使用根级的 supplierId 和 buyerId
formData.shipper = editData.supplierId || (delivery.supplierId ? parseInt(delivery.supplierId) : null);
formData.buyer = editData.buyerId || delivery.buyerId || null;
console.log('[EDIT-FILL] 发货方ID:', formData.shipper, '采购方ID:', formData.buyer);
// 车牌号
formData.plateNumber = delivery.licensePlate || '';
console.log('[EDIT-FILL] 车牌号:', formData.plateNumber);
console.log('[EDIT-FILL] 当前车辆列表:', vehicleOptions.value);
// 检查车牌号是否在车辆列表中
const vehicleExists = vehicleOptions.value.find(v => v.licensePlate === formData.plateNumber);
if (!vehicleExists && formData.plateNumber) {
@@ -918,17 +911,17 @@ const fillFormWithEditData = (editData) => {
// 设备信息:从根级读取
if (editData.serverIds && editData.serverIds !== '') {
formData.serverId = editData.serverIds;
console.log('[EDIT-FILL] 主机ID:', formData.serverId);
}
if (editData.eartagIds && Array.isArray(editData.eartagIds) && editData.eartagIds.length > 0) {
formData.eartagIds = editData.eartagIds;
console.log('[EDIT-FILL] 耳标IDs:', formData.eartagIds);
}
if (editData.collarIds && Array.isArray(editData.collarIds) && editData.collarIds.length > 0) {
formData.collarIds = editData.collarIds;
console.log('[EDIT-FILL] 项圈IDs:', formData.collarIds);
}
// 地址和坐标
@@ -972,7 +965,7 @@ const fillFormWithEditData = (editData) => {
// 保存编辑的ID用于区分是新增还是编辑
formData.editId = delivery.id;
console.log('[EDIT-FILL] 表单数据已填充:', formData);
ElMessage.success('已加载运单数据');
};
@@ -1109,8 +1102,7 @@ const handleOrderChange = async (orderId) => {
formData.shipper = sellerId ? parseInt(sellerId) : null;
formData.buyer = buyerId ? parseInt(buyerId) : null;
console.log('[订单选择] 选中的订单ID:', orderId);
console.log('[订单选择] orderId已保存到formData.orderId:', formData.orderId);
ElMessage.success('已自动填充发货方和采购方信息');
}
} catch (error) {
@@ -1128,15 +1120,15 @@ const handleDriverChange = (driverId) => {
const driver = driverOptions.value.find(item => item.id === driverId);
if (driver && driver.mobile) {
formData.driverPhone = driver.mobile;
console.log('[司机选择] 司机ID:', driverId, ', 已自动填充手机号:', driver.mobile);
ElMessage.success('已自动填充司机手机号');
} else {
console.log('[司机选择] 司机ID:', driverId, ', 但未找到手机号');
}
} else {
formData.driverId = null;
formData.driverPhone = '';
console.log('[司机选择] 司机已清除');
}
};
@@ -1195,7 +1187,7 @@ const updateSelectedDevicesDeliveryId = async (deliveryId) => {
});
}
console.log(`成功更新 ${devicesToUpdate.length} 个设备的delivery_id和car_number: ${formData.plateNumber}`);
} catch (error) {
console.error('更新设备delivery_id和car_number失败:', error);
// 不阻止流程,只记录错误
@@ -1222,15 +1214,13 @@ const handleSubmit = () => {
console.group('[CREATE-DELIVERY] 提交前检查');
try {
const formSnapshot = JSON.parse(JSON.stringify(formData));
console.log('表单快照 formData:', formSnapshot);
console.log('地图坐标校验: startLon/startLat/endLon/endLat =', formData.startLon, formData.startLat, formData.endLon, formData.endLat);
console.log('Token 是否存在:', !!userStore.$state.token);
} catch (e) {
console.warn('表单快照序列化失败:', e);
}
const submitData = buildSubmitData();
console.log('最终请求体 payload:', submitData);
console.table(Object.keys(submitData).map(k => ({ key: k, type: typeof submitData[k], value: Array.isArray(submitData[k]) ? `Array(len=${submitData[k].length})` : submitData[k] })));
if (submitData.eartagIds && submitData.eartagIds.some(v => typeof v === 'string')) {
console.warn('eartagIds 仍包含字符串,将被后端拒绝:', submitData.eartagIds);
@@ -1244,17 +1234,17 @@ const handleSubmit = () => {
// 判断是编辑还是新增
if (formData.editId) {
// 编辑模式:调用更新接口
console.log('[EDIT-DELIVERY] 编辑模式运单ID:', formData.editId);
submitData.deliveryId = formData.editId; // 添加deliveryId字段后端需要
res = await shippingApi.updateDeliveryInfo(submitData);
} else {
// 新增模式:调用创建接口
console.log('[CREATE-DELIVERY] 新增模式');
res = await createDelivery(submitData);
}
console.group(formData.editId ? '[EDIT-DELIVERY] 响应日志' : '[CREATE-DELIVERY] 响应日志');
console.log('完整响应:', res);
console.groupEnd();
if (res.code === 200) {
@@ -1324,10 +1314,10 @@ const handleStartMarkerDrag = (e) => {
// 打开起点地图并处理地址搜索
const openStartLocationMap = () => {
console.log('openStartLocationMap 被调用');
// 如果输入框有地址,先进行地理编码
if (formData.startLocation && formData.startLocation.trim()) {
console.log('搜索起点地址:', formData.startLocation);
// 先打开地图对话框,让地图组件加载
showStartLocationMap.value = true;
@@ -1339,31 +1329,31 @@ const openStartLocationMap = () => {
const geocoder = new window.BMap.Geocoder();
geocoder.getPoint(formData.startLocation, (point) => {
if (point) {
console.log('找到起点坐标:', point.lng, point.lat);
// 搜索到坐标,更新地图中心点和标记
formData.startLon = point.lng;
formData.startLat = point.lat;
// 更新地图中心点
ElMessage.success('已定位到该地址');
} else {
console.log('未找到起点地址');
ElMessage.warning('未找到该地址,请在地图上手动选择');
}
});
}
}, 500);
} else {
console.log('未输入起点地址,直接打开地图');
showStartLocationMap.value = true;
}
};
// 打开目的地地图并处理地址搜索
const openEndLocationMap = () => {
console.log('openEndLocationMap 被调用');
// 如果输入框有地址,先进行地理编码
if (formData.endLocation && formData.endLocation.trim()) {
console.log('搜索目的地地址:', formData.endLocation);
// 先打开地图对话框,让地图组件加载
showEndLocationMap.value = true;
@@ -1373,21 +1363,21 @@ const openEndLocationMap = () => {
const geocoder = new window.BMap.Geocoder();
geocoder.getPoint(formData.endLocation, (point) => {
if (point) {
console.log('找到目的地坐标:', point.lng, point.lat);
// 搜索到坐标,更新地图中心点和标记
formData.endLon = point.lng;
formData.endLat = point.lat;
// 更新地图中心点
ElMessage.success('已定位到该地址');
} else {
console.log('未找到目的地地址');
ElMessage.warning('未找到该地址,请在地图上手动选择');
}
});
}
}, 500);
} else {
console.log('未输入目的地地址,直接打开地图');
showEndLocationMap.value = true;
}
};
@@ -1428,7 +1418,7 @@ const makeUploadSuccessSetter = (key) => (response) => {
const url = resolveUploadUrl(response);
if (response?.code === 200 && url) {
formData[key] = url;
console.log(`[UPLOAD] ${key} =`, url);
ElMessage.success('上传成功');
} else {
console.warn(`[UPLOAD] 未识别的响应结构:`, response);

View File

@@ -632,7 +632,7 @@ const onClickSave = () => {
data.saveLoading = false;
});
} else {
console.log('error submit!');
}
});
}
@@ -650,9 +650,8 @@ const onShowDialog = (val) => {
if (val) {
Object.assign(ruleForm, val);
editId.value = val.id;
console.log(val.supplierId);
// console.log(data.purchaserOptions);
// 资金方
// // 资金方
if (data.financeOptions && data.financeOptions.length > 0) {
const financeObj = data.financeOptions.find((item) => item.id == val.fundId);
ruleForm.financeName = financeObj ? financeObj.mobile : '';
@@ -663,7 +662,7 @@ const onShowDialog = (val) => {
// 供应商
if (val.supplierId && data.supplierOptions && data.supplierOptions.length > 0) {
val.supplier = val.supplierId.split(',').map((id) => Number(id));
console.log(val.supplier);
ruleForm.supplierName = data.supplierOptions.filter((supplier) => val.supplier.includes(supplier.id)).map((supplier) => supplier.mobile);
} else {
val.supplier = [];

View File

@@ -450,16 +450,7 @@ const autoFillFormData = (apiData) => {
ruleForm.controlSlotVideo = apiData.controlSlotVideo || '';
ruleForm.cattleLoadingCircleVideo = apiData.cattleLoadingCircleVideo || '';
console.log('表单数据已自动填充:', ruleForm);
console.log('API数据映射详情:', {
deliveryId: apiData.id,
estimatedDeliveryTime: apiData.estimatedDeliveryTime,
emptyWeight: apiData.emptyWeight,
entruckWeight: apiData.entruckWeight,
landingEntruckWeight: apiData.landingEntruckWeight,
quarantineTickeyUrl: apiData.quarantineTickeyUrl,
poundListImg: apiData.poundListImg
});
};
// 查询详情
@@ -468,10 +459,9 @@ const getOrderDetail = () => {
orderLoadDetail({
deliveryId: data.deliveryId,
}).then((res) => {
console.log('getOrderDetail API 响应:', res);
if (res.code === 200) {
console.log('API 返回的数据:', res.data);
// 自动填充表单数据
autoFillFormData(res.data);
@@ -495,15 +485,12 @@ const getDevicesByOrder = () => {
return;
}
console.log('=== 开始获取订单设备信息deliveryId:', data.deliveryId);
// 先调用测试接口检查订单设备数据
testOrderDevices(parseInt(data.deliveryId)).then((res) => {
console.log('=== 测试接口返回结果:', res);
if (res.code === 200) {
console.log('=== 订单设备数据:', res.data);
console.log('=== 设备总数:', res.data.totalDevices);
console.log('=== 设备列表:', res.data.devices);
}
}).catch((error) => {
console.error('=== 测试接口调用失败:', error);
@@ -516,29 +503,22 @@ const getDevicesByOrder = () => {
deliveryId: parseInt(data.deliveryId),
deviceType: 2, // 智能耳标
}).then((res) => {
console.log('=== 智能耳标设备API返回结果:', res);
console.log('=== API返回的原始数据:', res.data);
console.log('=== 数据类型:', typeof res.data, '是否为数组:', Array.isArray(res.data));
if (res.code === 200) {
if (res.data && Array.isArray(res.data)) {
console.log('=== 原始设备数据:', res.data);
// 过滤出智能耳标设备并转换为需要的格式
const earDevices = res.data.filter(device => {
console.log('=== 检查设备:', device, 'deviceType:', device.deviceType, '类型:', typeof device.deviceType);
return device.deviceType === 2 || device.deviceType === '2';
});
console.log('=== 过滤后的智能耳标设备:', earDevices);
data.deliveryDevices = earDevices.map(device => ({
deviceId: device.deviceId,
bindWeight: device.bindWeight || '', // 如果有绑定重量则使用,否则为空
}));
console.log('=== 设置后的智能耳标设备:', data.deliveryDevices);
console.log('=== data.deliveryDevices长度:', data.deliveryDevices.length);
} else {
console.warn('API返回的数据不是数组格式:', res.data);
data.deliveryDevices = [];
@@ -559,29 +539,22 @@ const getDevicesByOrder = () => {
deliveryId: parseInt(data.deliveryId),
deviceType: 4, // 智能项圈
}).then((res) => {
console.log('=== 智能项圈设备API返回结果:', res);
console.log('=== API返回的原始数据:', res.data);
console.log('=== 数据类型:', typeof res.data, '是否为数组:', Array.isArray(res.data));
if (res.code === 200) {
if (res.data && Array.isArray(res.data)) {
console.log('=== 原始设备数据:', res.data);
// 过滤出智能项圈设备并转换为需要的格式
const collarDevices = res.data.filter(device => {
console.log('=== 检查设备:', device, 'deviceType:', device.deviceType, '类型:', typeof device.deviceType);
return device.deviceType === 4 || device.deviceType === '4';
});
console.log('=== 过滤后的智能项圈设备:', collarDevices);
data.xqDevices = collarDevices.map(device => ({
deviceId: device.deviceId,
bindWeight: device.bindWeight || '', // 如果有绑定重量则使用,否则为空
}));
console.log('=== 设置后的智能项圈设备:', data.xqDevices);
console.log('=== data.xqDevices长度:', data.xqDevices.length);
} else {
console.warn('API返回的数据不是数组格式:', res.data);
data.xqDevices = [];
@@ -615,7 +588,7 @@ const getHostList = () => {
...(data.hostNumber ? { deviceId: data.hostNumber } : {}),
// 不传递deliveryId获取所有可用的主机
}).then((res) => {
console.log('=== 智能主机设备API返回结果:', res);
data.hostLoading = false;
if (res.code === 200) {
// 过滤出智能主机设备
@@ -637,7 +610,7 @@ const getHostList = () => {
}));
data.hostTotal = hostDevices.length;
console.log('=== 设置后的智能主机选项:', data.hostOptions);
} else {
console.error('获取智能主机设备失败:', res.msg);
data.hostOptions = [];
@@ -760,12 +733,9 @@ const onClickSave = () => {
// 确保 deliveryId 是数字类型
const saveData = { ...ruleForm };
console.log('保存时的 deliveryId:', saveData.deliveryId, '类型:', typeof saveData.deliveryId);
console.log('选择的智能主机:', saveData.serverDeviceSn);
if (saveData.deliveryId) {
const parsedId = parseInt(saveData.deliveryId);
console.log('解析后的 ID:', parsedId, 'isNaN:', isNaN(parsedId));
if (isNaN(parsedId)) {
ElMessage.error('运送清单ID格式错误');
data.saveLoading = false;
@@ -781,8 +751,7 @@ const onClickSave = () => {
// 先保存装车信息
orderLoadSave(saveData).then((res) => {
if (res.code === 200) {
console.log('装车信息保存成功:', res);
// 如果选择了智能主机需要更新主机的delivery_id
if (saveData.serverDeviceSn) {
updateHostDeliveryId(saveData.serverDeviceSn, saveData.deliveryId);
@@ -803,17 +772,14 @@ const onClickSave = () => {
// 更新智能主机的delivery_id
const updateHostDeliveryId = (hostDeviceId, deliveryId) => {
console.log('=== 开始更新智能主机delivery_id ===');
console.log('主机设备ID:', hostDeviceId);
console.log('订单ID:', deliveryId);
// 调用后端接口更新主机的delivery_id
updateDeviceDeliveryId({
deviceId: hostDeviceId,
deliveryId: deliveryId
}).then((res) => {
if (res.code === 200) {
console.log('智能主机delivery_id更新成功:', res);
// 更新设备重量
updateDeviceWeightsLocal();
} else {
@@ -830,8 +796,7 @@ const updateHostDeliveryId = (hostDeviceId, deliveryId) => {
// 更新设备重量
const updateDeviceWeightsLocal = (customDevices = null) => {
console.log('=== 开始更新设备重量 ===');
// 收集所有设备的重量信息
const devices = [];
@@ -865,10 +830,9 @@ const updateDeviceWeightsLocal = (customDevices = null) => {
});
}
console.log('需要更新重量的设备:', devices);
if (devices.length === 0) {
console.log('没有设备需要更新重量,直接完成保存');
completeSave();
return;
}
@@ -879,7 +843,7 @@ const updateDeviceWeightsLocal = (customDevices = null) => {
devices: devices
}).then((res) => {
if (res.code === 200) {
console.log('设备重量更新成功:', res);
completeSave();
} else {
console.error('设备重量更新失败:', res.msg);
@@ -900,27 +864,19 @@ const checkOrderHostDevice = () => {
return;
}
console.log('=== 检查订单绑定的智能主机 ===');
console.log('订单ID:', data.deliveryId);
console.log('调用API前的ruleForm.serverDeviceSn:', ruleForm.serverDeviceSn);
getOrderHostDevice(parseInt(data.deliveryId)).then((res) => {
console.log('=== 订单绑定主机查询结果:', res);
console.log('API返回的完整响应:', JSON.stringify(res, null, 2));
if (res.code === 200) {
if (res.data) {
// 订单已绑定智能主机,自动填充
console.log('订单已绑定智能主机:', res.data.deviceId);
console.log('设置前的ruleForm.serverDeviceSn:', ruleForm.serverDeviceSn);
ruleForm.serverDeviceSn = res.data.deviceId;
console.log('设置后的ruleForm.serverDeviceSn:', ruleForm.serverDeviceSn);
console.log('自动填充智能主机成功');
} else {
// 订单未绑定智能主机
console.log('订单未绑定智能主机');
ruleForm.serverDeviceSn = '';
console.log('清空智能主机选择');
}
} else {
console.error('查询订单绑定主机失败:', res.msg);
@@ -959,8 +915,7 @@ const onShowDialog = (row, apiData = null) => {
nextTick(() => {
data.deliveryId = row.id;
ruleForm.deliveryId = row.id;
console.log('设置 deliveryId:', row.id, '类型:', typeof row.id);
// 如果提供了API数据直接填充表单
if (apiData) {
autoFillFormData(apiData);

View File

@@ -129,7 +129,7 @@ const form = reactive({
});
const searchFrom = () => {
console.log('=== 搜索功能被触发 ===');
form.pageNum = 1;
getDataList();
};
@@ -161,26 +161,21 @@ const getDataList = () => {
if (params.sellerName === '') delete params.sellerName;
}
console.log('订单列表查询参数:', params);
// 调用订单列表接口,而不是装车订单接口
orderPageQuery(params)
.then((res) => {
console.log('订单列表返回结果:', res);
data.dataListLoading = false;
// 直接赋值订单数据
console.log('=== 订单数据 ===');
console.log('完整响应:', res);
console.log('res.data:', res.data);
console.log('数据行数:', res.data?.rows?.length || 0);
rows.value = res.data?.rows || [];
data.total = res.data?.total || 0;
console.log('更新后rows长度:', rows.value.length);
console.log('更新后total:', data.total);
if (rows.value.length > 0) {
console.log('第一行订单数据:', rows.value[0]);
}
})
.catch(() => {
@@ -230,7 +225,7 @@ const del = (id) => {
};
onMounted(() => {
console.log('=== 装车订单页面已加载 ===');
getDataList();
});
</script>
@@ -278,8 +273,6 @@ onMounted(() => {
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.operation-scroll-bar {

View File

@@ -241,7 +241,7 @@ const onClickSave = () => {
data.saveLoading = false;
});
} else {
console.log('error submit!');
}
});
}

View File

@@ -166,7 +166,7 @@ const form = reactive({
});
const searchFrom = () => {
console.log('=== 运送清单搜索功能被触发 ===');
form.pageNum = 1;
getDataList();
};
@@ -188,10 +188,10 @@ const getDataList = () => {
delete params.createTimeRange;
}
console.log('运送清单列表查询参数:', params);
shippingList(params)
.then((res) => {
console.log('运送清单列表返回结果:', res);
data.dataListLoading = false;
if (res.data.rows && res.data.rows.length > 0) {
@@ -282,8 +282,7 @@ const handleDownload = async (row) => {
totalAmount: totalAmount
};
console.log('生成牛只验收单数据:', data);
// 生成HTML内容
const htmlContent = `
<!DOCTYPE html>

View File

@@ -98,8 +98,7 @@ const onClickSave = () => {
FormDataRef.value.validate((valid) => {
if (valid) {
submitting.value = true;
// console.log('用户ID');
// return false;
// // return false;
// if (pageNum.value == 2) {
// 修改密码时
// updatePassword({

View File

@@ -89,7 +89,7 @@ const form = reactive({
pageSize: 10,
});
const handleClick = (tab, event) => {
console.log('=== 标签页切换 ===', tab.props.name);
data.activeName = tab.props.name;
data.allotType = data.activeName === 'first' ? '0' : '1';
form.pageNum = 1;
@@ -100,52 +100,44 @@ const handleClick = (tab, event) => {
};
// 列表
const getDataList = () => {
console.log('=== getDataList 开始执行 ===');
data.dataListLoading = true;
let params;
if (data.mode === 'tenant') {
// 租户分配模式
if (data.allotType === '0') {
// 未分配标签页:查询未分配给任何租户的设备
params = {
...form,
deviceType: data.deviceType,
allotType: data.allotType,
// 不传tenantId让后端查询tenant_id为空的设备
};
console.log('=== 租户分配模式-未分配标签页参数 ===', params);
} else {
// 已分配标签页:查询已分配给该租户的设备
params = {
...form,
deviceType: data.deviceType,
allotType: data.allotType,
tenantId: data.tenantId,
};
console.log('=== 租户分配模式-已分配标签页参数 ===', params);
}
} else {
// 装车订单分配模式:查询未分配给装车订单的设备
// 租户分配模式:传递 mode='tenant' 参数,让后端根据 tenant_id 判断
params = {
...form,
deviceType: data.deviceType,
allotType: data.allotType,
mode: 'tenant', // ✅ 关键:明确告诉后端这是租户模式
};
// 如果是"已分配"标签页,才传递 tenantId用于过滤该租户的设备
if (data.allotType === '1') {
params.tenantId = data.tenantId;
}
} else {
// 装车订单分配模式:传递 mode='delivery' 参数(或不传,默认为 delivery
params = {
...form,
deviceType: data.deviceType,
allotType: data.allotType,
mode: 'delivery', // ✅ 明确告诉后端这是装车订单模式
tenantId: data.tenantId,
};
console.log('=== 装车订单分配模式参数 ===', params);
}
console.log('=== 请求参数 ===', params);
// 使用新的IoT设备API
console.log('=== 调用IoT设备API ===');
const apiCall = iotDeviceAssignableList(params);
apiCall
.then((res) => {
console.log('=== API 调用成功 ===', res);
console.log('=== 原始返回数据 res.data ===', JSON.parse(JSON.stringify(res.data)));
data.dataListLoading = false;
if (res.code == 200) {
let rawData = [];
@@ -156,17 +148,13 @@ const getDataList = () => {
// device.js 中的API返回 { list, total }
rawData = res.data.list || [];
total = res.data.total || 0;
console.log('=== 使用 list 格式数据 ===', { rawData, total });
} else {
// sys.js 中的API返回 { rows, total }
rawData = res.data?.rows || [];
total = res.data?.total || 0;
console.log('=== 使用 rows 格式数据 ===', { rawData, total });
}
console.log('=== rawData 原始数据数量 ===', rawData.length);
console.log('=== rawData 详细内容 ===', JSON.parse(JSON.stringify(rawData)));
// 处理数据:添加设备类型和分配状态
data.rows = rawData.map(item => {
const processedItem = { ...item };
@@ -187,39 +175,36 @@ const getDataList = () => {
break;
}
// 根据模式判断分配状态
// 根据模式判断分配状态
// ⚠️ 关键:租户模式和装车订单模式是完全独立的!
if (data.mode === 'tenant') {
// 租户模式根据tenantId判断分配状态
// 租户模式:根据 tenantId 判断分配状态(忽略 deliveryId
processedItem.isAssigned = !!(item.tenantId && item.tenantId !== null);
} else {
// 装车订单模式根据deliveryNumber判断分配状态
const deliveryNumber = item.deliveryNumber || item.delivery_number;
processedItem.isAssigned = !!(deliveryNumber && deliveryNumber.trim() !== '');
// 装车订单模式:根据 deliveryId 判断分配状态(忽略 tenantId
// 注意:这里应该用 deliveryId而不是 deliveryNumber
processedItem.isAssigned = !!(item.deliveryId && item.deliveryId !== null);
}
console.log(`=== 处理设备 ${item.deviceId || item.sn} ===`, {
deviceType: data.deviceType,
deviceTypeName: processedItem.deviceTypeName,
tenantId: item.tenantId,
deliveryNumber: item.deliveryNumber || item.delivery_number,
isAssigned: processedItem.isAssigned,
mode: data.mode
});
return processedItem;
});
// 根据当前标签页过滤数据
if (data.activeName === 'first') {
// 未分配标签页:显示未分配的设备
const beforeFilter = data.rows.length;
data.rows = data.rows.filter(item => !item.isAssigned);
} else if (data.activeName === 'second') {
// 已分配标签页:显示已分配的设备
const beforeFilter = data.rows.length;
data.rows = data.rows.filter(item => item.isAssigned);
}
data.total = data.rows.length;
console.log('=== 处理后的数据 ===', { rows: data.rows, total: data.total });
} else {
console.error('=== API 返回错误 ===', res);
ElMessage.error(res.msg || '获取数据失败');
@@ -410,7 +395,7 @@ const getRowKey = (row) => {
return row.id;
};
const onShowDialog = (tenantId, deviceType, deliveryId, deliveryNumber, carNumber, mode = 'delivery') => {
console.log('=== onShowDialog 被调用 ===', { tenantId, deviceType, deliveryId, deliveryNumber, carNumber, mode });
data.dialogVisible = true;
data.activeName = 'first';
data.deviceType = deviceType;
@@ -428,15 +413,7 @@ const onShowDialog = (tenantId, deviceType, deliveryId, deliveryNumber, carNumbe
data.title = '设备分配';
}
console.log('=== 设置后的数据 ===', {
deviceType: data.deviceType,
allotType: data.allotType,
tenantId: data.tenantId,
deliveryId: data.deliveryId,
deliveryNumber: data.deliveryNumber,
carNumber: data.carNumber,
mode: data.mode
});
getDataList();
if (multipleTableUnRef.value) {
multipleTableUnRef.value.clearSelection();

View File

@@ -66,7 +66,7 @@ const searchFrom = () => {
};
const searchChange = (val) => {
console.log('Search change:', val);
};
const getDataList = () => {

View File

@@ -72,7 +72,7 @@ const searchFrom = () => {
getDataList();
};
const searchChange = (val) => {
console.log(val);
};
const getDataList = () => {
dataListLoading.value = true;

View File

@@ -77,7 +77,7 @@ const onClickSave = () => {
})
.catch((err) => {});
} else {
console.log('error submit!');
}
});
}

View File

@@ -119,7 +119,7 @@ const delClick = (row) => {
// 编辑用户
const showAddDialog = (row) => {
// TODO: 实现编辑对话框
console.log('编辑用户:', row);
};
onMounted(() => {

View File

@@ -103,8 +103,7 @@ const rules = reactive({
});
const handleAvatarSuccess = (res, file, fileList, type) => {
console.log('上传成功响应:', res);
if (ruleForm.hasOwnProperty(type)) {
let imageUrl = null;
@@ -122,7 +121,7 @@ const handleAvatarSuccess = (res, file, fileList, type) => {
// 直接更新 fileList
file.url = imageUrl;
ruleForm[type] = fileList;
console.log(`${type} 上传成功:`, imageUrl, 'fileList:', fileList);
} else {
console.error('无法解析图片URL:', res);
ElMessage.error('上传失败无法获取图片URL');
@@ -182,8 +181,7 @@ const onClickSave = () => {
idCard: ruleForm.id_card.length > 0 ? ruleForm.id_card.map((item) => item.url).join(',') : '',
};
console.log('提交数据:', params);
const apiCall = data.isEdit ? driverEdit(params) : driverAdd(params);
apiCall.then((res) => {

View File

@@ -172,7 +172,7 @@ const onClickSave = () => {
});
}
} else {
console.log('error submit!');
}
});
}

View File

@@ -174,18 +174,15 @@ const getDataList = async () => {
...form,
...baseSearchRef.value.penetrateParams(),
};
console.log('[VEHICLE-SEARCH] 查询参数:', params);
const res = await vehicleList(params);
console.log('查询结果:', res);
if (res.code === 200) {
// 数据嵌套在 res.data.data 中
const dataInfo = res.data?.data || res.data;
data.rows = dataInfo?.rows || [];
data.total = dataInfo?.total || 0;
console.log('提取的数据:', dataInfo);
console.log('列表数据:', data.rows);
} else {
ElMessage.error(res.msg || '查询失败');
}

View File

@@ -118,8 +118,7 @@ const rules = reactive({
});
const handleAvatarSuccess = (res, file, fileList, type) => {
console.log('上传成功响应:', res);
if (ruleForm.hasOwnProperty(type)) {
let imageUrl = null;
@@ -135,7 +134,7 @@ const handleAvatarSuccess = (res, file, fileList, type) => {
if (imageUrl) {
ruleForm[type] = [{ url: imageUrl, uid: file.uid, name: file.name }];
console.log(`${type} 上传成功:`, imageUrl);
} else {
console.error('无法解析图片URL:', res);
ElMessage.error('上传失败无法获取图片URL');
@@ -187,8 +186,7 @@ const onClickSave = async () => {
remark: ruleForm.remark,
};
console.log('提交数据:', formData);
const res = data.isEdit ? await vehicleEdit(formData) : await vehicleAdd(formData);
if (res.code === 200) {

View File

@@ -1,104 +0,0 @@
// =============================================
// 测试脚本:验证身份证图片数据流
// 用途:测试前端到后端的数据传递
// =============================================
// 模拟前端数据
const mockFrontendData = {
username: '测试司机',
mobile: '13800138000',
carNumber: '京A12345',
driverLicense: 'https://example.com/driver1.jpg,https://example.com/driver2.jpg',
drivingLicense: 'https://example.com/license1.jpg',
carImg: 'https://example.com/car1.jpg,https://example.com/car2.jpg',
recordCode: 'https://example.com/code1.jpg',
idCard: 'https://example.com/id_front.jpg,https://example.com/id_back.jpg', // 身份证前后照片
remark: '测试备注'
};
console.log('=== 前端发送的数据 ===');
console.log(JSON.stringify(mockFrontendData, null, 2));
// 模拟后端接收的数据
const mockBackendReceived = {
username: mockFrontendData.username,
mobile: mockFrontendData.mobile,
carNumber: mockFrontendData.carNumber,
driverLicense: mockFrontendData.driverLicense,
drivingLicense: mockFrontendData.drivingLicense,
carImg: mockFrontendData.carImg,
recordCode: mockFrontendData.recordCode,
idCard: mockFrontendData.idCard, // 这个字段应该存储到数据库的 id_card 字段
remark: mockFrontendData.remark
};
console.log('\n=== 后端接收的数据 ===');
console.log(JSON.stringify(mockBackendReceived, null, 2));
// 模拟数据库存储
const mockDatabaseRecord = {
id: 1,
member_id: 1,
username: mockBackendReceived.username,
car_number: mockBackendReceived.carNumber,
driver_license: mockBackendReceived.driverLicense,
driving_license: mockBackendReceived.drivingLicense,
car_img: mockBackendReceived.carImg,
record_code: mockBackendReceived.recordCode,
id_card: mockBackendReceived.idCard, // 存储到 id_card 字段
remark: mockBackendReceived.remark,
create_time: new Date().toISOString()
};
console.log('\n=== 数据库存储记录 ===');
console.log(JSON.stringify(mockDatabaseRecord, null, 2));
// 验证身份证字段
console.log('\n=== 身份证字段验证 ===');
console.log('id_card 字段值:', mockDatabaseRecord.id_card);
console.log('身份证照片数量:', mockDatabaseRecord.id_card.split(',').length);
console.log('身份证照片列表:', mockDatabaseRecord.id_card.split(','));
// 模拟前端读取数据
const mockFrontendRead = {
id: mockDatabaseRecord.id,
username: mockDatabaseRecord.username,
carNumber: mockDatabaseRecord.car_number,
driver_license: mockDatabaseRecord.driver_license,
driving_license: mockDatabaseRecord.driving_license,
car_img: mockDatabaseRecord.car_img,
record_code: mockDatabaseRecord.record_code,
id_card: mockDatabaseRecord.id_card, // 从数据库读取
remark: mockDatabaseRecord.remark
};
console.log('\n=== 前端读取的数据 ===');
console.log(JSON.stringify(mockFrontendRead, null, 2));
// 模拟前端图片处理
const processImageUrls = (imageUrlString) => {
if (!imageUrlString || imageUrlString.trim() === '') {
return [];
}
return imageUrlString.split(',').map(url => url.trim()).filter(url => url !== '');
};
const idCardImages = processImageUrls(mockFrontendRead.id_card);
console.log('\n=== 前端图片处理结果 ===');
console.log('身份证图片数组:', idCardImages);
console.log('身份证图片数量:', idCardImages.length);
// 验证数据完整性
console.log('\n=== 数据完整性验证 ===');
const isValid = mockDatabaseRecord.id_card &&
mockDatabaseRecord.id_card.includes(',') &&
mockDatabaseRecord.id_card.split(',').length === 2;
console.log('数据完整性检查:', isValid ? '✅ 通过' : '❌ 失败');
if (isValid) {
console.log('✅ 身份证前后照片地址已正确存储到 id_card 字段');
console.log('✅ 使用英文逗号分隔多个URL');
console.log('✅ 前端可以正确读取和显示');
} else {
console.log('❌ 数据存储或处理存在问题');
}

View File

@@ -1,28 +0,0 @@
// 测试用户专属权限是否生效
console.log('=== 测试用户专属权限 ===');
// 模拟用户登录
const testUser = {
id: 3,
name: '12.27新增姓名',
mobile: '15500000000',
roleId: 1
};
console.log('测试用户:', testUser);
// 检查权限查询逻辑
console.log('=== 权限查询逻辑测试 ===');
console.log('1. 用户ID:', testUser.id);
console.log('2. 角色ID:', testUser.roleId);
console.log('3. 是否超级管理员角色:', testUser.roleId === 1);
console.log('=== 预期行为 ===');
console.log('1. 应该先查询用户专属权限');
console.log('2. 如果有专属权限,使用专属权限');
console.log('3. 如果没有专属权限,使用角色权限');
console.log('4. 超级管理员权限作为fallback');
console.log('=== 检查前端权限数据 ===');
// 这里需要用户手动检查浏览器控制台的权限数据
console.log('请检查浏览器控制台中的权限检查调试信息');