修改内容

This commit is contained in:
xuqiuyun
2025-10-27 17:38:20 +08:00
parent a40ce28318
commit 42e0abcbe3
50 changed files with 4240 additions and 1276 deletions

View File

@@ -0,0 +1,186 @@
# 司机管理删除功能实现报告
## 概述
实现司机管理页面中的删除按钮功能,可以删除数据库中的司机数据。
## 实现内容
### 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

@@ -0,0 +1,114 @@
# 超级管理员权限说明
## 问题原因
超级管理员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

@@ -0,0 +1,144 @@
# 用户管理删除功能实现报告
## 概述
实现用户管理页面中的删除按钮功能,可以删除数据库中的用户数据。
## 实现内容
### 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

@@ -49,6 +49,15 @@ export function updateDeviceDeliveryId(data) {
});
}
// 解绑单个设备将delivery_id设置为null
export function unbindDevice(deviceId) {
return request({
url: '/deliveryDevice/updateDeviceDeliveryId',
method: 'POST',
data: { deviceId, deliveryId: null },
});
}
// 批量更新设备weight
export function updateDeviceWeights(data) {
return request({
@@ -210,4 +219,48 @@ export function shippingList(data) {
method: 'POST',
data,
});
}
// ==================== 订单管理接口 ====================
// 订单列表查询
export function orderPageQuery(data) {
return request({
url: '/order/list',
method: 'POST',
data,
});
}
// 新增订单
export function orderAddNew(data) {
return request({
url: '/order/add',
method: 'POST',
data,
});
}
// 更新订单
export function orderUpdate(data) {
return request({
url: '/order/edit',
method: 'POST',
data,
});
}
// 删除订单
export function orderDelete(id) {
return request({
url: `/order/delete?id=${id}`,
method: 'GET',
});
}
// 查询订单详情
export function orderGetDetail(id) {
return request({
url: `/order/detail?id=${id}`,
method: 'GET',
});
}

View File

@@ -67,6 +67,15 @@ export function driverDetail(id) {
});
}
// 司机 - 删除
export function driverDel(id) {
return request({
url: '/member/deleteDriver',
method: 'POST',
data: { id },
});
}
// --------- 会员管理 -----------
// 根据类型获取会员列表(用于装车订单下拉框)
export function memberListByType(data) {
@@ -75,4 +84,40 @@ export function memberListByType(data) {
method: 'POST',
data,
});
}
// --------- 车辆管理 -----------
// 车辆 - 列表
export function vehicleList(data) {
return request({
url: '/vehicle/list',
method: 'POST',
data,
});
}
// 车辆 - 新增
export function vehicleAdd(data) {
return request({
url: '/vehicle/add',
method: 'POST',
data,
});
}
// 车辆 - 编辑
export function vehicleEdit(data) {
return request({
url: '/vehicle/edit',
method: 'POST',
data,
});
}
// 车辆 - 删除
export function vehicleDel(id) {
return request({
url: `/vehicle/delete?id=${id}`,
method: 'GET',
});
}

View File

@@ -183,6 +183,16 @@ export const constantRoutes: Array<RouteRecordRaw> = [
},
component: () => import('~/views/userManage/driver.vue'),
},
{
path: '/userManage/vehicle',
name: 'VehicleManage',
meta: {
title: '车辆管理',
keepAlive: true,
requireAuth: true,
},
component: () => import('~/views/userManage/vehicle.vue'),
},
],
},
// 早期预警路由

View File

@@ -52,6 +52,12 @@
<template #default="scope">
<el-tag type="warning" v-if="scope.row.warningType == 3">运输距离预警</el-tag>
<el-tag type="danger" v-if="scope.row.warningType == 2">数量盘单预警</el-tag>
<el-tag type="info" v-if="scope.row.warningType == 4">设备停留预警</el-tag>
<el-tag type="danger" v-if="scope.row.warningType == 5">高温预警</el-tag>
<el-tag type="info" v-if="scope.row.warningType == 6">低温预警</el-tag>
<el-tag type="warning" v-if="scope.row.warningType == 7">位置偏离预警</el-tag>
<el-tag type="danger" v-if="scope.row.warningType == 8">延误预警</el-tag>
<el-tag type="success" v-if="scope.row.warningType == 9">超前到达预警</el-tag>
</template>
</el-table-column>
<el-table-column prop="warningTime" label="预警时间" />
@@ -121,6 +127,12 @@ const formItemList = reactive([
selectOptions: [
{ value: 2, text: '数量盘单预警' },
{ value: 3, text: '运输距离预警' },
{ value: 4, text: '设备停留预警' },
{ value: 5, text: '高温预警' },
{ value: 6, text: '低温预警' },
{ value: 7, text: '位置偏离预警' },
{ value: 8, text: '延误预警' },
{ value: 9, text: '超前到达预警' },
],
param: 'warningType',
span: 7,

View File

@@ -31,7 +31,7 @@
<el-descriptions-item label="创建时间:">{{ data.baseInfo.createTime || '' }}</el-descriptions-item>
<el-descriptions-item label="登记设备数量:">{{ totalRegisteredDevices }} </el-descriptions-item>
<el-descriptions-item label="状态:">
<el-tag :type="data.baseInfo.status === 2 ? 'success' : 'warning'">{{ getStatusText(data.baseInfo.status) }}</el-tag>
<el-tag :type="getStatusType(data.baseInfo.status)">{{ getStatusText(data.baseInfo.status) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="车身照片:">
<span style="vertical-align: top">
@@ -1108,14 +1108,30 @@ const totalRegisteredDevices = computed(() => {
const getStatusText = (status) => {
const statusMap = {
1: '待装车',
2: '装车',
3: '运输中',
4: '已送达',
5: '已完成'
2: '装车/预付款已支付',
3: '已装车/尾款待支付',
4: '已核验/待买家付款',
5: '尾款已付款',
6: '发票待开/进项票',
7: '发票待开/销项'
};
return statusMap[status] || '未知状态';
};
// 根据状态返回标签类型(颜色)
const getStatusType = (status) => {
const typeMap = {
1: 'info', // 待装车 - 灰色
2: 'success', // 已装车/预付款已支付 - 绿色
3: 'warning', // 已装车/尾款待支付 - 橙色
4: 'warning', // 已核验/待买家付款 - 橙色
5: 'success', // 尾款已付款 - 绿色
6: 'primary', // 发票待开/进项票 - 蓝色
7: 'primary' // 发票待开/销项 - 蓝色
};
return typeMap[status] || 'info';
};
onMounted(() => {
data.id = route.query.id;
data.status = route.query.status;

View File

@@ -70,8 +70,10 @@
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { BmlLushu } from 'vue-baidu-map-3x';
import { collarTrack, collarTrackOrder } from '~/api/hardware.js';
import { getCollarTrajectory as getCollarTrajectoryFromAbroad } from '~/api/abroad.js';
import startIcon from '../../assets/images/qi.png';
import endIcon from '../../assets/images/zhong.png';
import biaoIcon from '../../assets/images/biaozhu.png';
@@ -172,6 +174,60 @@ const reset = () => {
// 查询定位
const getTrack = () => {
data.trackLoading = true;
// 使用新的API从xq_client_log表查询轨迹数据
if (data.deliveryId && data.xqDeviceId) {
getCollarTrajectoryFromAbroad({
deviceId: data.xqDeviceId,
deliveryId: parseInt(data.deliveryId), // 明确转换为整数
startTime: formData.time[0] ? formData.time[0] : '',
endTime: formData.time[1] ? formData.time[1] : '',
})
.then((res) => {
data.trackLoading = false;
console.log('=== 查询轨迹API返回结果:', res);
if (res.code === 200 && res.data && res.data.length > 0) {
data.mapShow = true;
data.path = [];
res.data.forEach((item, index) => {
const lng = parseFloat(item.longitude || item.lng || 0);
const lat = parseFloat(item.latitude || item.lat || 0);
if (lng !== 0 && lat !== 0 && !isNaN(lng) && !isNaN(lat)) {
data.path.push({
lng: lng,
lat: lat,
});
}
});
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;
}
})
.catch((error) => {
console.error('查询轨迹失败:', error);
data.trackLoading = false;
data.noTrack = true;
ElMessage.error('查询轨迹失败');
});
return;
}
// 兼容旧的查询方式
if (data.type == 'order') {
collarTrack({
deliveryId: data.deliveryId,
@@ -240,13 +296,54 @@ const onShowTrackDialog = (row) => {
data.dialogVisible = true;
getNowDate();
if (row) {
data.deliveryId = row.deliveryId;
data.xqDeviceId = row.deviceId;
// 确保deliveryId是数字类型
data.deliveryId = typeof row.deliveryId === 'string' ? parseInt(row.deliveryId) : row.deliveryId;
data.xqDeviceId = row.deviceId || row.sn;
data.type = row.type ? row.type : '';
data.mapShow = false;
data.noTrack = false;
data.formData = '';
getTrack();
// 如果传入了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({
lng: lng,
lat: lat,
});
} else {
console.warn(`轨迹点${index}经纬度无效,跳过`);
}
});
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;
}
} else {
// 没有传入轨迹点,使用原来的查询逻辑
data.formData = '';
getTrack();
}
}
};
defineExpose({

View File

@@ -255,6 +255,11 @@ const generateRoutes = async () => {
await permissionStore.generateRoutes();
}
// 确保路径以斜杠开头
if (!targetPath.startsWith('/')) {
targetPath = '/' + targetPath;
}
// 使用replace而不是push避免路由警告
try {
await router.replace({ path: targetPath });

View File

@@ -55,7 +55,6 @@
<el-button
type="primary"
size="small"
v-hasPermi="['permission:operation:assign']"
@click="handleSaveUserPermissions"
:disabled="!currentUser"
:loading="saveLoading"
@@ -66,7 +65,6 @@
<el-button
type="danger"
size="small"
v-hasPermi="['permission:operation:assign']"
@click="handleClearUserPermissions"
:disabled="!currentUser"
:loading="clearLoading"

View File

@@ -15,12 +15,26 @@
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="发货方" prop="shipper">
<el-input v-model="formData.shipper" placeholder="请输入发货方" />
<el-select v-model="formData.shipper" placeholder="请选择发货方" clearable filterable style="width: 100%">
<el-option
v-for="item in supplierList"
:key="item.id"
:label="item.username || item.mobile"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="采购方" prop="buyer">
<el-input v-model="formData.buyer" placeholder="请输入采购方" />
<el-select v-model="formData.buyer" placeholder="请选择采购方" clearable filterable style="width: 100%">
<el-option
v-for="item in buyerList"
:key="item.id"
:label="item.username || item.mobile"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
@@ -28,12 +42,26 @@
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="车牌号" prop="plateNumber">
<el-input v-model="formData.plateNumber" placeholder="京A12345" />
<el-select v-model="formData.plateNumber" placeholder="请选择车牌号" clearable filterable style="width: 100%">
<el-option
v-for="item in vehicleOptions"
:key="item.id"
:label="item.licensePlate"
:value="item.licensePlate"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="司机姓名" prop="driverName">
<el-input v-model="formData.driverName" placeholder="请输入司机姓名" />
<el-select v-model="formData.driverName" placeholder="请选择司机" clearable filterable style="width: 100%" @change="handleDriverChange">
<el-option
v-for="item in driverOptions"
:key="item.id"
:label="item.username"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
@@ -55,9 +83,9 @@
>
<el-option
v-for="item in serverList"
:key="item.id"
:label="item.deviceNo || item.deviceId"
:value="item.id"
:key="item.deviceId"
:label="item.deviceId + (item.name ? ' - ' + item.name : '')"
:value="item.deviceId"
/>
</el-select>
</el-form-item>
@@ -77,9 +105,9 @@
>
<el-option
v-for="item in eartagList"
:key="item.id"
:label="item.deviceNo || item.deviceId"
:value="item.id"
:key="item.deviceId"
:label="item.deviceId + (item.name ? ' - ' + item.name : '')"
:value="item.deviceId"
/>
</el-select>
</el-form-item>
@@ -96,9 +124,9 @@
>
<el-option
v-for="item in collarList"
:key="item.id"
:label="item.deviceNo || item.deviceId"
:value="item.id"
:key="item.deviceId"
:label="item.deviceId + (item.name ? ' - ' + item.name : '')"
:value="item.deviceId"
/>
</el-select>
</el-form-item>
@@ -131,14 +159,36 @@
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-col :span="24">
<el-form-item label="起点地址" prop="startLocation">
<el-input v-model="formData.startLocation" placeholder="请输入起点地址" />
<el-input
v-model="formData.startLocation"
placeholder="请输入或在地图上选择起点地址"
@click="showStartLocationMap = true"
readonly
style="cursor: pointer;"
>
<template #append>
<el-button @click="showStartLocationMap = true">在地图上选择</el-button>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="目的地地址" prop="endLocation">
<el-input v-model="formData.endLocation" placeholder="请输入目的地地址" />
<el-input
v-model="formData.endLocation"
placeholder="请输入或在地图上选择目的地地址"
@click="showEndLocationMap = true"
readonly
style="cursor: pointer;"
>
<template #append>
<el-button @click="showEndLocationMap = true">在地图上选择</el-button>
</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
@@ -197,12 +247,57 @@
</span>
</template>
</el-dialog>
<!-- 起点地址选择地图 -->
<el-dialog v-model="showStartLocationMap" title="选择起点地址" width="900px">
<baidu-map
class="map"
:center="{lng: 116.404, lat: 39.915}"
:zoom="15"
:scroll-wheel-zoom="true"
@click="handleStartLocationClick"
style="height: 500px"
>
<bm-marker v-if="formData.startLon && formData.startLat" :position="{lng: parseFloat(formData.startLon), lat: parseFloat(formData.startLat)}" :dragging="true" @dragging="handleStartMarkerDrag" />
<bm-map-type :map-types="['BMAP_NORMAL_MAP', 'BMAP_HYBRID_MAP']"></bm-map-type>
</baidu-map>
<template #footer>
<span class="dialog-footer">
<el-button @click="showStartLocationMap = false">取消</el-button>
<el-button type="primary" @click="showStartLocationMap = false">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 目的地地址选择地图 -->
<el-dialog v-model="showEndLocationMap" title="选择目的地地址" width="900px">
<baidu-map
class="map"
:center="{lng: 116.404, lat: 39.915}"
:zoom="15"
:scroll-wheel-zoom="true"
@click="handleEndLocationClick"
style="height: 500px"
>
<bm-marker v-if="formData.endLon && formData.endLat" :position="{lng: parseFloat(formData.endLon), lat: parseFloat(formData.endLat)}" :dragging="true" @dragging="handleEndMarkerDrag" />
<bm-map-type :map-types="['BMAP_NORMAL_MAP', 'BMAP_HYBRID_MAP']"></bm-map-type>
</baidu-map>
<template #footer>
<span class="dialog-footer">
<el-button @click="showEndLocationMap = false">取消</el-button>
<el-button type="primary" @click="showEndLocationMap = false">确定</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive } from 'vue';
import { ElMessage } from 'element-plus';
import { createDelivery, getAvailableServers, getAvailableEartags, getAvailableCollars } from '@/api/shipping.js';
import { createDelivery, updateDeviceDeliveryId } from '@/api/shipping.js';
import { memberListByType, driverList as fetchDriverList, vehicleList as fetchVehicleList } from '@/api/userManage.js';
import { iotDeviceQueryList } from '@/api/hardware.js';
import { BaiduMap, BmMapType, BmMarker } from 'vue-baidu-map-3x';
const dialogVisible = ref(false);
const formRef = ref(null);
@@ -210,12 +305,18 @@ const submitLoading = ref(false);
const serverList = ref([]);
const eartagList = ref([]);
const collarList = ref([]);
const supplierList = ref([]);
const buyerList = ref([]);
const driverOptions = ref([]);
const vehicleOptions = ref([]);
const showStartLocationMap = ref(false);
const showEndLocationMap = ref(false);
const formData = reactive({
shipper: '',
buyer: '',
plateNumber: '',
driverName: '',
shipper: null,
buyer: null,
plateNumber: null,
driverName: null,
driverPhone: '',
serverId: null,
eartagIds: [],
@@ -224,6 +325,10 @@ const formData = reactive({
estimatedArrivalTime: '',
startLocation: '',
endLocation: '',
startLat: '',
startLon: '',
endLat: '',
endLon: '',
cattleCount: 1,
estimatedWeight: null,
quarantineCertNo: '',
@@ -266,8 +371,8 @@ const validateArrivalTime = (rule, value, callback) => {
};
const rules = {
shipper: [{ required: true, message: '请输入发货方', trigger: 'blur' }],
buyer: [{ required: true, message: '请输入采购方', trigger: 'blur' }],
shipper: [{ required: true, message: '请选择发货方', trigger: 'change' }],
buyer: [{ required: true, message: '请选择采购方', trigger: 'change' }],
plateNumber: [{ required: true, validator: validatePlateNumber, trigger: 'blur' }],
driverName: [{ required: true, message: '请输入司机姓名', trigger: 'blur' }],
driverPhone: [{ required: true, validator: validatePhone, trigger: 'blur' }],
@@ -279,45 +384,173 @@ const rules = {
estimatedWeight: [{ required: true, message: '请输入预估重量', trigger: 'blur' }],
};
// 完善提交数据
const buildSubmitData = () => {
const data = { ...formData };
// 确保经纬度是字符串格式
if (data.startLat) data.startLat = String(data.startLat);
if (data.startLon) data.startLon = String(data.startLon);
if (data.endLat) data.endLat = String(data.endLat);
if (data.endLon) data.endLon = String(data.endLon);
return data;
};
// 打开弹窗
const open = () => {
dialogVisible.value = true;
loadSupplierAndBuyerList();
loadDeviceOptions();
loadDriverList();
loadVehicleList();
};
// 加载供应商和采购方列表
const loadSupplierAndBuyerList = async () => {
try {
// 加载供应商列表 (type=2)
const supplierRes = await memberListByType({ type: 2, pageNum: 1, pageSize: 9999 });
if (supplierRes.code === 200) {
supplierList.value = supplierRes.data?.rows || supplierRes.data || [];
}
// 加载采购方列表 (type=4)
const buyerRes = await memberListByType({ type: 4, pageNum: 1, pageSize: 9999 });
if (buyerRes.code === 200) {
buyerList.value = buyerRes.data?.rows || buyerRes.data || [];
}
} catch (error) {
console.error('加载供应商/采购方列表失败', error);
}
};
// 加载设备选项
const loadDeviceOptions = async () => {
try {
// 加载主机设备
const serverRes = await getAvailableServers({ pageNum: 1, pageSize: 9999 });
// 统一使用 iotDeviceQueryList 接口,通过 type 参数区分设备类型
// type: 1-主机, 2-耳标, 4-项圈
const [serverRes, eartagRes, collarRes] = await Promise.all([
iotDeviceQueryList({ pageNum: 1, pageSize: 9999, type: 1 }), // 主机
iotDeviceQueryList({ pageNum: 1, pageSize: 9999, type: 2 }), // 耳标
iotDeviceQueryList({ pageNum: 1, pageSize: 9999, type: 4 }) // 项圈
]);
if (serverRes.code === 200) {
serverList.value = serverRes.data?.rows || serverRes.data || [];
// 过滤出主机类型设备 (type === 1)
const allServers = serverRes.data?.rows || serverRes.data || [];
serverList.value = allServers.filter(item => item.type === 1 || item.type === '1');
}
// 加载耳标设备
const eartagRes = await getAvailableEartags({ pageNum: 1, pageSize: 9999 });
if (eartagRes.code === 200) {
eartagList.value = eartagRes.data?.rows || eartagRes.data || [];
// 过滤出耳标类型设备 (type === 2)
const allEartags = eartagRes.data?.rows || eartagRes.data || [];
eartagList.value = allEartags.filter(item => item.type === 2 || item.type === '2');
}
// 加载项圈设备
const collarRes = await getAvailableCollars({ pageNum: 1, pageSize: 9999 });
if (collarRes.code === 200) {
collarList.value = collarRes.data?.rows || collarRes.data || [];
// 过滤出项圈类型设备 (type === 4)
const allCollars = collarRes.data?.rows || collarRes.data || [];
collarList.value = allCollars.filter(item => item.type === 4 || item.type === '4');
}
} catch (error) {
console.error('加载设备列表失败', error);
}
};
// 加载司机列表
const loadDriverList = async () => {
try {
const res = await fetchDriverList({ pageNum: 1, pageSize: 9999 });
if (res.code === 200) {
driverOptions.value = res.data?.rows || res.data || [];
}
} catch (error) {
console.error('加载司机列表失败', error);
}
};
// 加载车辆列表
const loadVehicleList = async () => {
try {
const res = await fetchVehicleList({ pageNum: 1, pageSize: 9999 });
if (res.code === 200) {
vehicleOptions.value = res.data?.data?.rows || res.data?.rows || res.data || [];
}
} catch (error) {
console.error('加载车辆列表失败', error);
}
};
// 司机选择变化时自动填充电话
const handleDriverChange = (driverId) => {
if (driverId) {
const driver = driverOptions.value.find(item => item.id === driverId);
if (driver && driver.mobile) {
formData.driverPhone = driver.mobile;
}
} else {
formData.driverPhone = '';
}
};
// 更新选中设备的delivery_id
const updateSelectedDevicesDeliveryId = async (deliveryId) => {
try {
const devicesToUpdate = [];
// 收集所有选中的设备
if (formData.serverId) {
devicesToUpdate.push(formData.serverId);
}
if (formData.eartagIds && formData.eartagIds.length > 0) {
devicesToUpdate.push(...formData.eartagIds);
}
if (formData.collarIds && formData.collarIds.length > 0) {
devicesToUpdate.push(...formData.collarIds);
}
// 批量更新设备的delivery_id
for (const deviceId of devicesToUpdate) {
await updateDeviceDeliveryId({
deviceId: deviceId,
deliveryId: deliveryId
});
}
console.log(`成功更新 ${devicesToUpdate.length} 个设备的delivery_id`);
} catch (error) {
console.error('更新设备delivery_id失败:', error);
// 不阻止流程,只记录错误
}
};
// 提交表单
const handleSubmit = () => {
formRef.value.validate(async (valid) => {
if (valid) {
// 验证地址经纬度
if (!formData.startLat || !formData.startLon) {
ElMessage.warning('请在地图上选择起点位置');
return;
}
if (!formData.endLat || !formData.endLon) {
ElMessage.warning('请在地图上选择目的地位置');
return;
}
submitLoading.value = true;
try {
const res = await createDelivery(formData);
const submitData = buildSubmitData();
console.log('提交的数据:', submitData);
const res = await createDelivery(submitData);
if (res.code === 200) {
// 获取新创建的运送清单ID
const newDeliveryId = res.data?.id;
if (newDeliveryId) {
// 更新设备的delivery_id
await updateSelectedDevicesDeliveryId(newDeliveryId);
}
ElMessage.success('创建成功');
dialogVisible.value = false;
emit('success');
@@ -325,6 +558,7 @@ const handleSubmit = () => {
ElMessage.error(res.msg || '创建失败');
}
} catch (error) {
console.error('创建失败:', error);
ElMessage.error('创建失败,请稍后重试');
} finally {
submitLoading.value = false;
@@ -333,10 +567,64 @@ const handleSubmit = () => {
});
};
// 地图点击事件 - 起点
const handleStartLocationClick = (e) => {
formData.startLon = e.point.lng;
formData.startLat = e.point.lat;
// 反向地理编码获取地址
const geocoder = new window.BMap.Geocoder();
geocoder.getLocation(e.point, (res) => {
if (res) {
formData.startLocation = res.address;
ElMessage.success('已设置起点地址');
}
});
};
// 起点标记拖拽事件
const handleStartMarkerDrag = (e) => {
formData.startLon = e.point.lng;
formData.startLat = e.point.lat;
const geocoder = new window.BMap.Geocoder();
geocoder.getLocation(e.point, (res) => {
if (res) {
formData.startLocation = res.address;
}
});
};
// 地图点击事件 - 目的地
const handleEndLocationClick = (e) => {
formData.endLon = e.point.lng;
formData.endLat = e.point.lat;
// 反向地理编码获取地址
const geocoder = new window.BMap.Geocoder();
geocoder.getLocation(e.point, (res) => {
if (res) {
formData.endLocation = res.address;
ElMessage.success('已设置目的地地址');
}
});
};
// 目的地标记拖拽事件
const handleEndMarkerDrag = (e) => {
formData.endLon = e.point.lng;
formData.endLat = e.point.lat;
const geocoder = new window.BMap.Geocoder();
geocoder.getLocation(e.point, (res) => {
if (res) {
formData.endLocation = res.address;
}
});
};
// 关闭弹窗
const handleClose = () => {
formRef.value?.resetFields();
dialogVisible.value = false;
showStartLocationMap.value = false;
showEndLocationMap.value = false;
};
// 暴露方法给父组件
@@ -348,6 +636,11 @@ const emit = defineEmits(['success']);
</script>
<style scoped>
.map {
width: 100%;
height: 500px;
}
.dialog-footer {
display: flex;
justify-content: flex-end;

View File

@@ -842,20 +842,24 @@ const updateDeviceWeightsLocal = (customDevices = null) => {
// 否则收集当前表单中的设备重量
// 添加智能耳标的重量
data.deliveryDevices.forEach(device => {
if (device.bindWeight && device.bindWeight.trim() !== '') {
// 安全地检查bindWeight值
const weightStr = String(device.bindWeight || '').trim();
if (weightStr !== '' && !isNaN(parseFloat(weightStr))) {
devices.push({
deviceId: device.deviceId,
weight: parseFloat(device.bindWeight)
weight: parseFloat(weightStr)
});
}
});
// 添加智能项圈的重量
data.xqDevices.forEach(device => {
if (device.bindWeight && device.bindWeight.trim() !== '') {
// 安全地检查bindWeight值
const weightStr = String(device.bindWeight || '').trim();
if (weightStr !== '' && !isNaN(parseFloat(weightStr))) {
devices.push({
deviceId: device.deviceId,
weight: parseFloat(device.bindWeight)
weight: parseFloat(weightStr)
});
}
});

View File

@@ -4,143 +4,50 @@
<!-- 横向滚动操作栏 -->
<div class="operation-scroll-bar">
<div class="operation-scroll-container">
<el-button type="primary" v-hasPermi="['loading:create']" @click="showAddDialog(null)">创建装车订单</el-button>
<!-- <el-button
<el-button type="primary" v-hasPermi="['loading:create']" @click="showAddDialog(null)">创建订单</el-button>
<el-button
type="primary"
v-hasPermi="['loading:add']"
@click="showCreateDeliveryDialog"
style="margin-left: 10px"
>
新增运送清单
</el-button> -->
</el-button>
</div>
</div>
<div class="main-container">
<el-table :data="rows" :key="data.tableKey" border v-loading="data.dataListLoading" element-loading-text="数据加载中..." style="width: 100%">
<el-table-column label="装车订单编号" prop="deliveryNumber">
<el-table-column label="订单编号" prop="deliveryNumber" width="180">
<template #default="scope">
{{ scope.row.deliveryNumber || '--' }}
</template>
</el-table-column>
<el-table-column label="订单标题" prop="deliveryTitle">
<el-table-column label="供应商" prop="supplierName" width="150">
<template #default="scope">
{{ scope.row.deliveryTitle || '--' }}
{{ scope.row.supplierName || '--' }}
</template>
</el-table-column>
<el-table-column label="起始地" prop="startLocation">
<el-table-column label="采购商" prop="buyerName" width="150">
<template #default="scope">
{{ scope.row.startLocation || '--' }}
{{ scope.row.buyerName || '--' }}
</template>
</el-table-column>
<el-table-column label="目的地" prop="endLocation">
<el-table-column label="结算方式" prop="settlementMethod" width="150">
<template #default="scope">
{{ scope.row.endLocation || '--' }}
{{ getSettlementMethod(scope.row) }}
</template>
</el-table-column>
<el-table-column label="采购单价(元/公斤)" prop="buyerPrice">
<template #default="scope">
{{ scope.row.buyerPrice || '0' }}
</template>
</el-table-column>
<el-table-column label="销售单价(元/公斤)" prop="salePrice">
<template #default="scope">
{{ scope.row.salePrice || '0' }}
</template>
</el-table-column>
<el-table-column label="约定单价(元/公斤)" prop="firmPrice">
<template #default="scope">
{{ scope.row.firmPrice || '0' }}
</template>
</el-table-column>
<el-table-column label="装车数量" prop="ratedQuantity">
<template #default="scope">
{{ scope.row.ratedQuantity || '0' }}
</template>
</el-table-column>
<el-table-column label="已分配设备数量" prop="bindJbqCount">
<template #default="scope">
{{ getTotalDeviceCount(scope.row.id) }}
</template>
</el-table-column>
<el-table-column label="已佩戴设备数量" prop="wareCount">
<template #default="scope">
<span :style="{ color: getTotalDeviceCount(scope.row.id) == scope.row.wareCount ? '' : 'red' }">
{{ scope.row.wareCount || '0' }}
</span>
</template>
</el-table-column>
<el-table-column label="创建人" prop="createByName">
<el-table-column label="创建人" prop="createByName" width="120">
<template #default="scope">
{{ scope.row.createByName || '--' }}
</template>
</el-table-column>
<el-table-column label="创建时间" prop="createTime">
<el-table-column label="创建时间" prop="createTime" width="180">
<template #default="scope">
{{ scope.row.createTime || '--' }}
</template>
</el-table-column>
<el-table-column label="核验状态" prop="statusDesc" width="100" :key="`status-${data.forceUpdate}`">
<template #default="scope">
<!-- 调试信息 -->
<div style="display: none;">{{ console.log('核验状态调试:', { status: scope.row.status, statusDesc: scope.row.statusDesc, rowId: scope.row.id }) }}</div>
<el-tag :type="getStatusTagType(scope.row.status)" :key="`tag-${scope.row.id}-${data.forceUpdate}`">
{{ scope.row.statusDesc || getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="登记设备数量" prop="registeredJbqCount" width="120" :key="`count-${data.forceUpdate}`">
<template #default="scope">
<!-- 调试信息 -->
<div style="display: none;">{{ console.log('设备数量调试:', { registeredJbqCount: scope.row.registeredJbqCount, totalDeviceCount: getTotalDeviceCount(scope.row.id) }) }}</div>
<span :key="`count-span-${scope.row.id}-${data.forceUpdate}`">{{ getTotalDeviceCount(scope.row.id) }}</span>
</template>
</el-table-column>
<el-table-column label="车内盘点耳标数量" prop="earTagCount" width="140" :key="`ear-tag-${data.forceUpdate}`">
<template #default="scope">
<!-- 调试信息 -->
<div style="display: none;">{{ console.log('耳标数量调试:', { earTagCount: scope.row.earTagCount, actualEarTagCount: getEarTagCount(scope.row.id) }) }}</div>
<span :key="`ear-tag-span-${scope.row.id}-${data.forceUpdate}`">{{ getEarTagCount(scope.row.id) }}</span>
</template>
</el-table-column>
<el-table-column label="车牌号" prop="licensePlate" width="120">
<template #default="scope">
{{ scope.row.licensePlate || '--' }}
</template>
</el-table-column>
<el-table-column label="司机姓名" prop="driverName" width="120">
<template #default="scope">
{{ scope.row.driverName || '--' }}
</template>
</el-table-column>
<el-table-column label="车身照片" width="200">
<template #default="scope">
<div style="display: flex; flex-wrap: wrap; gap: 5px;">
<!-- 使用前端分割逻辑处理车身照片 -->
<template v-if="getProcessedCarPhotos(scope.row).length > 0">
<el-image
v-for="(img, index) in getProcessedCarPhotos(scope.row)"
:key="'car-' + index"
style="width: 60px; height: 60px; border-radius: 4px; border: 1px solid #dcdfe6;"
:src="img"
fit="cover"
:lazy="true"
:preview-src-list="getProcessedCarPhotos(scope.row)"
preview-teleported
@error="handleImageError(img, index)"
>
<template #error>
<div style="width: 60px; height: 60px; display: flex; justify-content: center; align-items: center; background: #f5f7fa;">
<el-icon style="font-size: 24px; color: #c0c4cc;"><Picture /></el-icon>
</div>
</template>
</el-image>
</template>
<!-- 无照片时显示占位 -->
<span v-else style="color: #999; font-size: 12px; display: flex; align-items: center;">暂无照片</span>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="420">
<template #default="scope">
<el-button link type="primary" :disabled="scope.row.status != 1" v-hasPermi="['loading:edit']" @click="showEditDialog(scope.row)">编辑</el-button>
@@ -201,41 +108,18 @@ const formItemList = reactive([
placeholder: '请输入运单号',
},
{
label: '订单标题',
label: '供应商',
type: 'input',
param: 'deliveryTitle',
param: 'supplierName',
span: 6,
placeholder: '请输入订单标题',
placeholder: '请输入供应商',
},
{
label: '目的地',
label: '采购商',
type: 'input',
param: 'endLocation',
param: 'buyerName',
span: 6,
placeholder: '请输入目的地',
},
{
label: '车牌号',
type: 'input',
param: 'licensePlate',
span: 6,
placeholder: '请输入车牌号',
},
{
label: '核验状态',
type: 'select',
param: 'status',
span: 6,
placeholder: '请选择核验状态',
selectOptions: [
{ text: '待装车', value: 1 },
{ text: '已装车/待资金方付款', value: 2 },
{ text: '待核验/资金方已付款', value: 3 },
{ text: '已核验/待买家付款', value: 4 },
{ text: '买家已付款', value: 5 }
],
labelKey: 'text',
valueKey: 'value',
placeholder: '请输入采购商',
},
{
label: '创建时间',
@@ -359,25 +243,25 @@ const getDataList = () => {
console.log('API响应:', res);
console.log('数据行数:', res.data.rows ? res.data.rows.length : 0);
// 前端精确搜索:在API返回的数据中根据车牌号精确搜索
if (searchParams.licensePlate && res.data.rows && res.data.rows.length > 0) {
console.log('=== 前端精确搜索车牌号 ===');
console.log('搜索车牌号:', searchParams.licensePlate);
console.log('API返回的所有车牌号:');
res.data.rows.forEach((row, index) => {
console.log(`${index + 1}行车牌号:`, row.licensePlate);
});
// 精确匹配车牌号
const filteredRows = res.data.rows.filter(row => row.licensePlate === searchParams.licensePlate);
console.log('精确匹配结果:', filteredRows.length, '条');
// 前端精确搜索:根据条件精确搜索
if (searchParams.buyerName && res.data.rows && res.data.rows.length > 0) {
// 精确匹配采购商名称
const filteredRows = res.data.rows.filter(row => row.buyerName === searchParams.buyerName);
if (filteredRows.length > 0) {
res.data.rows = filteredRows;
res.data.total = filteredRows.length;
} else {
res.data.rows = [];
res.data.total = 0;
}
}
if (searchParams.supplierName && res.data.rows && res.data.rows.length > 0) {
// 精确匹配供应商名称
const filteredRows = res.data.rows.filter(row => row.supplierName === searchParams.supplierName);
if (filteredRows.length > 0) {
console.log('找到匹配的车牌号数据:', filteredRows);
res.data.rows = filteredRows;
res.data.total = filteredRows.length;
} else {
console.log('未找到匹配的车牌号,显示空结果');
res.data.rows = [];
res.data.total = 0;
}
@@ -638,6 +522,22 @@ const getProcessedCarPhotos = (row) => {
return carImgUrls;
};
// 获取结算方式
const getSettlementMethod = (row) => {
// 根据现有字段判断结算方式
// 如果有空车磅重和装车磅重,使用上车重量结算
// 如果有落地磅重,使用下车重量结算
// 如果有约定单价,使用按照肉价结算
if (row.emptyWeight && row.entruckWeight) {
return '上车重量';
} else if (row.landingEntruckWeight) {
return '下车重量';
} else if (row.firmPrice) {
return '按照肉价';
}
return '--';
};
// 监听rows变化强制更新表格
watch(rows, (newRows) => {

View File

@@ -31,77 +31,9 @@
</el-tag>
</template>
</el-table-column>
<el-table-column label="牛只图片" prop="gpsState" width="350">
<el-table-column label="操作" width="100">
<template #default="scope">
<div style="display: flex" v-if="scope.row">
<!-- 正面图片 -->
<div v-if="scope.row.frontImg">
<el-image
v-for="(img, index) in getImageList(scope.row.frontImg)"
:key="'front-' + index"
:src="img"
style="width: 80px; height: 80px; margin-right: 10px"
fit="cover"
:preview-src-list="getImageList(scope.row.frontImg)"
preview-teleported
>
<template #error>
<div
style="width: 80px; height: 80px; display: flex; justify-content: center; align-items: center"
class="image-slot"
>
<el-icon style="font-size: 50px"><Picture /></el-icon>
</div>
</template>
</el-image>
</div>
<!-- 侧面图片 -->
<div v-if="scope.row.sideImg">
<el-image
v-for="(img, index) in getImageList(scope.row.sideImg)"
:key="'side-' + index"
:src="img"
style="width: 80px; height: 80px; margin-right: 10px"
fit="cover"
:preview-src-list="getImageList(scope.row.sideImg)"
preview-teleported
>
<template #error>
<div
style="width: 80px; height: 80px; display: flex; justify-content: center; align-items: center"
class="image-slot"
>
<el-icon style="font-size: 50px"><Picture /></el-icon>
</div>
</template>
</el-image>
</div>
<!-- 臀部图片 -->
<div v-if="scope.row.hipImg">
<el-image
v-for="(img, index) in getImageList(scope.row.hipImg)"
:key="'hip-' + index"
:src="img"
style="width: 80px; height: 80px; margin-right: 10px"
fit="cover"
:preview-src-list="getImageList(scope.row.hipImg)"
preview-teleported
>
<template #error>
<div
style="width: 80px; height: 80px; display: flex; justify-content: center; align-items: center"
class="image-slot"
>
<el-icon style="font-size: 50px"><Picture /></el-icon>
</div>
</template>
</el-image>
</div>
<!-- 无图片时显示占位 -->
<div v-if="!scope.row.frontImg && !scope.row.sideImg && !scope.row.hipImg" style="color: #999; display: flex; align-items: center;">
暂无图片
</div>
</div>
<el-button link type="danger" @click="handleUnbind(scope.row)">解绑</el-button>
</template>
</el-table-column>
</el-table>
@@ -116,7 +48,8 @@
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { deviceAllList } from '@/api/shipping.js';
import { ElMessage, ElMessageBox } from 'element-plus';
import { deviceAllList, unbindDevice } from '@/api/shipping.js';
const data = reactive({
dialogVisible: false,
@@ -170,6 +103,34 @@ const onShowLookDialog = (row) => {
getDataList();
}
};
// 解绑设备
const handleUnbind = (row) => {
ElMessageBox.confirm('确认解绑该设备吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
unbindDevice(row.deviceId || row.sn)
.then((res) => {
if (res.code === 200) {
ElMessage.success('解绑成功');
getDataList();
} else {
ElMessage.error(res.msg || '解绑失败');
}
})
.catch((error) => {
ElMessage.error('解绑失败');
console.error('解绑失败:', error);
});
})
.catch(() => {
// 用户取消操作
});
};
defineExpose({
onShowLookDialog,
});

View File

@@ -1,238 +1,83 @@
<template>
<el-dialog v-model="data.dialogVisible" title="创建装车订单" :before-close="handleClose" style="width: 1100px; padding-bottom: 20px">
<el-form ref="formDataRef" :model="ruleForm" :rules="rules" label-width="auto">
<el-row :gutter="40">
<el-col :span="12">
<el-form-item label="订单标题" prop="deliveryTitle">
<el-input v-model="ruleForm.deliveryTitle" placeholder="请输入订单标题" clearable></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="装车数量" prop="ratedQuantity">
<el-input v-model="ruleForm.ratedQuantity" placeholder="请输入装车数量" clearable> <template #append></template></el-input>
</el-form-item></el-col
<el-dialog v-model="data.dialogVisible" title="创建订单" :before-close="handleClose" width="600px">
<el-form ref="formDataRef" :model="ruleForm" :rules="rules" label-width="120px">
<el-form-item label="卖方" prop="sellerId">
<el-select
v-model="ruleForm.sellerId"
clearable
filterable
remote
:remote-method="supplierRemoteMethod"
:loading="data.supplierLoading"
placeholder="请选择卖方"
style="width: 100%"
multiple
collapse-tags
collapse-tags-tooltip
:max-collapse-tags="2"
>
</el-row>
<el-row :gutter="40">
<el-col :span="12">
<el-form-item label="选择供应商" prop="supplierName">
<el-select
v-model="ruleForm.supplierName"
clearable
filterable
remote
:remote-method="supplierRemoteMethod"
:loading="data.supplierLoading"
@change="supplierChange"
placeholder="请选择供应商"
style="width: 100%"
multiple
collapse-tags
collapse-tags-tooltip
:max-collapse-tags="2"
>
<el-option
v-for="item in data.supplierOptions"
:key="item.id"
:label="item.username + ' / ' + item.mobile + ' (' + item.tenantName + ')'"
:value="item.mobile"
>
</el-option>
<el-pagination
style="padding: 0px 20px"
@current-change="supplierHandleCurrentChange"
:page-size="10"
:current-page="data.supplierPageNum"
layout="total, prev, pager, next"
:total="data.supplierTotal"
>
</el-pagination>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="选择资金方" prop="financeName">
<el-select
v-model="ruleForm.financeName"
clearable
filterable
remote
:remote-method="financeRemoteMethod"
:loading="data.financeLoading"
@change="financeChange"
placeholder="请选择资金方"
style="width: 100%"
>
<el-option
v-for="item in data.financeOptions"
:key="item.id"
:label="item.username + ' / ' + item.mobile + ' (' + item.tenantName + ')'"
:value="item.mobile"
>
</el-option>
<el-pagination
style="padding: 0px 20px"
@current-change="financeHandleCurrentChange"
:page-size="10"
:current-page="data.financePageNum"
layout="total, prev, pager, next"
:total="data.financeTotal"
>
</el-pagination>
</el-select> </el-form-item
></el-col>
</el-row>
<el-row :gutter="40">
<el-col :span="12">
<el-form-item label="选择司机" prop="driverMobile">
<el-select
v-model="ruleForm.driverMobile"
clearable
filterable
remote
:remote-method="driverRemoteMethod"
:loading="data.driverLoading"
@change="driverChange"
placeholder="请选择司机"
style="width: 100%"
>
<el-option
v-for="item in data.driverOptions"
:key="item.id"
:label="item.username + ' / ' + item.mobile"
:value="item.mobile"
>
</el-option>
<el-pagination
style="padding: 0px 20px"
@current-change="driverHandleCurrentChange"
:page-size="10"
:current-page="data.driverPageNum"
layout="total, prev, pager, next"
:total="data.driverTotal"
>
</el-pagination>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="选择采购商" prop="purchaserMobile">
<el-select
v-model="ruleForm.purchaserMobile"
clearable
filterable
remote
:remote-method="purchaserRemoteMethod"
:loading="data.purchaserLoading"
@change="purchaserChange"
placeholder="请选择采购商"
style="width: 100%"
>
<el-option
v-for="item in data.purchaserOptions"
:key="item.id"
:label="item.username + ' / ' + item.mobile + ' (' + item.tenantName + ')'"
:value="item.mobile"
>
</el-option>
<el-pagination
style="padding: 0px 20px"
@current-change="purchaserHandleCurrentChange"
:page-size="10"
:current-page="data.purchaserPageNum"
layout="total, prev, pager, next"
:total="data.purchaserTotal"
>
</el-pagination>
</el-select> </el-form-item
></el-col>
</el-row>
<el-row :gutter="40">
<el-col :span="12">
<el-form-item label="采购单价" prop="buyerPrice">
<el-input v-model="ruleForm.buyerPrice" placeholder="请输入采购单价" clearable>
<template #append>/公斤</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="销售单价" prop="salePrice">
<el-input v-model="ruleForm.salePrice" placeholder="请输入销售单价" clearable> <template #append>/公斤</template></el-input>
</el-form-item></el-col
<el-option
v-for="item in data.supplierOptions"
:key="item.id"
:label="item.username + ' / ' + item.mobile + ' (' + item.tenantName + ')'"
:value="item.id"
>
</el-option>
<el-pagination
style="padding: 0px 20px"
@current-change="supplierHandleCurrentChange"
:page-size="10"
:current-page="data.supplierPageNum"
layout="total, prev, pager, next"
:total="data.supplierTotal"
>
</el-pagination>
</el-select>
</el-form-item>
<el-form-item label="买方" prop="buyerId">
<el-select
v-model="ruleForm.buyerId"
clearable
filterable
remote
:remote-method="purchaserRemoteMethod"
:loading="data.purchaserLoading"
placeholder="请选择买方"
style="width: 100%"
multiple
collapse-tags
collapse-tags-tooltip
:max-collapse-tags="2"
>
</el-row>
<el-row :gutter="40">
<el-col :span="12">
<el-form-item label="约定单价" prop="firmPrice">
<el-input v-model="ruleForm.firmPrice" placeholder="请输入约定单价" clearable>
<template #append>/公斤</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="40">
<el-col :span="12">
<el-form-item label="起始地" prop="startLocation">
<el-autocomplete
v-model="ruleForm.startLocation"
:fetch-suggestions="startSearchLocation"
placeholder="请输入起始地"
style="width: 100%"
:trigger-on-focus="false"
@select="startHandleSelects"
/>
<div class="maps" style="width: 100%">
<baidu-map
class="bm-view"
:center="data.startCenter"
:zoom="14"
style="height: 300px; width: 100%; border: 1px solid #ddd"
@ready="handler"
:scroll-wheel-zoom="true"
:extensions_road="true"
:extensions_town="true"
v-if="data.dialogVisible"
@click="startClickInfo"
:map-type="'BMAP_NORMAL_MAP'"
:enable-map-click="true"
>
<bm-marker :position="data.startCenter" :dragging="true"></bm-marker>
</baidu-map>
</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="目的地" prop="endLocation">
<el-autocomplete
v-model="ruleForm.endLocation"
:fetch-suggestions="endSearchLocation"
placeholder="请输入目的地"
style="width: 100%"
:trigger-on-focus="false"
@select="endHandleSelects"
/>
<div class="maps" style="width: 100%">
<baidu-map
class="bm-view"
:center="data.endCenter"
:zoom="14"
style="height: 300px; width: 100%; border: 1px solid #ddd"
@ready="handler"
:scroll-wheel-zoom="true"
:extensions_road="true"
:extensions_town="true"
v-if="data.dialogVisible"
@click="endClickInfo"
:map-type="'BMAP_NORMAL_MAP'"
:enable-map-click="true"
>
<bm-marker :position="data.endCenter" :dragging="true"></bm-marker>
</baidu-map>
</div>
</el-form-item>
</el-col>
</el-row>
<el-option
v-for="item in data.purchaserOptions"
:key="item.id"
:label="item.username + ' / ' + item.mobile + ' (' + item.tenantName + ')'"
:value="item.id"
>
</el-option>
<el-pagination
style="padding: 0px 20px"
@current-change="purchaserHandleCurrentChange"
:page-size="10"
:current-page="data.purchaserPageNum"
layout="total, prev, pager, next"
:total="data.purchaserTotal"
>
</el-pagination>
</el-select>
</el-form-item>
<el-form-item label="结算方式" prop="settlementType">
<el-select v-model="ruleForm.settlementType" placeholder="请选择结算方式" style="width: 100%">
<el-option label="上车重量" :value="1"></el-option>
<el-option label="下车重量" :value="2"></el-option>
<el-option label="按肉价结算" :value="3"></el-option>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button :loading="data.saveLoading" type="primary" @click="onClickSave">保存</el-button>
@@ -243,195 +88,56 @@
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { orderAdd } from '@/api/shipping.js';
import { driverList, userList, memberListByType } from '@/api/userManage.js';
import { ref, reactive } from 'vue';
import { orderAddNew, orderUpdate } from '@/api/shipping.js';
import { memberListByType } from '@/api/userManage.js';
const emits = defineEmits();
const formDataRef = ref(null);
const maps = ref();
const BMap = reactive({});
const data = reactive({
dialogVisible: false,
saveLoading: false,
supplierOptions: [],
financeOptions: [],
driverOptions: [],
purchaserOptions: [],
startCenter: { lng: 0, lat: 0 },
endCenter: { lng: 0, lat: 0 },
driverLoading: false, // 司机loading
driverLoading: false,
driverName: '',
driverPageNum: 1,
driverTotal: 0,
purchaserLoading: false, // 采购商loading
purchaserLoading: false,
purchaserPageNum: 1,
purchaserTotal: 0,
purchaserName: '',
financeLoading: false, // 资金方loading
financePageNum: 1,
financeTotal: 0,
financeName: '',
supplierLoading: false, // 供应商loading
supplierLoading: false,
supplierPageNum: 1,
supplierTotal: 0,
supplierName: '',
});
const ruleForm = reactive({
deliveryTitle: '', // 订单标题
ratedQuantity: '', // 装车数量
supplier: [], // 供应商
fundId: '', // 资金方
driverId: '', // 司机
buyerId: '', // 采购商
buyerPrice: '', // 采购单价
salePrice: '', // 销售单价
firmPrice: '', // 约定单价
startLocation: '', // 起始地
startLat: '',
startLon: '',
endLocation: '', // 目的地
endLat: '',
endLon: '',
driverMobile: '',
purchaserMobile: '',
supplierMobile: '',
id: null, // 订单ID编辑时使用
buyerId: [], // 买方ID数组
sellerId: [], // 卖方ID数组
settlementType: 1, // 结算方式1-上车重量2-下车重量3-按肉价结算
});
const rules = reactive({
deliveryTitle: [{ required: true, message: '请输入订单标题', trigger: 'blur' }],
// ratedQuantity: [{ required: true, message: '请输入装车数量', trigger: 'blur' }],
// supplierName: [{ required: true, message: '请选择供应商', trigger: 'change' }],
// financeName: [{ required: true, message: '请选择资金方', trigger: 'change' }],
// driverMobile: [{ required: true, message: '请选择司机', trigger: 'change' }],
// purchaserMobile: [{ required: true, message: '请选择采购商', trigger: 'change' }],
// buyerPrice: [{ required: true, message: '请输入采购单价', trigger: 'blur' }],
// salePrice: [{ required: true, message: '请输入销售单价', trigger: 'blur' }],
// startLocation: [{ required: true, message: '请输入起始地', trigger: 'blur' }],
// endLocation: [{ required: true, message: '请输入目的地', trigger: 'blur' }],
buyerId: [{ required: true, message: '请选择买方', trigger: 'change' }],
sellerId: [{ required: true, message: '请选择卖方', trigger: 'change' }],
settlementType: [{ required: true, message: '请选择结算方式', trigger: 'change' }],
});
const handleClose = () => {
if (formDataRef.value) {
formDataRef.value.resetFields();
}
// 重置表单数据
ruleForm.id = null;
ruleForm.buyerId = [];
ruleForm.sellerId = [];
ruleForm.settlementType = 1;
data.dialogVisible = false;
};
// ----------------
// 初始化
const handler = ({ BMap, map }) => {
BMap = BMap;
maps.value = map;
if (data.startCenter.lng == 0 && data.startCenter.lat == 0) {
const localcity = new BMap.LocalCity();
localcity.get((e) => {
data.startCenter.lng = e.center.lng;
data.startCenter.lat = e.center.lat;
data.endCenter.lng = e.center.lng;
data.endCenter.lat = e.center.lat;
});
} else {
// 如果有坐标点则,展示坐标点
}
};
const startSearchLocation = async (str, cb) => {
// 使用百度地图的地点搜索服务
const local = new window.BMap.LocalSearch(maps.value, {
onSearchComplete(res) {
const arr = [];
if (local.getStatus() == BMAP_STATUS_SUCCESS) {
for (let i = 0; i < res.getCurrentNumPois(); i++) {
const x = res.getPoi(i);
const item = { value: x.address + x.title, point: x.point };
arr.push(item);
}
cb(arr);
} else {
// ElMessage.error('未找到相关地点,请尝试其他关键字。');
}
},
});
local.search(str);
};
const endSearchLocation = async (str, cb) => {
// 使用百度地图的地点搜索服务
const local = new window.BMap.LocalSearch(maps.value, {
onSearchComplete(res) {
const arr = [];
if (local.getStatus() == BMAP_STATUS_SUCCESS) {
for (let i = 0; i < res.getCurrentNumPois(); i++) {
const x = res.getPoi(i);
const item = { value: x.address + x.title, point: x.point };
arr.push(item);
}
cb(arr);
} else {
// ElMessage.error('未找到相关地点,请尝试其他关键字。');
}
},
});
local.search(str);
};
const startHandleSelects = (item) => {
// 点击搜索的点位并地图跳转到该坐标
const { point } = item;
ruleForm.startLocation = item.value;
getStartClickInfo({ point });
};
const getStartClickInfo = ({ point }) => {
const geoc = new window.BMap.Geocoder(); // 创建地址解析器的实例
data.startCenter.lng = point.lng;
data.startCenter.lat = point.lat;
geoc.getLocation(point, function (result) {
if (result.surroundingPois.length > 0) {
const fcaArr = [result.addressComponents.province, result.addressComponents.city, result.addressComponents.district];
ruleForm.startLon = result.point.lng;
ruleForm.startLat = result.point.lat;
}
});
};
const startClickInfo = (e) => {
data.startCenter.lng = e.point.lng;
data.startCenter.lat = e.point.lat;
const geocoder = new window.BMap.Geocoder();
geocoder.getLocation(e.point, (res) => {
if (res) {
ruleForm.startLocation = res.address;
ruleForm.startLon = res.point.lng;
ruleForm.startLat = res.point.lat;
}
});
};
// 到达点
const endHandleSelects = (item) => {
// 点击搜索的点位并地图跳转到该坐标
const { point } = item;
ruleForm.endLocation = item.value;
getEndClickInfo({ point });
};
const getEndClickInfo = ({ point }) => {
const geoc = new window.BMap.Geocoder(); // 创建地址解析器的实例
data.endCenter.lng = point.lng;
data.endCenter.lat = point.lat;
geoc.getLocation(point, function (result) {
if (result.surroundingPois.length > 0) {
const fcaArr = [result.addressComponents.province, result.addressComponents.city, result.addressComponents.district];
ruleForm.endLon = result.point.lng;
ruleForm.endLat = result.point.lat;
}
});
};
const endClickInfo = (e) => {
data.endCenter.lng = e.point.lng;
data.endCenter.lat = e.point.lat;
const geocoder = new window.BMap.Geocoder();
geocoder.getLocation(e.point, (res) => {
if (res) {
ruleForm.endLocation = res.address;
ruleForm.endLon = res.point.lng;
ruleForm.endLat = res.point.lat;
}
});
};
// ----------------
// 供应商远程搜索
@@ -440,6 +146,7 @@ const supplierRemoteMethod = (e) => {
data.supplierPageNum = 1;
getSupplierList();
};
// 供应商 列表
const getSupplierList = () => {
data.supplierLoading = true;
@@ -459,101 +166,21 @@ const getSupplierList = () => {
data.supplierLoading = false;
});
};
// 选择供应商分页
const supplierHandleCurrentChange = (val) => {
data.supplierPageNum = val;
getSupplierList();
};
// 选择供应商
const supplierChange = (e) => {
if (e) {
// ruleForm.supplier = data.supplierOptions.find((item) => item.mobile == e).id;
ruleForm.supplier = data.supplierOptions.filter((user) => e.includes(user.mobile)).map((user) => user.id);
} else {
ruleForm.supplier = [];
}
};
// 供应商远程搜索
const financeRemoteMethod = (e) => {
data.financeName = e;
data.financePageNum = 1;
getFinanceList();
};
// 资金方 列表
const getFinanceList = () => {
data.financeLoading = true;
const params = {
pageNum: data.financePageNum,
pageSize: 10,
type: 3, // 资金方类型
username: data.financeName,
};
memberListByType(params)
.then((res) => {
data.financeLoading = false;
data.financeOptions = res.data.rows;
data.financeTotal = res.data.total;
})
.catch(() => {
data.financeLoading = false;
});
};
// 选择资金方分页
const financeHandleCurrentChange = (val) => {
data.financePageNum = val;
getFinanceList();
};
// 选择资金方
const financeChange = (e) => {
if (e) {
ruleForm.fundId = data.financeOptions.find((item) => item.mobile == e).id;
} else {
ruleForm.fundId = '';
}
};
// 司机远程搜索
const driverRemoteMethod = (e) => {
data.driverName = e;
data.driverPageNum = 1;
getDriverList();
};
// 列表
const getDriverList = () => {
data.driverLoading = true;
const params = {
pageNum: data.driverPageNum,
pageSize: 10,
username: data.driverName,
};
driverList(params)
.then((res) => {
data.driverLoading = false;
data.driverOptions = res.data.rows;
data.driverTotal = res.data.total;
})
.catch(() => {
data.driverLoading = false;
});
};
// 选择司机分页
const driverHandleCurrentChange = (val) => {
data.driverPageNum = val;
getDriverList();
};
// 选择司机
const driverChange = (e) => {
if (e) {
ruleForm.driverId = data.driverOptions.find((item) => item.mobile == e).id;
} else {
ruleForm.driverId = '';
}
};
// ----------------
// 采购商远程搜索
const purchaserRemoteMethod = (e) => {
data.purchaserName = e;
data.purchaserPageNum = 1;
getPurchaserList();
};
// 采购商 列表
const getPurchaserList = () => {
data.purchaserLoading = true;
@@ -573,99 +200,76 @@ const getPurchaserList = () => {
data.purchaserLoading = false;
});
};
// 采购商分页
const purchaserHandleCurrentChange = (val) => {
data.purchaserPageNum = val;
getPurchaserList();
};
// 选择采购商
const purchaserChange = (e) => {
if (e) {
ruleForm.buyerId = data.purchaserOptions.find((item) => item.mobile == e).id;
} else {
ruleForm.buyerId = '';
}
};
// 保存
const onClickSave = () => {
if (formDataRef.value) {
formDataRef.value.validate((valid) => {
if (valid) {
const params = {
deliveryTitle: ruleForm.deliveryTitle,
ratedQuantity: ruleForm.ratedQuantity,
supplierId: ruleForm.supplier.join(','),
fundId: ruleForm.fundId,
driverId: ruleForm.driverId,
buyerId: ruleForm.buyerId,
buyerPrice: ruleForm.buyerPrice,
salePrice: ruleForm.salePrice,
firmPrice: ruleForm.firmPrice,
startLocation: ruleForm.startLocation,
startLat: ruleForm.startLat,
startLon: ruleForm.startLon,
endLocation: ruleForm.endLocation,
endLat: ruleForm.endLat,
endLon: ruleForm.endLon,
id: ruleForm.id,
buyerId: ruleForm.buyerId.join(','), // 将数组转为逗号分隔的字符串
sellerId: ruleForm.sellerId.join(','), // 将数组转为逗号分隔的字符串
settlementType: ruleForm.settlementType,
};
data.saveLoading = true;
orderAdd(params)
.then((res) => {
data.saveLoading = false;
if (res.code === 200) {
ElMessage({
message: res.msg,
type: 'success',
});
emits('success');
if (formDataRef.value) {
formDataRef.value.resetFields();
data.dialogVisible = false;
}
} else {
ElMessage.error(res.msg);
}
})
.catch((err) => {
data.saveLoading = false;
});
// 根据是否有ID判断是新增还是编辑
const savePromise = ruleForm.id ? orderUpdate(params) : orderAddNew(params);
savePromise.then((res) => {
data.saveLoading = false;
if (res.code === 200) {
ElMessage({
message: res.msg,
type: 'success',
});
emits('success');
handleClose();
} else {
ElMessage.error(res.msg);
}
})
.catch((err) => {
data.saveLoading = false;
});
} else {
console.log('error submit!');
}
});
}
};
const onShowDialog = () => {
const onShowDialog = (orderData) => {
if (formDataRef.value) {
formDataRef.value.resetFields();
}
// 重置表单数据
ruleForm.id = orderData?.id || null;
ruleForm.buyerId = orderData?.buyerId ? orderData.buyerId.split(',') : [];
ruleForm.sellerId = orderData?.sellerId ? orderData.sellerId.split(',') : [];
ruleForm.settlementType = orderData?.settlementType || 1;
data.dialogVisible = true;
getDriverList();
getPurchaserList();
getFinanceList();
getSupplierList();
getPurchaserList();
};
defineExpose({
onShowDialog,
});
</script>
<style lang="less" scoped>
::v-deep .anchorBL {
display: none;
visibility: hidden;
}
.bm-view {
border-radius: 4px;
overflow: hidden;
/* 优化WebGL渲染 */
transform: translateZ(0);
-webkit-transform: translateZ(0);
will-change: transform;
}
.maps {
border-radius: 4px;
overflow: hidden;
::v-deep .el-select-dropdown__list {
padding-bottom: 40px;
}
</style>

View File

@@ -6,17 +6,21 @@
</div>
<div class="main-container">
<el-table :data="data.rows" border v-loading="data.dataListLoading" element-loading-text="数据加载中..." style="width: 100%">
<el-table-column label="用户姓名" prop="id"></el-table-column>
<el-table-column label="用户手机号" prop="roleName"> </el-table-column>
<el-table-column label="用户类型" prop="mobile"></el-table-column>
<el-table-column label="账号状态" prop="name"></el-table-column>
<el-table-column label="备注" prop="roleName"> </el-table-column>
<el-table-column label="创建人" prop="mobile"></el-table-column>
<el-table-column label="创建时间" prop="name"></el-table-column>
<el-table-column label="操作" width="120">
<el-table-column label="用户姓名" prop="name"></el-table-column>
<el-table-column label="用户手机号" prop="mobile"> </el-table-column>
<el-table-column label="用户类型" prop="roleName"></el-table-column>
<el-table-column label="账号状态" prop="status">
<template #default="scope">
<el-button link type="primary" @click="showAddDialog(scope.row)">编辑111</el-button>
<el-button link type="primary" @click="delClick(scope.row)">删除111</el-button>
{{ scope.row.status === 1 ? '启用' : '禁用' }}
</template>
</el-table-column>
<el-table-column label="备注" prop="remark"> </el-table-column>
<el-table-column label="创建人" prop="createBy"></el-table-column>
<el-table-column label="创建时间" prop="createTime"></el-table-column>
<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>
<template #empty>
@@ -30,58 +34,97 @@
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import baseSearch from '@/components/common/searchCustom/index.vue';
import { sysUserList, sysUserDel, sysUserSave } from '@/api/sys.js';
const baseSearchRef = ref();
const formItemList = reactive([
{
label: '用户姓名',
prop: 'username',
type: 'input',
placeholder: '请输入用户名',
param: 'name',
placeholder: '请输入用户姓名',
span: 7,
labelWidth: 100,
},
{
label: '用户手机号',
prop: 'phone',
type: 'input',
param: 'mobile',
placeholder: '请输入用户手机号',
span: 7,
labelWidth: 100,
},
{
label: '用户类型',
type: 'select',
selectOptions: [
{ value: 1, text: '供应商' },
{ value: 2, text: '资金方' },
{ value: 3, text: '采购商' },
],
param: 'type',
span: 7,
labelWidth: 100,
},
]);
const searchFrom = () => {
form.pageNum = 1;
getDataList();
};
const data = reactive({
rows: [
{
id: '1',
},
],
rows: [],
total: 0,
dataListLoading: false,
});
const form = reactive({
pageNum: 1,
pageSize: 10,
});
// 列表
const getDataList = () => {};
const getDataList = () => {
data.dataListLoading = true;
const params = {
...form,
...baseSearchRef.value?.penetrateParams(),
};
sysUserList(params)
.then((res) => {
data.dataListLoading = false;
data.rows = res.data.rows || [];
data.total = res.data.total || 0;
})
.catch(() => {
data.dataListLoading = false;
});
};
// 删除用户
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(() => {
// 用户取消删除
});
};
// 编辑用户
const showAddDialog = (row) => {
// TODO: 实现编辑对话框
console.log('编辑用户:', row);
};
onMounted(() => {
getDataList();
});
</script>
<style lang="less" scoped></style>

View File

@@ -6,23 +6,19 @@
</div>
<div class="main-container">
<el-table :data="data.rows" border v-loading="data.dataListLoading" element-loading-text="数据加载中..." style="width: 100%">
<el-table-column label="司机姓名" prop="username"></el-table-column>
<el-table-column label="司机手机号" prop="mobile"> </el-table-column>
<el-table-column label="车牌号" prop="car_number"></el-table-column>
<el-table-column label="车辆照片" prop="car_img" min-width="120">
<el-table-column label="司机姓名" prop="username" width="120"></el-table-column>
<el-table-column label="司机手机号" prop="mobile" width="130"></el-table-column>
<el-table-column label="驾驶证照片" prop="driver_license" min-width="120">
<template #default="scope">
<div style="display: flex; flex-wrap: wrap; gap: 5px">
<template v-if="scope.row.car_img && getImageListSync(scope.row.car_img).length > 0">
<template v-if="scope.row.driver_license">
<el-image
v-for="(item, index) in getImageListSync(scope.row.car_img)"
:key="index"
:src="item"
:src="scope.row.driver_license"
style="width: 60px; height: 60px; border-radius: 4px; border: 1px solid #dcdfe6"
fit="cover"
:preview-src-list="getImageListSync(scope.row.car_img)"
:preview-src-list="[scope.row.driver_license]"
preview-teleported
:lazy="true"
@error="handleImageErrorLocal(item, index)"
>
<template #error>
<div style="width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; background: #f5f7fa; border-radius: 4px; color: #909399">
@@ -35,13 +31,36 @@
</div>
</template>
</el-table-column>
<el-table-column label="备注" prop="remark"> </el-table-column>
<el-table-column label="创建时间" prop="create_time"></el-table-column>
<el-table-column label="身份证照片" prop="id_card" min-width="120">
<template #default="scope">
<div style="display: flex; flex-wrap: wrap; gap: 5px">
<template v-if="scope.row.id_card">
<el-image
v-for="(url, index) in scope.row.id_card.split(',')"
:key="index"
:src="url.trim()"
style="width: 60px; height: 60px; border-radius: 4px; border: 1px solid #dcdfe6"
fit="cover"
:preview-src-list="scope.row.id_card.split(',')"
preview-teleported
:lazy="true"
>
<template #error>
<div style="width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; background: #f5f7fa; border-radius: 4px; color: #909399">
<el-icon size="20"><Picture /></el-icon>
</div>
</template>
</el-image>
</template>
<span v-else style="color: #999; font-size: 12px">暂无图片</span>
</div>
</template>
</el-table-column>
<el-table-column label="创建时间" prop="create_time" width="180"></el-table-column>
<el-table-column label="操作" width="160">
<template #default="scope">
<el-button link type="primary" @click="showAddDialog(scope.row)">编辑</el-button>
<!-- <el-button link type="primary" @click="delClick(scope.row)">删除</el-button> -->
<el-button link type="primary" @click="showDetailDialog(scope.row)">详情</el-button>
<el-button link type="danger" @click="delClick(scope.row)">删除</el-button>
</template>
</el-table-column>
<template #empty>
@@ -50,23 +69,20 @@
</el-table>
<pagination v-model:limit="form.pageSize" v-model:page="form.pageNum" :total="data.total" @pagination="getDataList" />
<DriverDialog ref="DriverDialogRef" @success="getDataList" />
<DetailDialog ref="DetailDialogRef" />
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Picture } from '@element-plus/icons-vue';
import baseSearch from '@/components/common/searchCustom/index.vue';
import { driverList } from '@/api/userManage.js';
import { driverList, driverDel } from '@/api/userManage.js';
import DriverDialog from './driverDialog.vue';
import DetailDialog from './driverDetailDialog.vue';
import { getImageList, getImageListWithProxy, getImageListSmart, testImageUrl, handleImageError } from '@/utils/imageUtils.js';
const baseSearchRef = ref();
const DriverDialogRef = ref();
const DetailDialogRef = ref();
const formItemList = reactive([
{
label: '司机姓名',
@@ -106,48 +122,9 @@ const getDataList = () => {
...baseSearchRef.value.penetrateParams(),
};
// 添加调试信息
console.log('发送查询参数:', params);
driverList(params)
.then((res) => {
data.dataListLoading = false;
console.log('查询结果:', res);
console.log('司机数据详情:', res.data.rows);
// 特别检查car_img字段 - 分析能显示和不能显示的图片差异
if (res.data.rows && res.data.rows.length > 0) {
res.data.rows.forEach(async (driver, index) => {
console.log(`=== 司机${index + 1}: ${driver.username} (${driver.mobile}) ===`);
console.log(`car_img字段:`, driver.car_img);
console.log(`car_img类型:`, typeof driver.car_img);
if (driver.car_img) {
const urls = getImageList(driver.car_img);
console.log(`解析后的图片URLs:`, urls);
// 分析URL特征
urls.forEach((url, urlIndex) => {
console.log(`URL ${urlIndex + 1} 分析:`);
console.log(` - 完整URL: ${url}`);
console.log(` - 长度: ${url.length}`);
console.log(` - 是否HTTPS: ${url.startsWith('https://')}`);
console.log(` - 域名: ${url.includes('cos.ap-guangzhou.myqcloud.com') ? '腾讯云COS' : '其他'}`);
console.log(` - 文件名: ${url.split('/').pop()}`);
// 检查是否是车辆照片还是其他类型图片
const fileName = url.split('/').pop().toLowerCase();
if (fileName.includes('cow') || fileName.includes('4c4e')) {
console.log(` - 图片类型: 车辆照片 (可能CORS受限)`);
} else {
console.log(` - 图片类型: 其他图片 (可能不受CORS限制)`);
}
});
} else {
console.log(`car_img字段为空`);
}
console.log(`---`);
});
}
data.rows = res.data.rows;
data.total = res.data.total;
})
@@ -158,68 +135,33 @@ const getDataList = () => {
};
// 删除
const delClick = (row) => {
ElMessageBox.confirm('请确认是否删除该数据?', '删除', {
ElMessageBox.confirm('请确认是否删除该司机数据?', '删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error',
type: 'warning',
})
.then(() => {
ElMessage({
type: 'success',
message: '删除成功',
});
driverDel(row.id)
.then(() => {
ElMessage.success('删除成功');
getDataList();
})
.catch((error) => {
ElMessage.error('删除失败');
console.error('删除失败:', error);
});
})
.catch(() => {
ElMessage({
type: 'info',
message: '取消删除',
});
// 用户取消删除
});
};
// 新增司机
// 新增/编辑司机
const showAddDialog = (row) => {
if (DriverDialogRef.value) {
DriverDialogRef.value.onShowDialog(row);
}
};
// 详情
const showDetailDialog = (row) => {
if (DetailDialogRef.value) {
DetailDialogRef.value.onShowDetailDialog(row);
}
};
// 使用智能处理方案 - 根据图片域名决定是否使用代理
const getImageListSync = (imageUrl) => {
return getImageListSmart(imageUrl);
};
// 异步版本用于调试
const getImageListAsync = async (imageUrl) => {
const urls = getImageList(imageUrl);
console.log('getImageList 输入参数:', imageUrl);
console.log('getImageList 参数类型:', typeof imageUrl);
console.log('getImageList 解析结果:', urls);
// 测试每个URL的可访问性
for (let i = 0; i < urls.length; i++) {
const url = urls[i];
console.log(`URL ${i + 1}:`, url);
console.log(`URL ${i + 1} 是否以http开头:`, url.startsWith('http'));
console.log(`URL ${i + 1} 长度:`, url.length);
// 测试URL可访问性
const isAccessible = await testImageUrl(url);
console.log(`URL ${i + 1} 可访问性:`, isAccessible);
}
return urls;
};
// 使用工具函数处理图片错误
const handleImageErrorLocal = (imageUrl, index) => {
handleImageError(imageUrl, index);
};
onMounted(() => {
getDataList();

View File

@@ -7,158 +7,38 @@
<el-form-item label="司机手机号" prop="mobile">
<el-input v-model="ruleForm.mobile" placeholder="请输入司机手机号" clearable></el-input>
</el-form-item>
<el-form-item label="账号状态" prop="status">
<el-radio-group v-model="ruleForm.status">
<el-radio :value="0">启用</el-radio>
<el-radio :value="1">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="车牌号" prop="carNumber">
<el-input v-model="ruleForm.carNumber" placeholder="请输入车牌号" clearable></el-input>
</el-form-item>
<el-form-item label="驾驶证" prop="driverImg">
<el-form-item label="驾驶证照片" prop="driver_license">
<el-upload
:limit="2"
list-type="picture-card"
action="/api/common/upload"
:on-success="
(response, file, fileList) => {
return handleAvatarSuccess(response, file, fileList, 'driverImg');
}
"
:on-success="(response, file, fileList) => handleAvatarSuccess(response, file, fileList, 'driver_license')"
:on-preview="handlePreview"
:on-remove="
(file, fileList) => {
return handleRemove(file, fileList, 'driverImg');
}
"
:on-remove="(file, fileList) => handleRemove(file, fileList, 'driver_license')"
:before-upload="beforeAvatarUpload"
:file-list="ruleForm.driverImg"
:file-list="ruleForm.driver_license"
:headers="importHeaders"
:on-exceed="
(files, uploadFiles) => {
return handleExceed(files, uploadFiles, 2);
}
"
:on-exceed="() => handleExceed(2)"
>
<el-icon><Plus /></el-icon>
</el-upload>
</el-form-item>
<el-form-item label="行驶证" prop="licenseImg">
<el-form-item label="身份证照片" prop="id_card">
<el-upload
:limit="2"
list-type="picture-card"
action="/api/common/upload"
:on-success="
(response, file, fileList) => {
return handleAvatarSuccess(response, file, fileList, 'licenseImg');
}
"
:on-success="(response, file, fileList) => handleAvatarSuccess(response, file, fileList, 'id_card')"
:on-preview="handlePreview"
:on-remove="
(file, fileList) => {
return handleRemove(file, fileList, 'licenseImg');
}
"
:on-remove="(file, fileList) => handleRemove(file, fileList, 'id_card')"
:before-upload="beforeAvatarUpload"
:file-list="ruleForm.licenseImg"
:file-list="ruleForm.id_card"
:headers="importHeaders"
:on-exceed="
(files, uploadFiles) => {
return handleExceed(files, uploadFiles, 2);
}
"
:on-exceed="() => handleExceed(2)"
>
<el-icon><Plus /></el-icon>
</el-upload>
</el-form-item>
<el-form-item label="牧运通备案码" prop="codeImg">
<el-upload
:limit="2"
list-type="picture-card"
action="/api/common/upload"
:on-success="
(response, file, fileList) => {
return handleAvatarSuccess(response, file, fileList, 'codeImg');
}
"
:on-preview="handlePreview"
:on-remove="
(file, fileList) => {
return handleRemove(file, fileList, 'codeImg');
}
"
:before-upload="beforeAvatarUpload"
:file-list="ruleForm.codeImg"
:headers="importHeaders"
:on-exceed="
(files, uploadFiles) => {
return handleExceed(files, uploadFiles, 2);
}
"
>
<el-icon><Plus /></el-icon>
</el-upload>
</el-form-item>
<el-form-item label="车头&车身照片" prop="carImg">
<el-upload
:limit="2"
list-type="picture-card"
action="/api/common/upload"
:on-success="
(response, file, fileList) => {
return handleAvatarSuccess(response, file, fileList, 'carImg');
}
"
:on-preview="handlePreview"
:on-remove="
(file, fileList) => {
return handleRemove(file, fileList, 'carImg');
}
"
:before-upload="beforeAvatarUpload"
:file-list="ruleForm.carImg"
:headers="importHeaders"
:on-exceed="
(files, uploadFiles) => {
return handleExceed(files, uploadFiles, 2);
}
"
>
<el-icon><Plus /></el-icon>
</el-upload>
</el-form-item>
<el-form-item label="身份证前后面" prop="idCardImg">
<el-upload
:limit="2"
list-type="picture-card"
action="/api/common/upload"
:on-success="
(response, file, fileList) => {
return handleAvatarSuccess(response, file, fileList, 'idCardImg');
}
"
:on-preview="handlePreview"
:on-remove="
(file, fileList) => {
return handleRemove(file, fileList, 'idCardImg');
}
"
:before-upload="beforeAvatarUpload"
:file-list="ruleForm.idCardImg"
:headers="importHeaders"
:on-exceed="
(files, uploadFiles) => {
return handleExceed(files, uploadFiles, 2);
}
"
>
<el-icon><Plus /></el-icon>
</el-upload>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="ruleForm.remark" placeholder="请输入备注" clearable></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
@@ -174,13 +54,15 @@
<script setup>
import { ref, reactive, onMounted, nextTick } from 'vue';
import { ElMessage } from 'element-plus';
import { Plus } from '@element-plus/icons-vue';
import { driverAdd, driverEdit } from '@/api/userManage.js';
import { checkMobile } from '~/utils/validateFuns.js';
import { useUserStore } from '../../store/user';
const userStore = useUserStore();
const importHeaders = reactive({ Authorization: userStore.$state.token });
const emits = defineEmits();
const formDataRef = ref();
const data = reactive({
dialogVisible: false,
@@ -188,18 +70,15 @@ const data = reactive({
saveLoading: false,
dialogVisibleImg: false,
dialogImageUrl: '',
isEdit: false,
});
const ruleForm = reactive({
username: '', // 司机姓名
mobile: '', // 司机手机号
status: '', // 账号状态
carNumber: '', // 车牌号
driverImg: [], // 驾驶证
licenseImg: [], // 行驶证
codeImg: [], // 牧运通备案码
carImg: [], // 车头&车身照片
idCardImg: [], // 身份证前后面
remark: '', // 备注
id: null,
username: '',
mobile: '',
driver_license: [], // 驾驶证照片
id_card: [], // 身份证照片
});
const rules = reactive({
@@ -210,228 +89,141 @@ const rules = reactive({
validator(rule, value, callback) {
if (!value) {
callback(new Error('请输入司机手机号'));
}
if (!checkMobile(value)) {
} else if (!/^1[3-9]\d{9}$/.test(value)) {
callback(new Error('请输入正确的手机号'));
} else {
callback();
}
callback();
},
trigger: 'blur',
},
],
status: [{ required: true, message: '请选择账户状态', trigger: 'change' }],
carNumber: [{ required: true, message: '请输入车牌号', trigger: 'blur' }],
driverImg: [{ required: true, message: '请上传驾驶证', trigger: 'change' }],
licenseImg: [{ required: true, message: '请上传行驶证', trigger: 'change' }],
codeImg: [{ required: true, message: '请上传牧运通备案码', trigger: 'change' }],
carImg: [{ required: true, message: '请上传车头&车身照片', trigger: 'change' }],
idCardImg: [{ required: true, message: '请上传身份证前后面', trigger: 'change' }],
driver_license: [{ required: true, message: '请上传驾驶证照片', trigger: 'change' }],
id_card: [{ required: true, message: '请上传身份证照片', trigger: 'change' }],
});
const handleAvatarSuccess = (res, file, fileList, type) => {
console.log('上传成功响应:', res);
console.log('文件信息:', file);
console.log('类型:', type);
if (ruleForm.hasOwnProperty(type)) {
// 检查响应格式,支持多种可能的响应结构
let imageUrl = null;
if (res && res.data && res.data.src) {
// 格式1: res.data.src
imageUrl = res.data.src;
} else if (res && res.data && typeof res.data === 'string') {
// 格式2: res.data 直接是URL字符串
imageUrl = res.data;
} else if (res && res.url) {
// 格式3: res.url
imageUrl = res.url;
} else if (res && typeof res === 'string') {
// 格式4: res 直接是URL字符串
imageUrl = res;
} else {
console.error('无法解析上传响应:', res);
ElMessage.error('图片上传失败:响应格式不正确');
return;
}
console.log('解析到的图片URL:', imageUrl);
ruleForm[type].push({ url: imageUrl });
if (imageUrl) {
// 直接更新 fileList
file.url = imageUrl;
ruleForm[type] = fileList;
console.log(`${type} 上传成功:`, imageUrl, 'fileList:', fileList);
} else {
console.error('无法解析图片URL:', res);
ElMessage.error('上传失败无法获取图片URL');
}
}
};
// 移除
const handleRemove = (file, fileList, type) => {
if (ruleForm.hasOwnProperty(type)) {
ruleForm[type] = fileList;
}
};
// 上传时 - 判断
const beforeAvatarUpload = (file) => {
const isJPG = file.type === 'image/jpeg' || file.type === 'image/png';
const isLt1M = file.size / 1024 / 1024 < 2;
if (!isJPG) {
ElMessage.error('上传头像图片只能是 JPG 格式!');
const beforeAvatarUpload = (file) => {
const isImage = file.type.startsWith('image/');
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isImage) {
ElMessage.error('上传文件只能是图片格式!');
}
if (!isLt1M) {
ElMessage.error('上传头像图片大小不能超过 2MB!');
if (!isLt2M) {
ElMessage.error('上传图片大小不能超过 2MB!');
}
return isJPG && isLt1M;
return isImage && isLt2M;
};
// 超出限制
const handleExceed = (files, uploadFiles, number) => {
ElMessage({
message: `最多上传${number}张照片`,
type: 'warning',
});
const handleExceed = (number) => {
ElMessage.warning(`最多只能上传${number}张图片!`);
};
// 预览
const handlePreview = (file) => {
data.dialogImageUrl = file.url;
data.dialogVisibleImg = true;
};
const handleClose = () => {
if (formDataRef.value) {
formDataRef.value.resetFields();
}
formDataRef.value?.resetFields();
ruleForm.id = null;
ruleForm.username = '';
ruleForm.mobile = '';
ruleForm.driver_license = [];
ruleForm.id_card = [];
data.dialogVisible = false;
data.isEdit = false;
};
// 保存按钮
const onClickSave = () => {
if (formDataRef.value) {
formDataRef.value.validate((valid) => {
if (valid) {
const params = {
username: ruleForm.username,
mobile: ruleForm.mobile,
status: ruleForm.status,
carNumber: ruleForm.carNumber,
remark: ruleForm.remark,
};
params.driverLicense = ruleForm.driverImg.length > 0 ? ruleForm.driverImg.map((item) => item.url).join(',') : '';
params.drivingLicense = ruleForm.licenseImg.length > 0 ? ruleForm.licenseImg.map((item) => item.url).join(',') : '';
params.carImg = ruleForm.carImg.length > 0 ? ruleForm.carImg.map((item) => item.url).join(',') : '';
params.recordCode = ruleForm.codeImg.length > 0 ? ruleForm.codeImg.map((item) => item.url).join(',') : '';
params.idCard = ruleForm.idCardImg.length > 0 ? ruleForm.idCardImg.map((item) => item.url).join(',') : '';
// params.recordCode = ruleForm.codeImg.length > 0 ? ruleForm.codeImg[0].url : '';
data.saveLoading = true;
if (data.title === '新增司机') {
driverAdd(params)
.then((res) => {
data.saveLoading = false;
if (res.code === 200) {
ElMessage({
message: res.msg,
type: 'success',
});
emits('success');
if (formDataRef.value) {
formDataRef.value.resetFields();
data.dialogVisible = false;
} else {
data.saveLoading = false;
ElMessage.error(res.msg);
}
}
})
.catch((err) => {
data.saveLoading = false;
});
formDataRef.value.validate((valid) => {
if (valid) {
data.saveLoading = true;
const params = {
id: ruleForm.id,
username: ruleForm.username,
mobile: ruleForm.mobile,
driverLicense: ruleForm.driver_license.length > 0 ? ruleForm.driver_license.map((item) => item.url).join(',') : '',
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) => {
data.saveLoading = false;
if (res.code === 200) {
ElMessage.success(data.isEdit ? '编辑成功' : '新增成功');
handleClose();
emits('success');
} else {
params.id = ruleForm.id;
driverEdit(params)
.then((res) => {
data.saveLoading = false;
if (res.code === 200) {
ElMessage({
message: res.msg,
type: 'success',
});
emits('success');
if (formDataRef.value) {
formDataRef.value.resetFields();
data.dialogVisible = false;
} else {
data.saveLoading = false;
ElMessage.error(res.msg);
}
}
})
.catch((err) => {
data.saveLoading = false;
});
ElMessage.error(res.msg || '操作失败');
}
} else {
console.log('error submit!');
}
});
}
}).catch((error) => {
data.saveLoading = false;
console.error('保存失败:', error);
ElMessage.error('保存失败,请稍后重试');
});
}
});
};
const onShowDialog = (row) => {
data.title = row ? '编辑司机' : '新增司机';
const onShowDialog = (row = null) => {
data.dialogVisible = true;
if (formDataRef.value) {
formDataRef.value.resetFields();
}
data.isEdit = !!row;
data.title = row ? '编辑司机' : '新增司机';
if (row) {
nextTick(() => {
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; // 备注
ruleForm.driverImg = row.driver_license
? getImageList(row.driver_license).map((item) => {
return {
url: item,
};
})
: [];
ruleForm.licenseImg = row.driving_license
? getImageList(row.driving_license).map((item) => {
return {
url: item,
};
})
: [];
ruleForm.carImg = row.car_img
? getImageList(row.car_img).map((item) => {
return {
url: item,
};
})
: [];
ruleForm.codeImg = row.record_code
? getImageList(row.record_code).map((item) => {
return {
url: item,
};
})
: [];
ruleForm.idCardImg = row.id_card
? getImageList(row.id_card).map((item) => {
return {
url: item,
};
})
: [];
});
ruleForm.id = row.id;
ruleForm.username = row.username || '';
ruleForm.mobile = row.mobile || '';
ruleForm.driver_license = row.driver_license ? row.driver_license.split(',').map(url => ({ url: url.trim() })) : [];
ruleForm.id_card = row.id_card ? row.id_card.split(',').map(url => ({ url: url.trim() })) : [];
}
};
// 处理逗号分隔的图片URL
const getImageList = (imageUrl) => {
if (!imageUrl || imageUrl.trim() === '') {
return [];
}
// 按逗号分割并过滤空字符串
return imageUrl.split(',').map(url => url.trim()).filter(url => url !== '');
};
defineExpose({
onShowDialog,
});
defineExpose({ onShowDialog });
</script>
<style lang="less" scoped></style>
<style scoped>
.dialog-footer {
display: flex;
justify-content: flex-end;
}
</style>

View File

@@ -0,0 +1,218 @@
<template>
<div>
<base-search :formItemList="formItemList" @search="searchFrom" ref="baseSearchRef"></base-search>
<div style="display: flex; padding: 10px; background: #fff; margin-bottom: 10px">
<el-button type="primary" @click="showAddDialog(null)">新增车辆</el-button>
</div>
<div class="main-container">
<el-table :data="data.rows" border v-loading="data.dataListLoading" element-loading-text="数据加载中..." style="width: 100%">
<el-table-column label="车牌号" prop="licensePlate" width="120"></el-table-column>
<el-table-column label="车头照片" prop="carFrontPhoto" min-width="120">
<template #default="scope">
<el-image
v-if="scope.row.carFrontPhoto"
:src="scope.row.carFrontPhoto"
style="width: 60px; height: 60px; border-radius: 4px; border: 1px solid #dcdfe6"
fit="cover"
:preview-src-list="[scope.row.carFrontPhoto]"
preview-teleported
:lazy="true"
>
<template #error>
<div style="width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; background: #f5f7fa; border-radius: 4px; color: #909399">
<el-icon size="20"><Picture /></el-icon>
</div>
</template>
</el-image>
<span v-else style="color: #999; font-size: 12px">暂无图片</span>
</template>
</el-table-column>
<el-table-column label="车尾照片" prop="carRearPhoto" min-width="120">
<template #default="scope">
<el-image
v-if="scope.row.carRearPhoto"
:src="scope.row.carRearPhoto"
style="width: 60px; height: 60px; border-radius: 4px; border: 1px solid #dcdfe6"
fit="cover"
:preview-src-list="[scope.row.carRearPhoto]"
preview-teleported
:lazy="true"
>
<template #error>
<div style="width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; background: #f5f7fa; border-radius: 4px; color: #909399">
<el-icon size="20"><Picture /></el-icon>
</div>
</template>
</el-image>
<span v-else style="color: #999; font-size: 12px">暂无图片</span>
</template>
</el-table-column>
<el-table-column label="行驶证" prop="drivingLicensePhoto" min-width="120">
<template #default="scope">
<el-image
v-if="scope.row.drivingLicensePhoto"
:src="scope.row.drivingLicensePhoto"
style="width: 60px; height: 60px; border-radius: 4px; border: 1px solid #dcdfe6"
fit="cover"
:preview-src-list="[scope.row.drivingLicensePhoto]"
preview-teleported
:lazy="true"
>
<template #error>
<div style="width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; background: #f5f7fa; border-radius: 4px; color: #909399">
<el-icon size="20"><Picture /></el-icon>
</div>
</template>
</el-image>
<span v-else style="color: #999; font-size: 12px">暂无图片</span>
</template>
</el-table-column>
<el-table-column label="牧运通备案码" prop="recordCode" min-width="120">
<template #default="scope">
<el-image
v-if="scope.row.recordCode"
:src="scope.row.recordCode"
style="width: 60px; height: 60px; border-radius: 4px; border: 1px solid #dcdfe6"
fit="cover"
:preview-src-list="[scope.row.recordCode]"
preview-teleported
:lazy="true"
>
<template #error>
<div style="width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; background: #f5f7fa; border-radius: 4px; color: #909399">
<el-icon size="20"><Picture /></el-icon>
</div>
</template>
</el-image>
<span v-else style="color: #999; font-size: 12px">暂无图片</span>
</template>
</el-table-column>
<el-table-column label="备注" prop="remark" show-overflow-tooltip></el-table-column>
<el-table-column label="创建时间" prop="createTime" width="180"></el-table-column>
<el-table-column label="操作" width="160">
<template #default="scope">
<el-button link type="primary" @click="showAddDialog(scope.row)">编辑</el-button>
<el-button link type="danger" @click="delClick(scope.row)">删除</el-button>
</template>
</el-table-column>
<template #empty>
<div class="dataListOnEmpty">暂无数据</div>
</template>
</el-table>
<pagination v-model:limit="form.pageSize" v-model:page="form.pageNum" :total="data.total" @pagination="getDataList" />
<VehicleDialog ref="VehicleDialogRef" @success="getDataList" />
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Picture } from '@element-plus/icons-vue';
import baseSearch from '@/components/common/searchCustom/index.vue';
import { vehicleList, vehicleDel } from '@/api/userManage.js';
import VehicleDialog from './vehicleDialog.vue';
const baseSearchRef = ref();
const VehicleDialogRef = ref();
const formItemList = reactive([
{
label: '车牌号',
param: 'licensePlate',
type: 'input',
placeholder: '请输入车牌号',
span: 7,
labelWidth: 100,
},
]);
const searchFrom = () => {
form.pageNum = 1;
getDataList();
};
const data = reactive({
rows: [],
total: 0,
dataListLoading: false,
});
const form = reactive({
pageNum: 1,
pageSize: 10,
licensePlate: '',
});
const showAddDialog = (row) => {
VehicleDialogRef.value.open(row);
};
const delClick = (row) => {
ElMessageBox.confirm('请确认是否删除该车辆数据?', '删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
vehicleDel(row.id)
.then(() => {
ElMessage.success('删除成功');
getDataList();
})
.catch((error) => {
ElMessage.error('删除失败');
console.error('删除失败:', error);
});
})
.catch(() => {});
};
const getDataList = async () => {
data.dataListLoading = true;
try {
const params = {
pageNum: form.pageNum,
pageSize: form.pageSize,
licensePlate: form.licensePlate,
};
console.log('查询参数:', 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 || '查询失败');
}
} catch (error) {
console.error('查询车辆列表失败:', error);
ElMessage.error('查询失败,请稍后重试');
} finally {
data.dataListLoading = false;
}
};
onMounted(() => {
getDataList();
});
</script>
<style scoped>
.main-container {
padding: 10px;
background: #fff;
}
.dataListOnEmpty {
text-align: center;
padding: 20px;
color: #999;
}
</style>

View File

@@ -0,0 +1,249 @@
<template>
<el-dialog v-model="data.dialogVisible" :title="data.title" :before-close="handleClose" style="width: 650px; padding-bottom: 20px">
<el-form ref="formDataRef" :model="ruleForm" :rules="rules" label-width="auto">
<el-form-item label="车牌号" prop="licensePlate">
<el-input v-model="ruleForm.licensePlate" placeholder="请输入车牌号" clearable></el-input>
</el-form-item>
<el-form-item label="车头照片" prop="carFrontPhoto">
<el-upload
:limit="1"
list-type="picture-card"
action="/api/common/upload"
:on-success="(response, file, fileList) => handleAvatarSuccess(response, file, fileList, 'carFrontPhoto')"
:on-preview="handlePreview"
:on-remove="(file, fileList) => handleRemove(file, fileList, 'carFrontPhoto')"
:before-upload="beforeAvatarUpload"
:file-list="ruleForm.carFrontPhoto"
:headers="importHeaders"
:on-exceed="handleExceed"
>
<el-icon><Plus /></el-icon>
</el-upload>
</el-form-item>
<el-form-item label="车尾照片" prop="carRearPhoto">
<el-upload
:limit="1"
list-type="picture-card"
action="/api/common/upload"
:on-success="(response, file, fileList) => handleAvatarSuccess(response, file, fileList, 'carRearPhoto')"
:on-preview="handlePreview"
:on-remove="(file, fileList) => handleRemove(file, fileList, 'carRearPhoto')"
:before-upload="beforeAvatarUpload"
:file-list="ruleForm.carRearPhoto"
:headers="importHeaders"
:on-exceed="handleExceed"
>
<el-icon><Plus /></el-icon>
</el-upload>
</el-form-item>
<el-form-item label="行驶证照片" prop="drivingLicensePhoto">
<el-upload
:limit="1"
list-type="picture-card"
action="/api/common/upload"
:on-success="(response, file, fileList) => handleAvatarSuccess(response, file, fileList, 'drivingLicensePhoto')"
:on-preview="handlePreview"
:on-remove="(file, fileList) => handleRemove(file, fileList, 'drivingLicensePhoto')"
:before-upload="beforeAvatarUpload"
:file-list="ruleForm.drivingLicensePhoto"
:headers="importHeaders"
:on-exceed="handleExceed"
>
<el-icon><Plus /></el-icon>
</el-upload>
</el-form-item>
<el-form-item label="牧运通备案码" prop="recordCode">
<el-upload
:limit="1"
list-type="picture-card"
action="/api/common/upload"
:on-success="(response, file, fileList) => handleAvatarSuccess(response, file, fileList, 'recordCode')"
:on-preview="handlePreview"
:on-remove="(file, fileList) => handleRemove(file, fileList, 'recordCode')"
:before-upload="beforeAvatarUpload"
:file-list="ruleForm.recordCode"
:headers="importHeaders"
:on-exceed="handleExceed"
>
<el-icon><Plus /></el-icon>
</el-upload>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="ruleForm.remark" placeholder="请输入备注" clearable></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button :loading="data.saveLoading" type="primary" @click="onClickSave">保存</el-button>
<el-button @click="handleClose">取消</el-button>
</span>
</template>
<el-dialog v-model="data.dialogVisibleImg">
<img w-full :src="data.dialogImageUrl" alt="Preview Image" />
</el-dialog>
</el-dialog>
</template>
<script setup>
import { ref, reactive } from 'vue';
import { ElMessage } from 'element-plus';
import { Plus } from '@element-plus/icons-vue';
import { vehicleAdd, vehicleEdit } from '@/api/userManage.js';
import { useUserStore } from '../../store/user';
const userStore = useUserStore();
const importHeaders = reactive({ Authorization: userStore.$state.token });
const emits = defineEmits(['success']);
const formDataRef = ref();
const data = reactive({
dialogVisible: false,
title: '',
saveLoading: false,
dialogVisibleImg: false,
dialogImageUrl: '',
isEdit: false,
});
const ruleForm = reactive({
id: null,
licensePlate: '',
carFrontPhoto: [],
carRearPhoto: [],
drivingLicensePhoto: [],
recordCode: [],
remark: '',
});
const rules = reactive({
licensePlate: [{ required: true, message: '请输入车牌号', trigger: 'blur' }],
});
const handleAvatarSuccess = (res, file, fileList, type) => {
console.log('上传成功响应:', res);
if (ruleForm.hasOwnProperty(type)) {
let imageUrl = null;
if (res && res.data && res.data.src) {
imageUrl = res.data.src;
} else if (res && res.data && typeof res.data === 'string') {
imageUrl = res.data;
} else if (res && res.url) {
imageUrl = res.url;
} else if (res && typeof res === 'string') {
imageUrl = res;
}
if (imageUrl) {
ruleForm[type] = [{ url: imageUrl, uid: file.uid, name: file.name }];
console.log(`${type} 上传成功:`, imageUrl);
} else {
console.error('无法解析图片URL:', res);
ElMessage.error('上传失败无法获取图片URL');
}
}
};
const handleRemove = (file, fileList, type) => {
ruleForm[type] = fileList;
};
const handlePreview = (file) => {
data.dialogImageUrl = file.url;
data.dialogVisibleImg = true;
};
const beforeAvatarUpload = (file) => {
const isImage = file.type.startsWith('image/');
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isImage) {
ElMessage.error('上传文件只能是图片格式!');
return false;
}
if (!isLt2M) {
ElMessage.error('上传图片大小不能超过 2MB');
return false;
}
return true;
};
const handleExceed = () => {
ElMessage.warning('最多只能上传一张图片!');
};
const onClickSave = async () => {
await formDataRef.value.validate(async (valid) => {
if (valid) {
data.saveLoading = true;
try {
const formData = {
id: data.isEdit ? ruleForm.id : null,
licensePlate: ruleForm.licensePlate,
carFrontPhoto: ruleForm.carFrontPhoto.length > 0 ? ruleForm.carFrontPhoto[0].url : null,
carRearPhoto: ruleForm.carRearPhoto.length > 0 ? ruleForm.carRearPhoto[0].url : null,
drivingLicensePhoto: ruleForm.drivingLicensePhoto.length > 0 ? ruleForm.drivingLicensePhoto[0].url : null,
recordCode: ruleForm.recordCode.length > 0 ? ruleForm.recordCode[0].url : null,
remark: ruleForm.remark,
};
console.log('提交数据:', formData);
const res = data.isEdit ? await vehicleEdit(formData) : await vehicleAdd(formData);
if (res.code === 200) {
ElMessage.success(data.isEdit ? '编辑成功' : '新增成功');
handleClose();
emits('success');
} else {
ElMessage.error(res.msg || '操作失败');
}
} catch (error) {
console.error('保存失败:', error);
ElMessage.error('保存失败,请稍后重试');
} finally {
data.saveLoading = false;
}
}
});
};
const handleClose = () => {
formDataRef.value?.resetFields();
ruleForm.id = null;
ruleForm.licensePlate = '';
ruleForm.carFrontPhoto = [];
ruleForm.carRearPhoto = [];
ruleForm.drivingLicensePhoto = [];
ruleForm.recordCode = [];
ruleForm.remark = '';
data.dialogVisible = false;
data.isEdit = false;
};
const open = (row = null) => {
data.dialogVisible = true;
data.isEdit = !!row;
data.title = row ? '编辑车辆' : '新增车辆';
if (row) {
ruleForm.id = row.id;
ruleForm.licensePlate = row.licensePlate || '';
ruleForm.carFrontPhoto = row.carFrontPhoto ? [{ url: row.carFrontPhoto }] : [];
ruleForm.carRearPhoto = row.carRearPhoto ? [{ url: row.carRearPhoto }] : [];
ruleForm.drivingLicensePhoto = row.drivingLicensePhoto ? [{ url: row.drivingLicensePhoto }] : [];
ruleForm.recordCode = row.recordCode ? [{ url: row.recordCode }] : [];
ruleForm.remark = row.remark || '';
}
};
defineExpose({ open });
</script>
<style scoped>
.dialog-footer {
display: flex;
justify-content: flex-end;
}
</style>