集成百度鹰眼服务AK

This commit is contained in:
xuqiuyun
2025-11-21 17:22:20 +08:00
parent 73051df002
commit 0f963bf535
57 changed files with 6036 additions and 685 deletions

View File

@@ -47,7 +47,8 @@
"vue-use": "^0.2.0",
"vue3-baidu-map": "^1.0.0",
"vue3-json-viewer": "^2.2.2",
"vue3-print-nb": "^0.1.4"
"vue3-print-nb": "^0.1.4",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@commitlint/cli": "^17.1.2",

View File

@@ -48,6 +48,15 @@ export function inspectionList(data) {
data,
});
}
// 查询百度鹰眼轨迹与停留点
export function getYingyanTrack(data) {
return request({
url: '/delivery/yingyan/track',
method: 'POST',
data,
});
}
// 详情
export function waybillDetail(id) {
return request({

View File

@@ -265,6 +265,15 @@ export function orderGetDetail(id) {
});
}
// 批量导入订单
export function orderBatchImport(data) {
return request({
url: '/order/batchImport',
method: 'POST',
data,
});
}
// ==================== 运送清单管理新增 API ====================
// 逻辑删除运送清单

View File

@@ -109,7 +109,7 @@ export default defineComponent({
maxFileSize: {
// 新增最大文件大小属性
type: Number,
default: 2 * 1024 * 1024, // 默认值为2MB
default: 10 * 1024 * 1024, // 默认值为10MB
required: false,
},
},
@@ -188,7 +188,7 @@ export default defineComponent({
if (acceptType.value == 'video/*') {
dialogVisible.value = true;
}
// // },
},
imgPreviewClose: () => {
showImageViewer.value = false;
},

View File

@@ -308,4 +308,31 @@ el-form-item--large asterisk-left .top-title {
::v-deep(.el-form-item) {
width: 100%;
}
/* 修复标签与输入框的垂直居中与水平对齐问题 */
.top-screen :deep(.el-form-item) {
display: flex;
align-items: center;
}
.top-screen :deep(.el-form-item__label) {
display: flex;
align-items: center;
height: 40px; /* 与 Element Plus large 尺寸一致 */
line-height: 40px;
padding-right: 8px;
box-sizing: border-box;
white-space: nowrap;
}
.top-screen :deep(.el-input),
.top-screen :deep(.el-select),
.top-screen :deep(.el-date-editor),
.top-screen :deep(.el-cascader) {
height: 40px;
}
.top-screen :deep(.el-input__wrapper) {
min-height: 40px;
}
</style>

View File

@@ -0,0 +1,262 @@
import { defineStore } from 'pinia';
/**
* 运送清单表单数据缓存 Store
* 支持新增模式和编辑模式的表单数据缓存
*/
export const useDeliveryFormStore = defineStore('deliveryForm', {
state: () => {
return {
// 新增模式的缓存数据
newFormData: {
orderId: null as number | null,
shipper: null as number | null,
buyer: null as number | null,
plateNumber: null as string | null,
driverId: null as number | null,
driverPhone: '' as string,
serverId: null as number | null,
serverDeviceId: '' as string,
eartagIds: [] as number[],
collarIds: [] as number[],
eartagDeviceIds: [] as string[],
collarDeviceIds: [] as string[],
estimatedDepartureTime: '' as string,
estimatedArrivalTime: '' as string,
startLocation: '' as string,
endLocation: '' as string,
startLat: '' as string,
startLon: '' as string,
endLat: '' as string,
endLon: '' as string,
cattleCount: 1 as number,
quarantineCertNo: '' as string,
remark: '' as string,
emptyWeight: null as number | null,
entruckWeight: null as number | null,
landingEntruckWeight: null as number | null,
quarantineTickeyUrl: '' as string,
poundListImg: '' as string,
emptyVehicleFrontPhoto: '' as string,
loadedVehicleFrontPhoto: '' as string,
loadedVehicleWeightPhoto: '' as string,
driverIdCardPhoto: '' as string,
destinationPoundListImg: '' as string,
destinationVehicleFrontPhoto: '' as string,
entruckWeightVideo: '' as string,
emptyWeightVideo: '' as string,
cattleLoadingVideo: '' as string,
controlSlotVideo: '' as string,
cattleLoadingCircleVideo: '' as string,
unloadCattleVideo: '' as string,
destinationWeightVideo: '' as string,
},
// 编辑模式的缓存数据key为editId
editFormData: {} as Record<number, {
editId: number;
orderId: number | null;
shipper: number | null;
buyer: number | null;
plateNumber: string | null;
driverId: number | null;
driverPhone: string;
serverId: number | null;
serverDeviceId: string;
eartagIds: number[];
collarIds: number[];
eartagDeviceIds: string[];
collarDeviceIds: string[];
estimatedDepartureTime: string;
estimatedArrivalTime: string;
startLocation: string;
endLocation: string;
startLat: string;
startLon: string;
endLat: string;
endLon: string;
cattleCount: number;
quarantineCertNo: string;
remark: string;
emptyWeight: number | null;
entruckWeight: number | null;
landingEntruckWeight: number | null;
quarantineTickeyUrl: string;
poundListImg: string;
emptyVehicleFrontPhoto: string;
loadedVehicleFrontPhoto: string;
loadedVehicleWeightPhoto: string;
driverIdCardPhoto: string;
destinationPoundListImg: string;
destinationVehicleFrontPhoto: string;
entruckWeightVideo: string;
emptyWeightVideo: string;
cattleLoadingVideo: string;
controlSlotVideo: string;
cattleLoadingCircleVideo: string;
unloadCattleVideo: string;
destinationWeightVideo: string;
}>,
};
},
persist: {
enabled: true,
strategies: [
{
key: 'deliveryFormCache',
storage: localStorage,
},
],
},
actions: {
/**
* 保存新增模式表单数据
*/
saveNewFormData(data: Partial<typeof this.newFormData>) {
// 深度合并,确保数组和对象正确更新
Object.keys(data).forEach(key => {
const value = data[key];
if (Array.isArray(value)) {
// 数组类型,创建新数组
this.newFormData[key] = [...value] as any;
} else {
// 其他类型,直接赋值
this.newFormData[key] = value as any;
}
});
},
/**
* 获取新增模式表单数据
*/
getNewFormData() {
return { ...this.newFormData };
},
/**
* 清除新增模式表单数据
*/
clearNewFormData() {
this.newFormData = {
orderId: null,
shipper: null,
buyer: null,
plateNumber: null,
driverId: null,
driverPhone: '',
serverId: null,
serverDeviceId: '',
eartagIds: [],
collarIds: [],
eartagDeviceIds: [],
collarDeviceIds: [],
estimatedDepartureTime: '',
estimatedArrivalTime: '',
startLocation: '',
endLocation: '',
startLat: '',
startLon: '',
endLat: '',
endLon: '',
cattleCount: 1,
quarantineCertNo: '',
remark: '',
emptyWeight: null,
entruckWeight: null,
landingEntruckWeight: null,
quarantineTickeyUrl: '',
poundListImg: '',
emptyVehicleFrontPhoto: '',
loadedVehicleFrontPhoto: '',
loadedVehicleWeightPhoto: '',
driverIdCardPhoto: '',
destinationPoundListImg: '',
destinationVehicleFrontPhoto: '',
entruckWeightVideo: '',
emptyWeightVideo: '',
cattleLoadingVideo: '',
controlSlotVideo: '',
cattleLoadingCircleVideo: '',
unloadCattleVideo: '',
destinationWeightVideo: '',
};
},
/**
* 保存编辑模式表单数据
*/
saveEditFormData(editId: number, data: Partial<typeof this.editFormData[number]>) {
if (!this.editFormData[editId]) {
this.editFormData[editId] = {
editId,
orderId: null,
shipper: null,
buyer: null,
plateNumber: null,
driverId: null,
driverPhone: '',
serverId: null,
serverDeviceId: '',
eartagIds: [],
collarIds: [],
eartagDeviceIds: [],
collarDeviceIds: [],
estimatedDepartureTime: '',
estimatedArrivalTime: '',
startLocation: '',
endLocation: '',
startLat: '',
startLon: '',
endLat: '',
endLon: '',
cattleCount: 1,
quarantineCertNo: '',
remark: '',
emptyWeight: null,
entruckWeight: null,
landingEntruckWeight: null,
quarantineTickeyUrl: '',
poundListImg: '',
emptyVehicleFrontPhoto: '',
loadedVehicleFrontPhoto: '',
loadedVehicleWeightPhoto: '',
driverIdCardPhoto: '',
destinationPoundListImg: '',
destinationVehicleFrontPhoto: '',
entruckWeightVideo: '',
emptyWeightVideo: '',
cattleLoadingVideo: '',
controlSlotVideo: '',
cattleLoadingCircleVideo: '',
unloadCattleVideo: '',
destinationWeightVideo: '',
};
}
this.editFormData[editId] = { ...this.editFormData[editId], ...data, editId };
},
/**
* 获取编辑模式表单数据
*/
getEditFormData(editId: number) {
return this.editFormData[editId] ? { ...this.editFormData[editId] } : null;
},
/**
* 清除指定编辑模式表单数据
*/
clearEditFormData(editId: number) {
if (this.editFormData[editId]) {
delete this.editFormData[editId];
}
},
/**
* 清除所有缓存
*/
clearAllCache() {
this.clearNewFormData();
this.editFormData = {};
},
},
});

View File

@@ -58,12 +58,36 @@
</div>
</template>
</el-table-column>
<el-table-column prop="startLocation" label="起始地" />
<el-table-column prop="endLocation" label="送达目的地" />
<el-table-column prop="createTime" label="创建时间" />
<el-table-column prop="estimatedDeliveryTime" label="预计送达时间" />
<el-table-column prop="driverName" label="司机姓名" />
<el-table-column prop="createByName" label="创建人" />
<el-table-column prop="startLocation" label="起始地">
<template #default="scope">
{{ scope.row.startLocation || '--' }}
</template>
</el-table-column>
<el-table-column prop="endLocation" label="送达目的地">
<template #default="scope">
{{ scope.row.endLocation || '--' }}
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间">
<template #default="scope">
{{ scope.row.createTime || '--' }}
</template>
</el-table-column>
<el-table-column prop="estimatedDeliveryTime" label="预计送达时间">
<template #default="scope">
{{ scope.row.estimatedDeliveryTime || '--' }}
</template>
</el-table-column>
<el-table-column prop="driverName" label="司机姓名">
<template #default="scope">
{{ scope.row.driverName || '--' }}
</template>
</el-table-column>
<el-table-column prop="createByName" label="创建人">
<template #default="scope">
{{ scope.row.createByName || '--' }}
</template>
</el-table-column>
<el-table-column label="预警类型" prop="warningType">
<template #default="scope">
<el-tag type="warning" v-if="scope.row.warningType == 3">运输距离预警</el-tag>

View File

@@ -6,6 +6,20 @@
:close-on-click-modal="false"
@close="handleClose"
>
<!-- 轨迹定位按钮 -->
<template #header>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>{{ dialogTitle }}</span>
<el-button
type="primary"
:icon="Location"
@click="handleTrackClick"
:disabled="!warningData.deliveryId"
>
轨迹定位
</el-button>
</div>
</template>
<!-- 温度预警 - 只显示设备信息不显示地图 -->
<div v-if="isTemperatureWarning" class="warning-content temperature-warning">
<!-- 预警基本信息 -->
@@ -54,6 +68,10 @@
<!-- 绑定设备列表 -->
<div v-if="deviceList.length > 0" class="device-list-section">
<!-- 调试信息开发环境可显示 -->
<div v-if="false" style="font-size: 12px; color: #909399; margin-bottom: 10px;">
调试设备数量={{ deviceList.length }}, deliveryId={{ warningData.deliveryId }}
</div>
<div class="section-header">
<h4>
<el-icon style="vertical-align: middle;"><Connection /></el-icon>
@@ -90,8 +108,16 @@
</el-table-column>
</el-table>
</div>
<div v-else class="no-data-tip">
<div v-else-if="!loadingDevices" class="no-data-tip">
<el-empty description="暂无绑定设备信息" :image-size="80" />
<div style="margin-top: 10px; font-size: 12px; color: #909399;">
<p v-if="!warningData.deliveryId">提示运单ID为空无法查询设备</p>
<p v-else>提示该运单可能没有绑定设备或设备已被删除</p>
</div>
</div>
<div v-else class="no-data-tip">
<el-icon class="is-loading"><Loading /></el-icon>
<span style="margin-left: 10px;">正在加载设备列表...</span>
</div>
<!-- 设备温度日志重点显示温度数据 -->
@@ -284,13 +310,118 @@
</span>
</template>
</el-dialog>
<!-- 轨迹定位对话框 -->
<el-dialog
v-model="trackDialogVisible"
title="轨迹定位"
width="1000px"
:close-on-click-modal="false"
@close="handleTrackDialogClose"
>
<div v-loading="trackLoading" style="min-height: 500px;">
<!-- 状态提示 -->
<div v-if="deliveryStatus === 1" class="status-tip">
<el-alert
title="运单尚未开始运输"
description="当前运单状态为准备中,暂无轨迹数据"
type="info"
:closable="false"
show-icon
/>
</div>
<!-- 轨迹地图容器 -->
<div v-else>
<!-- 控制按钮 -->
<div class="track-controls" style="margin-bottom: 15px;">
<el-button
type="primary"
:icon="VideoPlay"
@click="handlePlayTrack"
:disabled="!trackMapShow || trackPath.length === 0"
>
{{ isPlaying ? '暂停' : '播放' }}
</el-button>
<el-button
:icon="Refresh"
@click="handleResetTrack"
:disabled="!trackMapShow || trackPath.length === 0"
>
重置
</el-button>
<el-tag type="info" style="margin-left: 10px;">
轨迹点数{{ trackPath.length }}
</el-tag>
<el-tag type="info" style="margin-left: 10px;">
状态{{ getDeliveryStatusText(deliveryStatus) }}
</el-tag>
</div>
<!-- 地图容器 -->
<div
id="trackMap"
style="width: 100%; height: 500px; border: 1px solid #dcdfe6; border-radius: 4px;"
></div>
<!-- 无轨迹数据提示 -->
<div v-if="!trackMapShow && !trackLoading" class="no-track-tip">
<el-empty description="暂无轨迹数据" :image-size="100" />
</div>
<div v-if="yingyanMeta.entityName" class="track-meta-panel">
<el-descriptions :column="3" border>
<el-descriptions-item label="终端名称">{{ yingyanMeta.entityName }}</el-descriptions-item>
<el-descriptions-item label="查询开始">{{ formatTimestamp(yingyanMeta.startTime) }}</el-descriptions-item>
<el-descriptions-item label="查询结束">{{ formatTimestamp(yingyanMeta.endTime) }}</el-descriptions-item>
</el-descriptions>
</div>
<div v-if="stayPoints.length > 0" class="staypoint-section">
<h4>停留点分析15分钟</h4>
<el-table :data="stayPoints" border style="width: 100%;" size="small">
<el-table-column label="开始时间" min-width="160">
<template #default="scope">
{{ formatTimestamp(scope.row.startTime) }}
</template>
</el-table-column>
<el-table-column label="结束时间" min-width="160">
<template #default="scope">
{{ formatTimestamp(scope.row.endTime) }}
</template>
</el-table-column>
<el-table-column label="停留时长" width="120">
<template #default="scope">
{{ formatDuration(scope.row.duration) }}
</template>
</el-table-column>
<el-table-column label="位置" min-width="160">
<template #default="scope">
{{ `${scope.row.latitude || '--'}, ${scope.row.longitude || '--'}` }}
</template>
</el-table-column>
</el-table>
</div>
<div v-else-if="trackMapShow && !trackLoading" class="no-data-tip">
暂无满足 15 分钟的停留点数据
</div>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="trackDialogVisible = false">关闭</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, computed, nextTick } from 'vue';
import { ref, reactive, computed, nextTick, onUnmounted } from 'vue';
import { ElMessage } from 'element-plus';
import { Location, VideoPlay, Refresh, InfoFilled, Connection, DataLine, Loading } from '@element-plus/icons-vue';
import { BMPGL } from '@/utils/loadBmap.js';
import { pageDeviceList, getCollarLogs, getEarTagLogs, getHostLogs } from '@/api/abroad.js';
import { pageDeviceList, getCollarLogs, getEarTagLogs, getHostLogs, getYingyanTrack, waybillDetail } from '@/api/abroad.js';
const dialogVisible = ref(false);
const warningData = reactive({
@@ -321,6 +452,28 @@ const loadingLogs = ref(false); // 新增:加载日志状态
let mapInstance = null;
let markerInstance = null;
// 轨迹定位相关
const trackDialogVisible = ref(false);
const trackLoading = ref(false);
const trackMapShow = ref(false);
const trackPath = ref([]); // 轨迹点数组
const trackMapInstance = ref(null); // 轨迹地图实例
const trackPolyline = ref(null); // 轨迹线条
const trackStartMarker = ref(null); // 起点标记
const trackEndMarker = ref(null); // 终点标记
const trackPlayMarker = ref(null); // 播放位置标记
const deliveryStatus = ref(null); // 运输状态1-准备中2-运输中3-已结束
const isPlaying = ref(false); // 是否正在播放
const playTimer = ref(null); // 播放定时器
const currentPlayIndex = ref(0); // 当前播放到的轨迹点索引
const trackBMapGL = ref(null); // 保存 BMapGL 实例,避免重复加载
const stayPoints = ref([]); // 停留点列表
const yingyanMeta = reactive({
entityName: '',
startTime: null,
endTime: null
});
// 计算属性:判断预警类型
const isTemperatureWarning = computed(() => {
// 5-高温预警6-低温预警
@@ -344,6 +497,7 @@ const dialogTitle = computed(() => {
// 打开对话框
const open = async (row) => {
console.log('[WARNING-DETAIL] 打开预警详情对话框,原始数据:', row);
// 填充数据
Object.keys(warningData).forEach(key => {
@@ -352,12 +506,18 @@ const open = async (row) => {
}
});
console.log('[WARNING-DETAIL] 填充后的 warningData:', warningData);
console.log('[WARNING-DETAIL] deliveryId:', warningData.deliveryId);
dialogVisible.value = true;
// ✅ 查询运单绑定的设备列表
if (warningData.deliveryId) {
console.log('[WARNING-DETAIL] 开始加载设备列表deliveryId:', warningData.deliveryId);
await loadDeviceList(warningData.deliveryId);
} else {
console.warn('[WARNING-DETAIL] 警告deliveryId 为空,无法加载设备列表');
console.warn('[WARNING-DETAIL] 请检查预警详情 API 是否返回了 deliveryId 字段');
}
// 如果是位置相关预警,加载地图
@@ -379,6 +539,11 @@ const loadDeviceList = async (deliveryId) => {
loadingDevices.value = true;
try {
console.log('[WARNING-DETAIL] 调用 pageDeviceList API参数:', {
deliveryId: deliveryId,
pageNum: 1,
pageSize: 100
});
const res = await pageDeviceList({
deliveryId: deliveryId,
@@ -386,22 +551,41 @@ const loadDeviceList = async (deliveryId) => {
pageSize: 100, // 一次性加载所有设备
});
console.log('[WARNING-DETAIL] pageDeviceList API 返回结果:', res);
if (res.code === 200 && res.data) {
// ✅ 修复:后端直接返回数组,不是嵌套在 list 或 rows 中
let devices = [];
if (Array.isArray(res.data)) {
deviceList.value = res.data;
devices = res.data;
} else {
deviceList.value = res.data.list || res.data.rows || [];
devices = res.data.list || res.data.rows || [];
}
console.log('[WARNING-DETAIL] 解析后的设备列表:', devices);
console.log('[WARNING-DETAIL] 设备数量:', devices.length);
deviceList.value = devices;
// 自动加载所有设备的日志
await loadAllDeviceLogs();
if (devices.length === 0) {
console.warn('[WARNING-DETAIL] 警告:设备列表为空');
console.warn('[WARNING-DETAIL] 可能原因:');
console.warn('[WARNING-DETAIL] 1. 该运单确实没有绑定设备');
console.warn('[WARNING-DETAIL] 2. 设备被标记为已删除is_delet=1');
console.warn('[WARNING-DETAIL] 3. 设备表中的 delivery_id 字段为空或不匹配');
ElMessage.warning('该运单暂无绑定设备');
} else {
console.log('[WARNING-DETAIL] 成功加载设备列表,设备数量:', devices.length);
// 自动加载所有设备的日志
await loadAllDeviceLogs();
}
} else {
console.error('[WARNING-DETAIL] API 返回错误:', res);
ElMessage.warning('加载设备列表失败:' + (res.msg || '未知错误'));
}
} catch (error) {
console.error('[WARNING-DETAIL] 加载设备列表失败:', error);
ElMessage.error('加载设备列表失败');
console.error('[WARNING-DETAIL] 加载设备列表异常:', error);
ElMessage.error('加载设备列表失败' + (error.message || '网络错误'));
} finally {
loadingDevices.value = false;
}
@@ -586,6 +770,52 @@ const getWarningTagType = (type) => {
}
};
const normalizeTimestamp = (value) => {
if (value === null || value === undefined || value === '') {
return null;
}
const num = Number(value);
if (Number.isNaN(num)) {
return null;
}
return num < 1e12 ? num * 1000 : num;
};
const formatTimestamp = (value) => {
const ms = normalizeTimestamp(value);
if (!ms) return '--';
const date = new Date(ms);
if (Number.isNaN(date.getTime())) {
return '--';
}
const pad = (num) => `${num}`.padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
};
const formatDuration = (seconds) => {
if (seconds === null || seconds === undefined) {
return '--';
}
const total = Number(seconds);
if (Number.isNaN(total) || total <= 0) {
return '--';
}
const hours = Math.floor(total / 3600);
const minutes = Math.floor((total % 3600) / 60);
const sec = Math.floor(total % 60);
const parts = [];
if (hours > 0) {
parts.push(`${hours}小时`);
}
if (minutes > 0) {
parts.push(`${minutes}分钟`);
}
if (sec > 0 && hours === 0) {
parts.push(`${sec}`);
}
return parts.join('') || `${sec}`;
};
// 关闭对话框
const handleClose = () => {
// 清理地图实例
@@ -614,6 +844,436 @@ const handleClose = () => {
});
};
// 轨迹定位按钮点击
const handleTrackClick = async () => {
if (!warningData.deliveryId) {
ElMessage.warning('运单ID不存在无法查看轨迹');
return;
}
trackDialogVisible.value = true;
trackLoading.value = true;
trackMapShow.value = false;
trackPath.value = [];
isPlaying.value = false;
currentPlayIndex.value = 0;
// 获取运单运输状态
await getDeliveryStatus();
// 如果状态为准备中,直接返回
if (deliveryStatus.value === 1) {
trackLoading.value = false;
return;
}
await loadYingyanTrack();
if (trackPath.value.length === 0) {
trackLoading.value = false;
return;
}
// 初始化地图
await nextTick();
await initTrackMap();
};
// 获取运送清单运输状态
const getDeliveryStatus = async () => {
// 先检查 warningData 中是否包含 status
if (warningData.status !== undefined && warningData.status !== null) {
// 注意:这里的 status 可能是业务状态1-7需要判断是否是运输状态1-3
// 如果 status 在 1-3 范围内,可能是运输状态
const status = parseInt(warningData.status);
if (status >= 1 && status <= 3) {
deliveryStatus.value = status;
return;
}
}
// 如果没有,通过 API 获取运单详情
try {
const res = await waybillDetail(warningData.deliveryId);
if (res.code === 200 && res.data) {
const delivery = res.data.delivery || res.data;
// 注意Delivery 实体中的 status 是业务状态1-7不是运输状态
// 需要查看是否有专门的运输状态字段,或者根据业务状态推断
// 这里先假设 status 字段就是运输状态,如果不对需要调整
deliveryStatus.value = delivery.status || null;
}
} catch (error) {
console.error('[TRACK] 获取运单状态失败:', error);
// 默认设置为运输中,允许查看轨迹
deliveryStatus.value = 2;
}
};
// 加载百度鹰眼轨迹与停留点
const loadYingyanTrack = async () => {
stayPoints.value = [];
trackPath.value = [];
trackMapShow.value = false;
yingyanMeta.entityName = '';
yingyanMeta.startTime = null;
yingyanMeta.endTime = null;
if (!warningData.deliveryId) {
ElMessage.warning('运单ID缺失无法查询轨迹');
return;
}
try {
const res = await getYingyanTrack({ deliveryId: warningData.deliveryId });
if (res.code === 200 && res.data) {
const rawPoints = Array.isArray(res.data.trackPoints) ? res.data.trackPoints : [];
trackPath.value = rawPoints
.map(item => {
const lng = parseFloat(item.longitude ?? item.lng ?? 0);
const lat = parseFloat(item.latitude ?? item.lat ?? 0);
if (Number.isNaN(lng) || Number.isNaN(lat) || lng === 0 || lat === 0) {
return null;
}
return {
lng,
lat,
locTime: item.locTime
};
})
.filter(Boolean);
stayPoints.value = Array.isArray(res.data.stayPoints) ? res.data.stayPoints : [];
yingyanMeta.entityName = res.data.entityName || '';
yingyanMeta.startTime = res.data.startTime || null;
yingyanMeta.endTime = res.data.endTime || null;
if (trackPath.value.length > 0) {
trackMapShow.value = true;
} else {
ElMessage.warning('暂无有效轨迹点');
}
} else {
ElMessage.warning(res.msg || '暂无轨迹数据');
}
} catch (error) {
console.error('[TRACK] 加载百度鹰眼轨迹失败:', error);
ElMessage.error('加载轨迹数据失败');
}
};
// 初始化轨迹地图
const initTrackMap = async () => {
if (trackPath.value.length === 0) {
return;
}
try {
const BMapGL = await BMPGL('fLz8UwJSM3ayYl6dtsWYp7TQ8993R6kC');
trackBMapGL.value = BMapGL; // 保存 BMapGL 实例
// 创建地图实例
trackMapInstance.value = new BMapGL.Map('trackMap');
// 计算地图中心点和缩放级别
const bounds = calculateBounds(trackPath.value);
const centerPoint = new BMapGL.Point(bounds.center.lng, bounds.center.lat);
trackMapInstance.value.centerAndZoom(centerPoint, bounds.zoom);
trackMapInstance.value.enableScrollWheelZoom(true);
// 根据状态绘制轨迹
if (deliveryStatus.value === 3) {
// 已结束:静态轨迹
drawStaticTrack(BMapGL);
} else if (deliveryStatus.value === 2) {
// 运输中:动态轨迹(先绘制已有部分)
drawDynamicTrack(BMapGL);
}
trackLoading.value = false;
} catch (error) {
console.error('[TRACK] 地图初始化失败:', error);
ElMessage.error('地图加载失败');
trackLoading.value = false;
}
};
// 计算轨迹边界
const calculateBounds = (points) => {
if (points.length === 0) {
return { center: { lng: 116.404, lat: 39.915 }, zoom: 15 };
}
let minLng = points[0].lng;
let maxLng = points[0].lng;
let minLat = points[0].lat;
let maxLat = points[0].lat;
points.forEach(point => {
minLng = Math.min(minLng, point.lng);
maxLng = Math.max(maxLng, point.lng);
minLat = Math.min(minLat, point.lat);
maxLat = Math.max(maxLat, point.lat);
});
const center = {
lng: (minLng + maxLng) / 2,
lat: (minLat + maxLat) / 2
};
// 计算缩放级别(简单估算)
const lngDiff = maxLng - minLng;
const latDiff = maxLat - minLat;
const maxDiff = Math.max(lngDiff, latDiff);
let zoom = 15;
if (maxDiff > 0.1) zoom = 10;
else if (maxDiff > 0.05) zoom = 11;
else if (maxDiff > 0.02) zoom = 12;
else if (maxDiff > 0.01) zoom = 13;
else if (maxDiff > 0.005) zoom = 14;
return { center, zoom };
};
// 绘制静态轨迹(已结束)
const drawStaticTrack = (BMapGL) => {
if (!trackMapInstance.value || trackPath.value.length === 0) {
return;
}
// 清除之前的覆盖物
trackMapInstance.value.clearOverlays();
// 绘制轨迹线
const polyline = new BMapGL.Polyline(
trackPath.value.map(p => new BMapGL.Point(p.lng, p.lat)),
{
strokeColor: '#3388ff',
strokeWeight: 4,
strokeOpacity: 0.8
}
);
trackMapInstance.value.addOverlay(polyline);
trackPolyline.value = polyline;
// 添加起点标记
if (trackPath.value.length > 0) {
const startPoint = new BMapGL.Point(trackPath.value[0].lng, trackPath.value[0].lat);
// 使用简单的圆形标记作为起点(绿色)
const startMarker = new BMapGL.Marker(startPoint, {
icon: new BMapGL.Icon(
'http://api.map.baidu.com/images/marker_red.png',
new BMapGL.Size(32, 32),
{ anchor: new BMapGL.Size(16, 32) }
)
});
trackMapInstance.value.addOverlay(startMarker);
trackStartMarker.value = startMarker;
// 起点信息窗口
const startInfoWindow = new BMapGL.InfoWindow(
'<div style="padding: 5px;"><strong style="color: #67c23a;">起点</strong></div>',
{ width: 80, height: 30 }
);
startMarker.addEventListener('click', () => {
trackMapInstance.value.openInfoWindow(startInfoWindow, startPoint);
});
}
// 添加终点标记
if (trackPath.value.length > 1) {
const endPoint = new BMapGL.Point(
trackPath.value[trackPath.value.length - 1].lng,
trackPath.value[trackPath.value.length - 1].lat
);
// 使用红色标记作为终点
const endMarker = new BMapGL.Marker(endPoint, {
icon: new BMapGL.Icon(
'http://api.map.baidu.com/images/marker_red.png',
new BMapGL.Size(32, 32),
{ anchor: new BMapGL.Size(16, 32) }
)
});
trackMapInstance.value.addOverlay(endMarker);
trackEndMarker.value = endMarker;
// 终点信息窗口
const endInfoWindow = new BMapGL.InfoWindow(
'<div style="padding: 5px;"><strong style="color: #f56c6c;">终点</strong></div>',
{ width: 80, height: 30 }
);
endMarker.addEventListener('click', () => {
trackMapInstance.value.openInfoWindow(endInfoWindow, endPoint);
});
}
};
// 绘制动态轨迹(运输中)
const drawDynamicTrack = (BMapGL) => {
if (!trackMapInstance.value || trackPath.value.length === 0) {
return;
}
// 清除之前的覆盖物
trackMapInstance.value.clearOverlays();
// 绘制已有轨迹线
const polyline = new BMapGL.Polyline(
trackPath.value.map(p => new BMapGL.Point(p.lng, p.lat)),
{
strokeColor: '#3388ff',
strokeWeight: 4,
strokeOpacity: 0.6
}
);
trackMapInstance.value.addOverlay(polyline);
trackPolyline.value = polyline;
// 添加起点标记
if (trackPath.value.length > 0) {
const startPoint = new BMapGL.Point(trackPath.value[0].lng, trackPath.value[0].lat);
const startMarker = new BMapGL.Marker(startPoint, {
icon: new BMapGL.Icon(
'http://api.map.baidu.com/images/marker_red.png',
new BMapGL.Size(32, 32),
{ anchor: new BMapGL.Size(16, 32) }
)
});
trackMapInstance.value.addOverlay(startMarker);
trackStartMarker.value = startMarker;
}
// 添加当前位置标记(用于动画)
const currentPoint = new BMapGL.Point(trackPath.value[0].lng, trackPath.value[0].lat);
const playMarker = new BMapGL.Marker(currentPoint, {
icon: new BMapGL.Icon(
'http://api.map.baidu.com/images/marker_red.png',
new BMapGL.Size(20, 20),
{ anchor: new BMapGL.Size(10, 20) }
)
});
trackMapInstance.value.addOverlay(playMarker);
trackPlayMarker.value = playMarker;
currentPlayIndex.value = 0;
};
// 播放轨迹动画
const handlePlayTrack = () => {
if (trackPath.value.length === 0 || !trackMapInstance.value) {
return;
}
if (isPlaying.value) {
// 暂停
if (playTimer.value) {
clearInterval(playTimer.value);
playTimer.value = null;
}
isPlaying.value = false;
} else {
// 播放
if (currentPlayIndex.value >= trackPath.value.length - 1) {
// 如果已经播放完,重新开始
currentPlayIndex.value = 0;
}
isPlaying.value = true;
playTimer.value = setInterval(() => {
if (currentPlayIndex.value < trackPath.value.length - 1) {
currentPlayIndex.value++;
updatePlayMarker();
} else {
// 播放完成
if (playTimer.value) {
clearInterval(playTimer.value);
playTimer.value = null;
}
isPlaying.value = false;
}
}, 200); // 每200ms移动一次
}
};
// 更新播放位置标记
const updatePlayMarker = () => {
if (!trackMapInstance.value || !trackPlayMarker.value || !trackBMapGL.value || currentPlayIndex.value >= trackPath.value.length) {
return;
}
const point = trackPath.value[currentPlayIndex.value];
const bdPoint = new trackBMapGL.value.Point(point.lng, point.lat);
trackPlayMarker.value.setPosition(bdPoint);
// 地图跟随
trackMapInstance.value.panTo(bdPoint);
};
// 重置轨迹动画
const handleResetTrack = () => {
if (playTimer.value) {
clearInterval(playTimer.value);
playTimer.value = null;
}
isPlaying.value = false;
currentPlayIndex.value = 0;
if (trackPlayMarker.value && trackPath.value.length > 0 && trackMapInstance.value && trackBMapGL.value) {
const point = trackPath.value[0];
const bdPoint = new trackBMapGL.value.Point(point.lng, point.lat);
trackPlayMarker.value.setPosition(bdPoint);
trackMapInstance.value.panTo(bdPoint);
}
};
// 获取运输状态文本
const getDeliveryStatusText = (status) => {
const statusMap = {
1: '准备中',
2: '运输中',
3: '已结束'
};
return statusMap[status] || '未知';
};
// 关闭轨迹对话框
const handleTrackDialogClose = () => {
// 清理定时器
if (playTimer.value) {
clearInterval(playTimer.value);
playTimer.value = null;
}
// 清理地图实例
if (trackMapInstance.value) {
trackMapInstance.value.clearOverlays();
trackMapInstance.value = null;
}
trackPolyline.value = null;
trackStartMarker.value = null;
trackEndMarker.value = null;
trackPlayMarker.value = null;
trackBMapGL.value = null;
isPlaying.value = false;
currentPlayIndex.value = 0;
trackPath.value = [];
trackMapShow.value = false;
stayPoints.value = [];
yingyanMeta.entityName = '';
yingyanMeta.startTime = null;
yingyanMeta.endTime = null;
};
// 组件卸载时清理
onUnmounted(() => {
if (playTimer.value) {
clearInterval(playTimer.value);
}
if (trackMapInstance.value) {
trackMapInstance.value.clearOverlays();
}
});
// 导出方法
defineExpose({
open
@@ -676,6 +1336,21 @@ defineExpose({
}
}
.track-meta-panel {
margin-top: 20px;
}
.staypoint-section {
margin-top: 20px;
h4 {
margin: 0 0 10px 0;
font-size: 15px;
color: #303133;
font-weight: 600;
}
}
.map-container {
margin-top: 20px;
@@ -721,5 +1396,24 @@ defineExpose({
font-size: 12px;
}
}
// 轨迹对话框样式
.status-tip {
margin-bottom: 20px;
}
.track-controls {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.no-track-tip {
display: flex;
align-items: center;
justify-content: center;
height: 500px;
}
</style>

View File

@@ -111,7 +111,13 @@
<div class="dataListOnEmpty">暂无数据</div>
</template>
</el-table>
<pagination v-model:limit="form.pageSize" v-model:page="form.pageNum" :total="data.total" @pagination="getDataList" />
<Pagination
v-if="data.total > 0"
v-model:limit="form.pageSize"
v-model:page="form.pageNum"
:total="data.total"
@pagination="handlePagination"
/>
</div>
<!-- 编辑对话框 -->
@@ -124,6 +130,7 @@ import { ref, reactive, onMounted, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus';
import baseSearch from '@/components/common/searchCustom/index.vue';
import Pagination from '@/components/Pagination/index.vue';
import createDeliveryDialog from '@/views/shipping/createDeliveryDialog.vue';
import { inspectionList, downloadZip, pageDeviceList } from '@/api/abroad.js';
import { updateDeliveryStatus, deleteDeliveryLogic, downloadDeliveryPackage, getDeliveryDetail, downloadAcceptanceForm } from '@/api/shipping.js';
@@ -136,11 +143,11 @@ const dataListLoading = ref(false);
const downLoading = reactive({});
const form = reactive({
pageNum: 1,
pageSize: 20,
pageSize: 10,
});
const data = reactive({
rows: [],
total: 20,
total: 10,
deviceCounts: {}, // 存储每个订单的设备数量 {orderId: {host: 0, ear: 0, collar: 0, total: 0}}
});
const formItemList = reactive([
@@ -235,6 +242,18 @@ const searchFrom = () => {
form.pageNum = 1;
getDataList();
};
// 处理分页事件
const handlePagination = (paginationData) => {
console.log('[PAGINATION] 分页变化:', paginationData);
if (paginationData) {
form.pageNum = paginationData.page || form.pageNum;
form.pageSize = paginationData.limit || form.pageSize;
console.log('[PAGINATION] 更新后的分页参数 - pageNum:', form.pageNum, 'pageSize:', form.pageSize);
}
getDataList();
};
const getDataList = () => {
dataListLoading.value = true;
@@ -263,34 +282,46 @@ const getDataList = () => {
}
console.log('[INSPECTION-LIST] 请求参数:', params);
inspectionList(params)
.then(async (ret) => {
data.rows = ret.data.rows;
data.total = ret.data.total;
console.log('[INSPECTION-LIST] 响应数据:', ret);
// 处理响应数据结构
// PageResultResponse 返回的结构是 { code, msg, data: { total, rows } }
let responseData = ret.data || ret;
// 如果 data 是对象且包含 total 和 rows直接使用
if (responseData && typeof responseData === 'object' && 'total' in responseData && 'rows' in responseData) {
data.rows = responseData.rows || [];
data.total = responseData.total || 0;
} else if (responseData && responseData.data) {
// 如果 data 是嵌套的,使用 data.data
data.rows = responseData.data.rows || [];
data.total = responseData.data.total || 0;
} else {
// 兜底处理
data.rows = [];
data.total = 0;
}
console.log('[INSPECTION-LIST] 解析后的数据 - 总数:', data.total, '当前页数据:', data.rows.length);
dataListLoading.value = false;
// 为每个订单获取设备数量
if (ret.data.rows && ret.data.rows.length > 0) {
for (const row of ret.data.rows) {
if (data.rows && data.rows.length > 0) {
for (const row of data.rows) {
if (row.id) {
await getDeviceCounts(row.id);
}
}
}
// 调试:检查第一行数据的字段
if (ret.data.rows && ret.data.rows.length > 0) {
const firstRow = ret.data.rows[0];
// 检查Word导出所需字段
}
})
.catch(() => {
.catch((error) => {
console.error('[INSPECTION-LIST] 请求失败:', error);
dataListLoading.value = false;
data.rows = [];
data.total = 0;
});
};
// 详情

View File

@@ -80,183 +80,187 @@
</div>
<div class="info-box">
<div class="title">装车信息</div>
<el-descriptions :column="4">
<!-- 重量信息 -->
<el-descriptions-item label="空车过磅重量:">{{
data.baseInfo.emptyWeight ? data.baseInfo.emptyWeight + 'kg' : ''
}}</el-descriptions-item>
<el-descriptions-item label="装车过磅重量:">{{
data.baseInfo.entruckWeight ? data.baseInfo.entruckWeight + 'kg' : ''
}}</el-descriptions-item>
<el-descriptions-item label="落地过磅重量:">{{
data.baseInfo.landingEntruckWeight ? data.baseInfo.landingEntruckWeight + 'kg' : ''
}}</el-descriptions-item>
<!-- 照片上传区域 -->
<el-descriptions-item label="检疫票:">
<span style="vertical-align: top">
<el-image
v-if="data.baseInfo.quarantineTickeyUrl"
style="width: 50px; height: 50px; margin-right: 10px"
:src="data.baseInfo.quarantineTickeyUrl ? data.baseInfo.quarantineTickeyUrl : ''"
fit="cover"
:preview-src-list="[data.baseInfo.quarantineTickeyUrl] ? [data.baseInfo.quarantineTickeyUrl] : []"
preview-teleported
/>
</span>
</el-descriptions-item>
<el-descriptions-item label="纸质磅单:">
<span style="vertical-align: top">
<el-image
v-if="data.baseInfo.poundListImg"
style="width: 50px; height: 50px; margin-right: 10px"
:src="data.baseInfo.poundListImg ? data.baseInfo.poundListImg : ''"
fit="cover"
:preview-src-list="[data.baseInfo.poundListImg] ? [data.baseInfo.poundListImg] : []"
preview-teleported
/>
</span>
</el-descriptions-item>
<el-descriptions-item label="车辆空磅上磅车头照片:">
<span style="vertical-align: top">
<el-image
v-if="data.baseInfo.emptyVehicleFrontPhoto"
style="width: 50px; height: 50px; margin-right: 10px"
:src="data.baseInfo.emptyVehicleFrontPhoto ? data.baseInfo.emptyVehicleFrontPhoto : ''"
fit="cover"
:preview-src-list="[data.baseInfo.emptyVehicleFrontPhoto] ? [data.baseInfo.emptyVehicleFrontPhoto] : []"
preview-teleported
/>
</span>
</el-descriptions-item>
<el-descriptions-item label="车辆过重磅车头照片:">
<span style="vertical-align: top">
<el-image
v-if="data.baseInfo.loadedVehicleFrontPhoto"
style="width: 50px; height: 50px; margin-right: 10px"
:src="data.baseInfo.loadedVehicleFrontPhoto ? data.baseInfo.loadedVehicleFrontPhoto : ''"
fit="cover"
:preview-src-list="[data.baseInfo.loadedVehicleFrontPhoto] ? [data.baseInfo.loadedVehicleFrontPhoto] : []"
preview-teleported
/>
</span>
</el-descriptions-item>
<el-descriptions-item label="车辆重磅照片:">
<span style="vertical-align: top">
<el-image
v-if="data.baseInfo.loadedVehicleWeightPhoto"
style="width: 50px; height: 50px; margin-right: 10px"
:src="data.baseInfo.loadedVehicleWeightPhoto ? data.baseInfo.loadedVehicleWeightPhoto : ''"
fit="cover"
:preview-src-list="[data.baseInfo.loadedVehicleWeightPhoto] ? [data.baseInfo.loadedVehicleWeightPhoto] : []"
preview-teleported
/>
</span>
</el-descriptions-item>
<el-descriptions-item label="驾驶员手持身份证站车头照片:">
<span style="vertical-align: top">
<el-image
v-if="data.baseInfo.driverIdCardPhoto"
style="width: 50px; height: 50px; margin-right: 10px"
:src="data.baseInfo.driverIdCardPhoto ? data.baseInfo.driverIdCardPhoto : ''"
fit="cover"
:preview-src-list="[data.baseInfo.driverIdCardPhoto] ? [data.baseInfo.driverIdCardPhoto] : []"
preview-teleported
/>
</span>
</el-descriptions-item>
<!-- 到地相关照片 -->
<el-descriptions-item label="到地纸质磅单:">
<span style="vertical-align: top">
<el-image
v-if="data.baseInfo.destinationPoundListImg"
style="width: 50px; height: 50px; margin-right: 10px"
:src="data.baseInfo.destinationPoundListImg ? data.baseInfo.destinationPoundListImg : ''"
fit="cover"
:preview-src-list="[data.baseInfo.destinationPoundListImg] ? [data.baseInfo.destinationPoundListImg] : []"
preview-teleported
/>
</span>
</el-descriptions-item>
<el-descriptions-item label="到地车辆过重磅车头照片:">
<span style="vertical-align: top">
<el-image
v-if="data.baseInfo.destinationVehicleFrontPhoto"
style="width: 50px; height: 50px; margin-right: 10px"
:src="data.baseInfo.destinationVehicleFrontPhoto ? data.baseInfo.destinationVehicleFrontPhoto : ''"
fit="cover"
:preview-src-list="[data.baseInfo.destinationVehicleFrontPhoto] ? [data.baseInfo.destinationVehicleFrontPhoto] : []"
preview-teleported
/>
</span>
</el-descriptions-item>
<!-- 视频上传区域 -->
<el-descriptions-item label="空车过磅视频(含车牌、地磅数):">
<span style="vertical-align: top" v-if="data.baseInfo.emptyWeightVideo">
<video
style="height: 250px; width: auto; border-radius: 5px; margin-left: 60px"
controls
:src="data.baseInfo.emptyWeightVideo"
/>
</span>
</el-descriptions-item>
<el-descriptions-item label="装车过磅视频:">
<span style="vertical-align: top" v-if="data.baseInfo.entruckWeightVideo">
<video
style="height: 250px; width: auto; border-radius: 5px; margin-left: 60px"
controls
:src="data.baseInfo.entruckWeightVideo"
/>
</span>
</el-descriptions-item>
<el-descriptions-item label="装车视频:">
<span style="vertical-align: top" v-if="data.baseInfo.entruckVideo">
<video
style="height: 250px; width: auto; border-radius: 5px; margin-left: 60px"
controls
:src="data.baseInfo.entruckVideo"
/>
</span>
</el-descriptions-item>
<el-descriptions-item label="控槽视频:">
<span style="vertical-align: top" v-if="data.baseInfo.controlSlotVideo">
<video
style="height: 250px; width: auto; border-radius: 5px; margin-left: 60px"
controls
:src="data.baseInfo.controlSlotVideo"
/>
</span>
</el-descriptions-item>
<el-descriptions-item label="装完牛绕车一圈视频:">
<span style="vertical-align: top" v-if="data.baseInfo.cattleLoadingCircleVideo">
<video
style="height: 250px; width: auto; border-radius: 5px; margin-left: 60px"
controls
:src="data.baseInfo.cattleLoadingCircleVideo"
/>
</span>
</el-descriptions-item>
<el-descriptions-item label="卸牛视频:">
<span style="vertical-align: top" v-if="data.baseInfo.unloadCattleVideo">
<video
style="height: 250px; width: auto; border-radius: 5px; margin-left: 60px"
controls
:src="data.baseInfo.unloadCattleVideo"
/>
</span>
</el-descriptions-item>
<el-descriptions-item label="到地过磅视频:">
<span style="vertical-align: top" v-if="data.baseInfo.destinationWeightVideo">
<video
style="height: 250px; width: auto; border-radius: 5px; margin-left: 60px"
controls
:src="data.baseInfo.destinationWeightVideo"
/>
</span>
</el-descriptions-item>
</el-descriptions>
<!-- 重量信息分组 -->
<div v-if="hasValue(data.baseInfo.emptyWeight) || hasValue(data.baseInfo.entruckWeight) || hasValue(data.baseInfo.landingEntruckWeight)" class="info-group">
<div class="group-title">重量信息</div>
<el-descriptions :column="3" border>
<el-descriptions-item v-if="hasValue(data.baseInfo.emptyWeight)" label="空车过磅重量:">
{{ data.baseInfo.emptyWeight }}kg
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.entruckWeight)" label="装车过磅重量:">
{{ data.baseInfo.entruckWeight }}kg
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.landingEntruckWeight)" label="落地过磅重量:">
{{ data.baseInfo.landingEntruckWeight }}kg
</el-descriptions-item>
</el-descriptions>
</div>
<!-- 照片信息分组 -->
<div v-if="hasValue(data.baseInfo.quarantineTickeyUrl) || hasValue(data.baseInfo.poundListImg) || hasValue(data.baseInfo.emptyVehicleFrontPhoto) || hasValue(data.baseInfo.loadedVehicleFrontPhoto) || hasValue(data.baseInfo.loadedVehicleWeightPhoto) || hasValue(data.baseInfo.driverIdCardPhoto) || hasValue(data.baseInfo.destinationPoundListImg) || hasValue(data.baseInfo.destinationVehicleFrontPhoto)" class="info-group">
<div class="group-title">照片信息</div>
<el-descriptions :column="3" border>
<el-descriptions-item v-if="hasValue(data.baseInfo.quarantineTickeyUrl)" label="检疫票:">
<div class="photo-container">
<el-image
style="width: 100px; height: 100px; border-radius: 4px; cursor: pointer"
:src="data.baseInfo.quarantineTickeyUrl"
fit="cover"
:preview-src-list="[data.baseInfo.quarantineTickeyUrl]"
preview-teleported
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.poundListImg)" label="纸质磅单:">
<div class="photo-container">
<el-image
style="width: 100px; height: 100px; border-radius: 4px; cursor: pointer"
:src="data.baseInfo.poundListImg"
fit="cover"
:preview-src-list="[data.baseInfo.poundListImg]"
preview-teleported
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.emptyVehicleFrontPhoto)" label="车辆空磅上磅车头照片:">
<div class="photo-container">
<el-image
style="width: 100px; height: 100px; border-radius: 4px; cursor: pointer"
:src="data.baseInfo.emptyVehicleFrontPhoto"
fit="cover"
:preview-src-list="[data.baseInfo.emptyVehicleFrontPhoto]"
preview-teleported
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.loadedVehicleFrontPhoto)" label="车辆过重磅车头照片:">
<div class="photo-container">
<el-image
style="width: 100px; height: 100px; border-radius: 4px; cursor: pointer"
:src="data.baseInfo.loadedVehicleFrontPhoto"
fit="cover"
:preview-src-list="[data.baseInfo.loadedVehicleFrontPhoto]"
preview-teleported
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.loadedVehicleWeightPhoto)" label="车辆重磅照片:">
<div class="photo-container">
<el-image
style="width: 100px; height: 100px; border-radius: 4px; cursor: pointer"
:src="data.baseInfo.loadedVehicleWeightPhoto"
fit="cover"
:preview-src-list="[data.baseInfo.loadedVehicleWeightPhoto]"
preview-teleported
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.driverIdCardPhoto)" label="驾驶员手持身份证站车头照片:">
<div class="photo-container">
<el-image
style="width: 100px; height: 100px; border-radius: 4px; cursor: pointer"
:src="data.baseInfo.driverIdCardPhoto"
fit="cover"
:preview-src-list="[data.baseInfo.driverIdCardPhoto]"
preview-teleported
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.destinationPoundListImg)" label="到地纸质磅单:">
<div class="photo-container">
<el-image
style="width: 100px; height: 100px; border-radius: 4px; cursor: pointer"
:src="data.baseInfo.destinationPoundListImg"
fit="cover"
:preview-src-list="[data.baseInfo.destinationPoundListImg]"
preview-teleported
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.destinationVehicleFrontPhoto)" label="到地车辆过重磅车头照片:">
<div class="photo-container">
<el-image
style="width: 100px; height: 100px; border-radius: 4px; cursor: pointer"
:src="data.baseInfo.destinationVehicleFrontPhoto"
fit="cover"
:preview-src-list="[data.baseInfo.destinationVehicleFrontPhoto]"
preview-teleported
/>
</div>
</el-descriptions-item>
</el-descriptions>
</div>
<!-- 视频信息分组 -->
<div v-if="hasValue(data.baseInfo.emptyWeightVideo) || hasValue(data.baseInfo.entruckWeightVideo) || hasValue(data.baseInfo.entruckVideo) || hasValue(data.baseInfo.controlSlotVideo) || hasValue(data.baseInfo.cattleLoadingCircleVideo) || hasValue(data.baseInfo.unloadCattleVideo) || hasValue(data.baseInfo.destinationWeightVideo)" class="info-group">
<div class="group-title">视频信息</div>
<el-descriptions :column="1" border>
<el-descriptions-item v-if="hasValue(data.baseInfo.emptyWeightVideo)" label="空车过磅视频(含车牌、地磅数):">
<div class="video-container">
<video
style="max-width: 100%; height: 200px; border-radius: 4px"
controls
:src="data.baseInfo.emptyWeightVideo"
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.entruckWeightVideo)" label="装车过磅视频:">
<div class="video-container">
<video
style="max-width: 100%; height: 200px; border-radius: 4px"
controls
:src="data.baseInfo.entruckWeightVideo"
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.entruckVideo)" label="装车视频:">
<div class="video-container">
<video
style="max-width: 100%; height: 200px; border-radius: 4px"
controls
:src="data.baseInfo.entruckVideo"
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.controlSlotVideo)" label="控槽视频:">
<div class="video-container">
<video
style="max-width: 100%; height: 200px; border-radius: 4px"
controls
:src="data.baseInfo.controlSlotVideo"
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.cattleLoadingCircleVideo)" label="装完牛绕车一圈视频:">
<div class="video-container">
<video
style="max-width: 100%; height: 200px; border-radius: 4px"
controls
:src="data.baseInfo.cattleLoadingCircleVideo"
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.unloadCattleVideo)" label="卸牛视频:">
<div class="video-container">
<video
style="max-width: 100%; height: 200px; border-radius: 4px"
controls
:src="data.baseInfo.unloadCattleVideo"
/>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="hasValue(data.baseInfo.destinationWeightVideo)" label="到地过磅视频:">
<div class="video-container">
<video
style="max-width: 100%; height: 200px; border-radius: 4px"
controls
:src="data.baseInfo.destinationWeightVideo"
/>
</div>
</el-descriptions-item>
</el-descriptions>
</div>
</div>
<div class="ear-box">
<div class="title">智能主机</div>
@@ -1158,6 +1162,17 @@ const getStatusType = (status) => {
return typeMap[status] || 'info';
};
// 判断字段是否有有效值(用于隐藏空值字段)
const hasValue = (value) => {
if (value === null || value === undefined) {
return false;
}
if (typeof value === 'string') {
return value.trim() !== '';
}
return true;
};
onMounted(() => {
data.id = route.query.id;
data.status = route.query.status;
@@ -1200,6 +1215,21 @@ onMounted(() => {
font-weight: bold;
margin-bottom: 10px;
}
.info-group {
margin-top: 20px;
margin-bottom: 20px;
&:first-child {
margin-top: 0;
}
}
.group-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-left: 8px;
border-left: 4px solid #409eff;
}
.quarantine-text {
margin-top: 20px;
}
@@ -1240,4 +1270,19 @@ onMounted(() => {
.red {
color: #ff6332;
}
.photo-container {
display: inline-block;
margin-right: 10px;
transition: transform 0.2s;
&:hover {
transform: scale(1.05);
}
}
.video-container {
margin-top: 8px;
margin-bottom: 8px;
video {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}
</style>

View File

@@ -90,13 +90,13 @@ const calculateBatteryPercentage = (voltage) => {
const getList = async () => {
const { pageSize, pageNum, sn, startNo, endNo } = form;
// 为了获取所有项圈设备使用更大的pageSize
const params = {
pageSize: 100, // 使用更大的页面大小确保能获取所有设备
pageNum: 1,
pageSize,
pageNum,
sn,
startNo,
endNo,
type: 4, // 项圈设备类型
};
try {
@@ -104,16 +104,9 @@ const getList = async () => {
const { data = {}, code } = res;
if (code === 200) {
// 后端已经过滤了organName为'牛只运输跟踪系统'的数据
// 前端根据设备类型过滤项圈数据type=4
const allData = data.rows || [];
const filteredData = allData.filter(item => item.type === 4);
form.tableData = filteredData;
form.total = filteredData.length;
// 重新计算分页,因为我们现在显示的是过滤后的数据
form.pageNum = 1; // 重置到第一页
// 使用后端返回的分页数据
form.tableData = data.rows || [];
form.total = data.total || 0;
} else {
console.error('API调用失败:', res.msg);
form.tableData = [];

View File

@@ -84,6 +84,7 @@ const getList = async () => {
deviceId,
startNo,
endNo,
type: 2, // 耳标设备类型
};
try {
@@ -91,13 +92,9 @@ const getList = async () => {
const { data = {}, code } = res;
if (code === 200) {
// 后端已经过滤了organName为'牛只运输跟踪系统'的数据
// 前端根据设备类型过滤耳标数据type=2
const allData = data.rows || [];
const filteredData = allData.filter(item => item.type === 2);
form.tableData = filteredData;
form.total = filteredData.length;
// 使用后端返回的分页数据
form.tableData = data.rows || [];
form.total = data.total || 0;
} else {
console.error('API调用失败:', res.msg);
form.tableData = [];

View File

@@ -75,6 +75,7 @@ const getList = async () => {
pageSize,
pageNum,
deviceId,
type: 1, // 主机设备类型
};
try {
@@ -82,13 +83,9 @@ const getList = async () => {
const { data = {}, code } = res;
if (code === 200) {
// 后端已经过滤了organName为'牛只运输跟踪系统'的数据
// 前端根据设备类型过滤主机数据type=1
const allData = data.rows || [];
const filteredData = allData.filter(item => item.type === 1);
form.tableData = filteredData;
form.total = filteredData.length;
// 使用后端返回的分页数据
form.tableData = data.rows || [];
form.total = data.total || 0;
} else {
console.error('API调用失败:', res.msg);
form.tableData = [];

View File

@@ -13,6 +13,23 @@
>
新增运送清单
</el-button>
<el-dropdown
v-hasPermi="['order:import']"
@command="handleImportCommand"
style="margin-left: 10px"
>
<el-button type="success">
导入数据
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="import">导入数据</el-dropdown-item>
<el-dropdown-item command="download">下载模板</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<input ref="fileInputRef" type="file" accept=".xlsx,.xls" style="display: none" @change="handleFileChange" />
</div>
</div>
@@ -58,7 +75,13 @@
<div class="dataListOnEmpty">暂无数据</div>
</template>
</el-table>
<pagination v-model:limit="form.pageSize" v-model:page="form.pageNum" :total="data.total" @pagination="getDataList" />
<Pagination
v-if="data.total > 0"
v-model:limit="form.pageSize"
v-model:page="form.pageNum"
:total="data.total"
@pagination="handlePagination"
/>
<OrderDialog ref="OrderDialogRef" @success="getDataList" />
<CreateDeliveryDialog ref="CreateDeliveryDialogRef" @success="getDataList" />
</div>
@@ -68,14 +91,19 @@
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { ArrowDown } from '@element-plus/icons-vue';
import * as XLSX from 'xlsx';
import baseSearch from '@/components/common/searchCustom/index.vue';
import { orderPageQuery, orderDelete } from '@/api/shipping.js';
import Pagination from '@/components/Pagination/index.vue';
import { orderPageQuery, orderDelete, orderBatchImport } from '@/api/shipping.js';
import { memberListByType, userAdd } from '@/api/userManage.js';
import OrderDialog from './orderDialog.vue';
import CreateDeliveryDialog from './createDeliveryDialog.vue';
const baseSearchRef = ref();
const OrderDialogRef = ref();
const CreateDeliveryDialogRef = ref();
const fileInputRef = ref();
const formItemList = reactive([
{
label: '买方',
@@ -129,10 +157,19 @@ const form = reactive({
});
const searchFrom = () => {
form.pageNum = 1;
getDataList();
};
// 处理分页事件
const handlePagination = (paginationData) => {
if (paginationData) {
form.pageNum = paginationData.page || form.pageNum;
form.pageSize = paginationData.limit || form.pageSize;
}
getDataList();
};
// 列表
const getDataList = () => {
data.dataListLoading = true;
@@ -163,23 +200,38 @@ const getDataList = () => {
// 调用订单列表接口,而不是装车订单接口
console.log('[ORDER-LIST] 请求参数:', params);
orderPageQuery(params)
.then((res) => {
data.dataListLoading = false;
// 直接赋值订单数据
rows.value = res.data?.rows || [];
data.total = res.data?.total || 0;
console.log('[ORDER-LIST] 响应数据:', res);
if (rows.value.length > 0) {
// 处理响应数据结构
// PageResultResponse 返回的结构是 { code, msg, data: { total, rows } }
let responseData = res.data || res;
// 如果 data 是对象且包含 total 和 rows直接使用
if (responseData && typeof responseData === 'object' && 'total' in responseData && 'rows' in responseData) {
rows.value = responseData.rows || [];
data.total = responseData.total || 0;
} else if (responseData && responseData.data) {
// 如果 data 是嵌套的,使用 data.data
rows.value = responseData.data.rows || [];
data.total = responseData.data.total || 0;
} else {
// 兜底处理
rows.value = [];
data.total = 0;
}
console.log('[ORDER-LIST] 解析后的数据 - 总数:', data.total, '当前页数据:', rows.value.length);
})
.catch(() => {
.catch((error) => {
console.error('[ORDER-LIST] 请求失败:', error);
data.dataListLoading = false;
rows.value = [];
data.total = 0;
});
};
// 新增装车订单
@@ -224,6 +276,319 @@ const del = (id) => {
});
};
// 导入数据相关
const handleImportCommand = (command) => {
if (command === 'import') {
if (fileInputRef.value) {
fileInputRef.value.click();
}
} else if (command === 'download') {
downloadTemplate();
}
};
// 下载导入模板
const downloadTemplate = () => {
try {
// 创建表头
const headers = ['单价/斤', '卖方全称', '买方全称'];
// 创建工作簿
const wb = XLSX.utils.book_new();
// 创建工作表数据(只有表头)
const wsData = [headers];
const ws = XLSX.utils.aoa_to_sheet(wsData);
// 设置列宽
ws['!cols'] = [
{ wch: 15 }, // 单价/斤
{ wch: 30 }, // 卖方全称
{ wch: 30 } // 买方全称
];
// 将工作表添加到工作簿
XLSX.utils.book_append_sheet(wb, ws, '订单导入模板');
// 生成Excel文件并下载
XLSX.writeFile(wb, '订单导入模板.xlsx');
ElMessage.success('模板下载成功');
} catch (error) {
console.error('下载模板失败:', error);
ElMessage.error('下载模板失败:' + (error.message || '未知错误'));
}
};
// 处理文件选择
const handleFileChange = async (event) => {
const file = event.target.files[0];
if (!file) {
return;
}
// 验证文件类型
const fileName = file.name;
const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
if (fileExtension !== 'xlsx' && fileExtension !== 'xls') {
ElMessage.error('请选择Excel文件.xlsx或.xls格式');
event.target.value = '';
return;
}
try {
// 读取Excel文件
const excelData = await readExcelFile(file);
// 解析和转换数据
const orderDataList = await parseExcelData(excelData);
if (orderDataList.length === 0) {
ElMessage.warning('Excel文件中没有有效数据');
event.target.value = '';
return;
}
// 批量导入
await batchImportOrders(orderDataList);
// 清空文件选择
event.target.value = '';
} catch (error) {
console.error('导入失败:', error);
ElMessage.error('导入失败:' + (error.message || '未知错误'));
event.target.value = '';
}
};
// 读取Excel文件
const readExcelFile = (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: 'array' });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
resolve(jsonData);
} catch (error) {
reject(new Error('Excel文件解析失败' + error.message));
}
};
reader.onerror = () => {
reject(new Error('文件读取失败'));
};
reader.readAsArrayBuffer(file);
});
};
// 解析Excel数据
const parseExcelData = async (data) => {
if (!data || data.length < 2) {
return [];
}
// 第一行是表头
const headers = data[0].map(h => String(h || '').trim());
// 查找列索引
const priceIndex = headers.findIndex(h => h.includes('单价') || h.includes('价格'));
const sellerIndex = headers.findIndex(h => h.includes('卖方'));
const buyerIndex = headers.findIndex(h => h.includes('买方'));
if (priceIndex === -1) {
throw new Error('未找到"单价/斤"列');
}
if (sellerIndex === -1) {
throw new Error('未找到"卖方全称"列');
}
if (buyerIndex === -1) {
throw new Error('未找到"买方全称"列');
}
const orderDataList = [];
// 从第二行开始读取数据
for (let i = 1; i < data.length; i++) {
const row = data[i];
if (!row || row.length === 0) {
continue;
}
const price = row[priceIndex];
const sellerName = String(row[sellerIndex] || '').trim();
const buyerName = String(row[buyerIndex] || '').trim();
// 跳过空行
if (!sellerName && !buyerName) {
continue;
}
// 验证必填字段
if (!sellerName) {
throw new Error(`${i + 1}行:卖方全称不能为空`);
}
if (!buyerName) {
throw new Error(`${i + 1}行:买方全称不能为空`);
}
if (price === null || price === undefined || price === '') {
throw new Error(`${i + 1}行:单价/斤不能为空`);
}
// 转换价格为数字
const firmPrice = parseFloat(price);
if (isNaN(firmPrice) || firmPrice < 0) {
throw new Error(`${i + 1}行:单价/斤格式不正确必须为大于等于0的数字`);
}
// 查找或创建用户
const sellerId = await findOrCreateUser(sellerName);
const buyerId = await findOrCreateUser(buyerName);
if (!sellerId) {
throw new Error(`${i + 1}行:无法创建或找到卖方"${sellerName}"`);
}
if (!buyerId) {
throw new Error(`${i + 1}行:无法创建或找到买方"${buyerName}"`);
}
orderDataList.push({
sellerId: String(sellerId),
buyerId: String(buyerId),
settlementType: 1, // 默认结算方式为上车重量
firmPrice: firmPrice
});
}
return orderDataList;
};
// 查找或创建用户
const findOrCreateUser = async (username) => {
if (!username || !username.trim()) {
return null;
}
try {
// 先尝试查找用户(同时查询 type=2 和 type=4
const [supplierRes, purchaserRes] = await Promise.all([
memberListByType({
pageNum: 1,
pageSize: 100,
type: 2,
username: username.trim()
}),
memberListByType({
pageNum: 1,
pageSize: 100,
type: 4,
username: username.trim()
})
]);
// 合并结果并查找匹配的用户(精确匹配用户名)
const supplierList = supplierRes.data?.rows || [];
const purchaserList = purchaserRes.data?.rows || [];
const allUsers = [...supplierList, ...purchaserList];
// 精确匹配用户名
const matchedUser = allUsers.find(user => {
const userUsername = String(user.username || '').trim();
return userUsername === username.trim();
});
if (matchedUser && matchedUser.id) {
return matchedUser.id;
}
// 如果找不到创建新用户默认类型为2-供应商)
try {
const createRes = await userAdd({
username: username.trim(),
type: 2, // 默认类型为供应商
status: 0, // 启用
mobile: null,
cbkAccount: '',
remark: '通过订单导入自动创建'
});
if (createRes.code === 200) {
// 创建成功后再次查询获取新用户的ID
const newUserRes = await memberListByType({
pageNum: 1,
pageSize: 1,
type: 2,
username: username.trim()
});
const newUserList = newUserRes.data?.rows || [];
if (newUserList.length > 0) {
const newUser = newUserList.find(user => {
const userUsername = String(user.username || '').trim();
return userUsername === username.trim();
});
if (newUser && newUser.id) {
return newUser.id;
}
}
} else {
throw new Error(createRes.msg || '创建用户失败');
}
} catch (createError) {
console.error(`创建用户失败 "${username}":`, createError);
throw new Error(`创建用户"${username}"失败:${createError.message || '未知错误'}`);
}
return null;
} catch (error) {
console.error(`查找或创建用户失败 "${username}":`, error);
throw error;
}
};
// 批量导入订单
const batchImportOrders = async (orderDataList) => {
const total = orderDataList.length;
ElMessage.info(`开始导入,共${total}条数据`);
try {
const res = await orderBatchImport({
orders: orderDataList
});
if (res.code === 200) {
const successCount = res.data?.successCount || total;
const failCount = res.data?.failCount || 0;
const failMessages = res.data?.failMessages || [];
if (failCount === 0) {
ElMessage.success(`导入成功!共导入${successCount}条数据`);
} else {
ElMessage.warning(`导入完成:成功${successCount}条,失败${failCount}`);
if (failMessages.length > 0) {
ElMessageBox.alert(
failMessages.slice(0, 10).join('\n') + (failMessages.length > 10 ? `\n...还有${failMessages.length - 10}条错误` : ''),
'导入失败详情',
{ type: 'error' }
);
}
}
// 刷新列表
getDataList();
} else {
ElMessage.error(res.msg || '导入失败');
}
} catch (error) {
console.error('批量导入失败:', error);
const errorMsg = error.response?.data?.msg || error.message || '未知错误';
ElMessage.error('导入失败:' + errorMsg);
}
};
onMounted(() => {
getDataList();
@@ -290,3 +655,4 @@ onMounted(() => {
}
}
</style>

View File

@@ -7,17 +7,13 @@
clearable
filterable
remote
:remote-method="supplierRemoteMethod"
:loading="data.supplierLoading"
:remote-method="sellerRemoteMethod"
:loading="data.sellerLoading"
placeholder="请选择卖方"
style="width: 100%"
multiple
collapse-tags
collapse-tags-tooltip
:max-collapse-tags="2"
>
<el-option
v-for="item in data.supplierOptions"
v-for="item in data.sellerOptions"
:key="item.id"
:label="item.username + ' / ' + item.mobile + ' (' + item.tenantName + ')'"
:value="item.id"
@@ -25,11 +21,11 @@
</el-option>
<el-pagination
style="padding: 0px 20px"
@current-change="supplierHandleCurrentChange"
@current-change="sellerHandleCurrentChange"
:page-size="10"
:current-page="data.supplierPageNum"
:current-page="data.sellerPageNum"
layout="total, prev, pager, next"
:total="data.supplierTotal"
:total="data.sellerTotal"
>
</el-pagination>
</el-select>
@@ -41,17 +37,13 @@
clearable
filterable
remote
:remote-method="purchaserRemoteMethod"
:loading="data.purchaserLoading"
:remote-method="buyerRemoteMethod"
:loading="data.buyerLoading"
placeholder="请选择买方"
style="width: 100%"
multiple
collapse-tags
collapse-tags-tooltip
:max-collapse-tags="2"
>
<el-option
v-for="item in data.purchaserOptions"
v-for="item in data.buyerOptions"
:key="item.id"
:label="item.username + ' / ' + item.mobile + ' (' + item.tenantName + ')'"
:value="item.id"
@@ -59,11 +51,11 @@
</el-option>
<el-pagination
style="padding: 0px 20px"
@current-change="purchaserHandleCurrentChange"
@current-change="buyerHandleCurrentChange"
:page-size="10"
:current-page="data.purchaserPageNum"
:current-page="data.buyerPageNum"
layout="total, prev, pager, next"
:total="data.purchaserTotal"
:total="data.buyerTotal"
>
</el-pagination>
</el-select>
@@ -110,33 +102,52 @@ const formDataRef = ref(null);
const data = reactive({
dialogVisible: false,
saveLoading: false,
supplierOptions: [],
purchaserOptions: [],
driverLoading: false,
driverName: '',
driverPageNum: 1,
driverTotal: 0,
purchaserLoading: false,
purchaserPageNum: 1,
purchaserTotal: 0,
purchaserName: '',
supplierLoading: false,
supplierPageNum: 1,
supplierTotal: 0,
supplierName: '',
sellerOptions: [],
buyerOptions: [],
sellerLoading: false,
sellerPageNum: 1,
sellerTotal: 0,
sellerName: '',
buyerLoading: false,
buyerPageNum: 1,
buyerTotal: 0,
buyerName: '',
});
const ruleForm = reactive({
id: null, // 订单ID编辑时使用
buyerId: [], // 买方ID数组
sellerId: [], // 卖方ID数组
buyerId: null, // 买方ID(单选)
sellerId: null, // 卖方ID(单选)
settlementType: 1, // 结算方式1-上车重量2-下车重量3-按肉价结算
firmPrice: null, // 约定价格(元/斤)
});
// 验证买方和卖方不能是同一用户
const validateBuyerSeller = (rule, value, callback) => {
if (value && value === ruleForm.sellerId) {
callback(new Error('买方和卖方不能是同一用户'));
} else {
callback();
}
};
const validateSellerBuyer = (rule, value, callback) => {
if (value && value === ruleForm.buyerId) {
callback(new Error('卖方和买方不能是同一用户'));
} else {
callback();
}
};
const rules = reactive({
buyerId: [{ required: true, message: '请选择买方', trigger: 'change' }],
sellerId: [{ required: true, message: '请选择方', trigger: 'change' }],
buyerId: [
{ required: true, message: '请选择方', trigger: 'change' },
{ validator: validateBuyerSeller, trigger: 'change' }
],
sellerId: [
{ required: true, message: '请选择卖方', trigger: 'change' },
{ validator: validateSellerBuyer, trigger: 'change' }
],
settlementType: [{ required: true, message: '请选择结算方式', trigger: 'change' }],
firmPrice: [
{ required: true, message: '请输入约定价格', trigger: 'blur' },
@@ -150,79 +161,127 @@ const handleClose = () => {
}
// 重置表单数据
ruleForm.id = null;
ruleForm.buyerId = [];
ruleForm.sellerId = [];
ruleForm.buyerId = null;
ruleForm.sellerId = null;
ruleForm.settlementType = 1;
ruleForm.firmPrice = null;
data.dialogVisible = false;
};
// ----------------
// 供应商远程搜索
const supplierRemoteMethod = (e) => {
data.supplierName = e;
data.supplierPageNum = 1;
getSupplierList();
// 卖方远程搜索(同时查询 type=2 和 type=4
const sellerRemoteMethod = (e) => {
data.sellerName = e;
data.sellerPageNum = 1;
getSellerList();
};
// 供应商 列表
const getSupplierList = () => {
data.supplierLoading = true;
const params = {
pageNum: data.supplierPageNum,
pageSize: 10,
type: 2, // 供应商类型
username: data.supplierName,
};
memberListByType(params)
.then((res) => {
data.supplierLoading = false;
data.supplierOptions = res.data.rows;
data.supplierTotal = res.data.total;
// 卖方列表(合并 type=2 和 type=4 的用户)
const getSellerList = () => {
data.sellerLoading = true;
const searchName = data.sellerName || '';
// 同时查询供应商type=2和采购商type=4
Promise.all([
memberListByType({
pageNum: data.sellerPageNum,
pageSize: 10,
type: 2,
username: searchName,
}),
memberListByType({
pageNum: data.sellerPageNum,
pageSize: 10,
type: 4,
username: searchName,
})
])
.then(([supplierRes, purchaserRes]) => {
// 合并结果并去重(根据 id
const supplierList = supplierRes.data.rows || [];
const purchaserList = purchaserRes.data.rows || [];
const mergedList = [...supplierList, ...purchaserList];
// 去重:使用 Map 根据 id 去重
const uniqueMap = new Map();
mergedList.forEach(item => {
if (!uniqueMap.has(item.id)) {
uniqueMap.set(item.id, item);
}
});
data.sellerOptions = Array.from(uniqueMap.values());
// 总数取两个查询的总和(实际可能包含重复,但用于分页显示)
data.sellerTotal = (supplierRes.data.total || 0) + (purchaserRes.data.total || 0);
data.sellerLoading = false;
})
.catch(() => {
data.supplierLoading = false;
data.sellerLoading = false;
});
};
// 选择供应商分页
const supplierHandleCurrentChange = (val) => {
data.supplierPageNum = val;
getSupplierList();
// 卖方分页
const sellerHandleCurrentChange = (val) => {
data.sellerPageNum = val;
getSellerList();
};
// ----------------
// 采购商远程搜索
const purchaserRemoteMethod = (e) => {
data.purchaserName = e;
data.purchaserPageNum = 1;
getPurchaserList();
// 买方远程搜索(同时查询 type=2 和 type=4
const buyerRemoteMethod = (e) => {
data.buyerName = e;
data.buyerPageNum = 1;
getBuyerList();
};
// 采购商 列表
const getPurchaserList = () => {
data.purchaserLoading = true;
const params = {
pageNum: data.purchaserPageNum,
pageSize: 10,
type: 4, // 采购商类型
username: data.purchaserName,
};
memberListByType(params)
.then((res) => {
data.purchaserLoading = false;
data.purchaserOptions = res.data.rows;
data.purchaserTotal = res.data.total;
// 买方列表(合并 type=2 和 type=4 的用户)
const getBuyerList = () => {
data.buyerLoading = true;
const searchName = data.buyerName || '';
// 同时查询供应商type=2和采购商type=4
Promise.all([
memberListByType({
pageNum: data.buyerPageNum,
pageSize: 10,
type: 2,
username: searchName,
}),
memberListByType({
pageNum: data.buyerPageNum,
pageSize: 10,
type: 4,
username: searchName,
})
])
.then(([supplierRes, purchaserRes]) => {
// 合并结果并去重(根据 id
const supplierList = supplierRes.data.rows || [];
const purchaserList = purchaserRes.data.rows || [];
const mergedList = [...supplierList, ...purchaserList];
// 去重:使用 Map 根据 id 去重
const uniqueMap = new Map();
mergedList.forEach(item => {
if (!uniqueMap.has(item.id)) {
uniqueMap.set(item.id, item);
}
});
data.buyerOptions = Array.from(uniqueMap.values());
// 总数取两个查询的总和(实际可能包含重复,但用于分页显示)
data.buyerTotal = (supplierRes.data.total || 0) + (purchaserRes.data.total || 0);
data.buyerLoading = false;
})
.catch(() => {
data.purchaserLoading = false;
data.buyerLoading = false;
});
};
// 采购商分页
const purchaserHandleCurrentChange = (val) => {
data.purchaserPageNum = val;
getPurchaserList();
// 买方分页
const buyerHandleCurrentChange = (val) => {
data.buyerPageNum = val;
getBuyerList();
};
// 保存
@@ -232,8 +291,8 @@ const onClickSave = () => {
if (valid) {
const params = {
id: ruleForm.id,
buyerId: ruleForm.buyerId.join(','), // 将数组转为逗号分隔的字符串
sellerId: ruleForm.sellerId.join(','), // 将数组转为逗号分隔的字符串
buyerId: ruleForm.buyerId ? String(ruleForm.buyerId) : '', // 转为字符串格式(后端可能期望字符串
sellerId: ruleForm.sellerId ? String(ruleForm.sellerId) : '', // 转为字符串格式
settlementType: ruleForm.settlementType,
firmPrice: ruleForm.firmPrice, // 约定价格(元/斤)
};
@@ -274,38 +333,45 @@ const onShowDialog = (orderData) => {
// 重置表单数据
ruleForm.id = orderData?.id || null;
// 处理buyerId支持字符串数组两种格式
// 处理buyerId支持字符串数组和数字格式,取第一个值(单选模式)
if (orderData?.buyerId) {
if (Array.isArray(orderData.buyerId)) {
if (typeof orderData.buyerId === 'number') {
ruleForm.buyerId = orderData.buyerId;
} else if (Array.isArray(orderData.buyerId) && orderData.buyerId.length > 0) {
ruleForm.buyerId = parseInt(orderData.buyerId[0]);
} else if (typeof orderData.buyerId === 'string' && orderData.buyerId.trim() !== '') {
ruleForm.buyerId = orderData.buyerId.split(',').map(id => id.trim()).filter(id => id !== '');
const ids = orderData.buyerId.split(',').map(id => id.trim()).filter(id => id !== '');
ruleForm.buyerId = ids.length > 0 ? parseInt(ids[0]) : null;
} else {
ruleForm.buyerId = [];
ruleForm.buyerId = null;
}
} else {
ruleForm.buyerId = [];
ruleForm.buyerId = null;
}
// 处理sellerId支持字符串数组两种格式
// 处理sellerId支持字符串数组和数字格式,取第一个值(单选模式)
if (orderData?.sellerId) {
if (Array.isArray(orderData.sellerId)) {
if (typeof orderData.sellerId === 'number') {
ruleForm.sellerId = orderData.sellerId;
} else if (Array.isArray(orderData.sellerId) && orderData.sellerId.length > 0) {
ruleForm.sellerId = parseInt(orderData.sellerId[0]);
} else if (typeof orderData.sellerId === 'string' && orderData.sellerId.trim() !== '') {
ruleForm.sellerId = orderData.sellerId.split(',').map(id => id.trim()).filter(id => id !== '');
const ids = orderData.sellerId.split(',').map(id => id.trim()).filter(id => id !== '');
ruleForm.sellerId = ids.length > 0 ? parseInt(ids[0]) : null;
} else {
ruleForm.sellerId = [];
ruleForm.sellerId = null;
}
} else {
ruleForm.sellerId = [];
ruleForm.sellerId = null;
}
ruleForm.settlementType = orderData?.settlementType || 1;
ruleForm.firmPrice = orderData?.firmPrice != null ? orderData.firmPrice : null;
data.dialogVisible = true;
getSupplierList();
getPurchaserList();
// 初始化时加载列表
getSellerList();
getBuyerList();
};
defineExpose({

View File

@@ -1,12 +1,25 @@
<template>
<el-dialog v-model="dialogVisible" :before-close="handleClose" :title="title" style="width: 700px; padding-bottom: 20px">
<div class="global_dialog_content">
<el-form ref="FormDataRef" :model="form.data" :rules="form.rules" label-width="100">
<el-form-item label="岗位名称" prop="name">
<el-input v-model="form.data.name" placeholder="请输入岗位名称" style="width: 320px" />
<el-form ref="FormDataRef" :model="form.data" :rules="form.rules" label-width="120px" class="post-form">
<el-form-item label="岗位名称" prop="name" class="post-name-form-item">
<el-input
v-model="form.data.name"
placeholder="请输入岗位名称"
clearable
class="post-name-input"
/>
</el-form-item>
<el-form-item label="岗位描述" prop="description">
<el-input maxlength="100" v-model="form.data.description" placeholder="请输入岗位描述" style="width: 320px" />
<el-input
v-model="form.data.description"
type="textarea"
:rows="4"
maxlength="100"
placeholder="请输入岗位描述"
show-word-limit
class="post-description-input"
/>
</el-form-item>
</el-form>
</div>
@@ -19,7 +32,8 @@
</el-dialog>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { reactive, ref, nextTick, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { sysPositionSave } from '~/api/sys.js';
const dialogVisible = ref(false);
@@ -29,6 +43,31 @@ const title = ref('');
const userId = ref('');
const emits = defineEmits();
// 修复标签宽度问题:覆盖内联样式
const fixLabelWidth = () => {
nextTick(() => {
const label = document.querySelector('.post-name-form-item .el-form-item__label');
if (label) {
// 直接设置样式属性,覆盖内联样式
label.style.width = '120px';
label.style.minWidth = '120px';
label.style.maxWidth = '120px';
label.style.whiteSpace = 'nowrap';
label.style.overflow = 'visible';
label.style.wordBreak = 'keep-all';
}
});
};
// 监听对话框打开,修复标签宽度
watch(dialogVisible, (newVal) => {
if (newVal) {
fixLabelWidth();
// 延迟再次修复,确保 DOM 完全渲染
setTimeout(fixLabelWidth, 100);
}
});
const form = reactive({
data: {
name: '',
@@ -48,6 +87,8 @@ const onShowDialog = (val, page) => {
userId.value = val.id;
form.data = val;
}
// 修复标签宽度
fixLabelWidth();
};
const onClickSave = () => {
@@ -83,4 +124,72 @@ defineExpose({
onShowDialog,
});
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.post-form {
// 针对岗位名称表单项的标签,确保不换行
// 使用更具体的选择器来覆盖内联样式
:deep(.post-name-form-item) {
.el-form-item__label {
// 使用 !important 覆盖内联样式
width: 120px !important;
min-width: 120px !important;
max-width: 120px !important;
white-space: nowrap !important;
overflow: visible !important;
text-overflow: clip !important;
padding-right: 12px !important;
line-height: 32px !important;
word-break: keep-all !important;
word-wrap: normal !important;
display: inline-block !important;
flex-shrink: 0 !important;
box-sizing: border-box !important;
// 强制覆盖任何内联样式
&[style*="width"] {
width: 120px !important;
}
}
// 确保表单项内容区域不会挤压标签
.el-form-item__content {
flex: 1;
min-width: 0;
}
}
// 全局覆盖所有标签的内联样式
:deep(.el-form-item__label[style*="width: 65px"]) {
width: 120px !important;
min-width: 120px !important;
max-width: 120px !important;
}
// 所有标签的通用样式
:deep(.el-form-item__label) {
width: 120px !important;
min-width: 120px !important;
white-space: nowrap !important;
overflow: visible !important;
word-break: keep-all !important;
word-wrap: normal !important;
}
// 优化输入框宽度
.post-name-input {
width: 100%;
max-width: 500px;
}
.post-description-input {
width: 100%;
max-width: 500px;
}
// 确保表单项布局正常
:deep(.el-form-item) {
margin-bottom: 22px;
display: flex;
align-items: flex-start;
}
}
</style>

View File

@@ -137,15 +137,15 @@ const handleRemove = (file, fileList, type) => {
const beforeAvatarUpload = (file) => {
const isImage = file.type.startsWith('image/');
const isLt2M = file.size / 1024 / 1024 < 2;
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isImage) {
ElMessage.error('上传文件只能是图片格式!');
}
if (!isLt2M) {
ElMessage.error('上传图片大小不能超过 2MB!');
if (!isLt10M) {
ElMessage.error('上传图片大小不能超过 10MB!');
}
return isImage && isLt2M;
return isImage && isLt10M;
};
const handleExceed = (number) => {

View File

@@ -1,8 +1,10 @@
<template>
<div>
<base-search :formItemList="formItemList" @search="searchFrom" ref="baseSearchRef"> </base-search>
<div style="display: flex; padding: 10px; background: #fff; margin-bottom: 10px">
<div style="display: flex; padding: 10px; background: #fff; margin-bottom: 10px; gap: 10px">
<el-button type="primary" @click="showAddDialog(null)">新增用户</el-button>
<el-button type="success" @click="handleImportClick">导入数据</el-button>
<input ref="fileInputRef" type="file" accept=".xlsx,.xls" style="display: none" @change="handleFileChange" />
</div>
<div class="main-container">
<el-table :data="data.rows" border v-loading="data.dataListLoading" element-loading-text="数据加载中..." style="width: 100%">
@@ -51,12 +53,15 @@
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import * as XLSX from 'xlsx';
import baseSearch from '@/components/common/searchCustom/index.vue';
import { userList, userStatus } from '@/api/userManage.js';
import { userList, userStatus, userAdd, userEdit } from '@/api/userManage.js';
import UserDialog from './userDialog.vue';
const baseSearchRef = ref();
const UserDialogRef = ref();
const fileInputRef = ref();
const formItemList = reactive([
{
label: '用户姓名',
@@ -179,6 +184,224 @@ const showAddDialog = (row) => {
UserDialogRef.value.onShowDialog(row);
}
};
// 导入数据相关
const handleImportClick = () => {
if (fileInputRef.value) {
fileInputRef.value.click();
}
};
const handleFileChange = async (event) => {
const file = event.target.files[0];
if (!file) {
return;
}
// 验证文件类型
const fileName = file.name;
const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
if (fileExtension !== 'xlsx' && fileExtension !== 'xls') {
ElMessage.error('请选择Excel文件.xlsx或.xls格式');
event.target.value = '';
return;
}
try {
// 读取Excel文件
const data = await readExcelFile(file);
// 解析和转换数据
const userDataList = parseExcelData(data);
if (userDataList.length === 0) {
ElMessage.warning('Excel文件中没有有效数据');
event.target.value = '';
return;
}
// 批量导入
await batchImportUsers(userDataList);
// 清空文件选择
event.target.value = '';
} catch (error) {
console.error('导入失败:', error);
ElMessage.error('导入失败:' + (error.message || '未知错误'));
event.target.value = '';
}
};
// 读取Excel文件
const readExcelFile = (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: 'array' });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
resolve(jsonData);
} catch (error) {
reject(new Error('Excel文件解析失败' + error.message));
}
};
reader.onerror = () => {
reject(new Error('文件读取失败'));
};
reader.readAsArrayBuffer(file);
});
};
// 解析Excel数据
const parseExcelData = (data) => {
if (!data || data.length < 2) {
return [];
}
// 第一行是表头
const headers = data[0].map(h => String(h || '').trim());
// 查找列索引
const companyNameIndex = headers.findIndex(h => h.includes('公司昵称') || h.includes('公司名称'));
const statusIndex = headers.findIndex(h => h.includes('合作状态'));
const contactPersonIndex = headers.findIndex(h => h.includes('对接人'));
const contactInfoIndex = headers.findIndex(h => h.includes('联系方式'));
if (companyNameIndex === -1) {
throw new Error('未找到"公司昵称"列');
}
if (statusIndex === -1) {
throw new Error('未找到"合作状态"列');
}
const userDataList = [];
// 从第二行开始读取数据
for (let i = 1; i < data.length; i++) {
const row = data[i];
if (!row || row.length === 0) {
continue;
}
const companyName = String(row[companyNameIndex] || '').trim();
const status = String(row[statusIndex] || '').trim();
const contactPerson = contactPersonIndex >= 0 ? String(row[contactPersonIndex] || '').trim() : '';
const contactInfo = contactInfoIndex >= 0 ? String(row[contactInfoIndex] || '').trim() : '';
// 跳过空行
if (!companyName && !status) {
continue;
}
// 构建用户姓名:公司昵称/对接人
let username = companyName;
if (contactPerson) {
username = `${companyName}/${contactPerson}`;
}
// 映射用户类型:采购商=4供应商=2
let type = null;
if (status === '采购商') {
type = 4;
} else if (status === '供应商') {
type = 2;
} else if (status) {
// 如果合作状态不是采购商或供应商,跳过该行
console.warn(`${i + 1}行:未知的合作状态"${status}",已跳过`);
continue;
}
// 如果用户类型无法确定,跳过
if (type === null) {
console.warn(`${i + 1}行:无法确定用户类型,已跳过`);
continue;
}
userDataList.push({
username,
mobile: contactInfo || null, // 联系方式作为手机号如果为空则为null
type,
cbkAccount: '', // CBK账号为空
status: 0, // 账号状态:启用
remark: '', // 备注为空
});
}
return userDataList;
};
// 批量导入用户
const batchImportUsers = async (userDataList) => {
const total = userDataList.length;
let successCount = 0;
let failCount = 0;
const failMessages = [];
ElMessage.info(`开始导入,共${total}条数据`);
for (let i = 0; i < userDataList.length; i++) {
const userData = userDataList[i];
try {
// 如果手机号不为空,先检查是否存在
let existingUser = null;
if (userData.mobile) {
// 查询是否存在该手机号的用户
const queryResult = await userList({
pageNum: 1,
pageSize: 1,
mobile: userData.mobile,
mobileExact: true,
});
if (queryResult.data && queryResult.data.rows && queryResult.data.rows.length > 0) {
existingUser = queryResult.data.rows[0];
}
}
if (existingUser) {
// 如果存在,则更新
await userEdit({
id: existingUser.id,
...userData,
});
successCount++;
} else {
// 如果不存在,则新增
await userAdd(userData);
successCount++;
}
} catch (error) {
failCount++;
const errorMsg = error.response?.data?.msg || error.message || '未知错误';
failMessages.push(`${i + 1}行(${userData.username}${errorMsg}`);
console.error(`导入第${i + 1}行失败:`, error);
}
}
// 显示导入结果
if (failCount === 0) {
ElMessage.success(`导入成功!共导入${successCount}条数据`);
} else {
ElMessage.warning(`导入完成:成功${successCount}条,失败${failCount}`);
if (failMessages.length > 0) {
console.error('导入失败详情:', failMessages);
// 可以在这里显示详细的错误信息比如使用MessageBox
ElMessageBox.alert(
failMessages.slice(0, 10).join('\n') + (failMessages.length > 10 ? `\n...还有${failMessages.length - 10}条错误` : ''),
'导入失败详情',
{ type: 'error' }
);
}
}
// 刷新列表
getDataList();
};
onMounted(() => {
getDataList();
});

View File

@@ -92,13 +92,16 @@ const rules = reactive({
username: [{ required: true, message: '请输入用户姓名', trigger: 'blur' }],
mobile: [
{
required: true,
validator(rule, value, callback) {
if (!value) {
callback(new Error('请输入用户手机号'));
// 如果为空,直接通过验证
if (!value || value.trim() === '') {
callback();
return;
}
// 如果有值,则验证格式
if (!checkMobile(value)) {
callback(new Error('请输入正确的手机号'));
return;
}
callback();
},
@@ -106,7 +109,7 @@ const rules = reactive({
},
],
type: [{ required: true, message: '请选择用户类型', trigger: 'change' }],
cbkAccount: [{ required: true, message: '请输入CBK账号', trigger: 'blur' }],
cbkAccount: [],
status: [{ required: true, message: '请选择账户状态', trigger: 'change' }],
});
const handleClose = () => {

View File

@@ -49,41 +49,49 @@
</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>
<template v-if="getFirstImageUrl(scope.row.drivingLicensePhoto)">
<el-image
:src="getFirstImageUrl(scope.row.drivingLicensePhoto)"
style="width: 60px; height: 60px; border-radius: 4px; border: 1px solid #dcdfe6"
fit="cover"
:preview-src-list="getImageList(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>
<div v-if="getImageCount(scope.row.drivingLicensePhoto) > 1" style="margin-top: 4px; font-size: 12px; color: #409eff">
{{ getImageCount(scope.row.drivingLicensePhoto) }}
</div>
</template>
<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>
<template v-if="getFirstImageUrl(scope.row.recordCode)">
<el-image
:src="getFirstImageUrl(scope.row.recordCode)"
style="width: 60px; height: 60px; border-radius: 4px; border: 1px solid #dcdfe6"
fit="cover"
:preview-src-list="getImageList(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>
<div v-if="getImageCount(scope.row.recordCode) > 1" style="margin-top: 4px; font-size: 12px; color: #409eff">
{{ getImageCount(scope.row.recordCode) }}
</div>
</template>
<span v-else style="color: #999; font-size: 12px">暂无图片</span>
</template>
</el-table-column>
@@ -194,6 +202,69 @@ const getDataList = async () => {
}
};
// 解析图片 URL支持 JSON 数组和单图片)
const getFirstImageUrl = (value) => {
if (!value) return null;
// 检查是否是 JSON 字符串格式(以 [ 开头)
if (typeof value === 'string' && value.trim().startsWith('[')) {
try {
const parsed = JSON.parse(value);
if (Array.isArray(parsed) && parsed.length > 0) {
return parsed[0];
}
return null;
} catch (e) {
// 解析失败,按单图片处理(兼容旧数据)
return value;
}
}
// 不是 JSON 格式,按单图片处理(兼容旧数据)
return value;
};
// 获取图片列表(用于预览)
const getImageList = (value) => {
if (!value) return [];
// 检查是否是 JSON 字符串格式(以 [ 开头)
if (typeof value === 'string' && value.trim().startsWith('[')) {
try {
const parsed = JSON.parse(value);
if (Array.isArray(parsed) && parsed.length > 0) {
return parsed;
}
return [];
} catch (e) {
// 解析失败,按单图片处理(兼容旧数据)
return [value];
}
}
// 不是 JSON 格式,按单图片处理(兼容旧数据)
return [value];
};
// 获取图片数量
const getImageCount = (value) => {
if (!value) return 0;
// 检查是否是 JSON 字符串格式(以 [ 开头)
if (typeof value === 'string' && value.trim().startsWith('[')) {
try {
const parsed = JSON.parse(value);
if (Array.isArray(parsed)) {
return parsed.length;
}
return 0;
} catch (e) {
// 解析失败,按单图片处理(兼容旧数据)
return 1;
}
}
// 不是 JSON 格式,按单图片处理(兼容旧数据)
return 1;
};
onMounted(() => {
getDataList();
});

View File

@@ -38,7 +38,6 @@
</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')"
@@ -47,14 +46,12 @@
: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')"
@@ -63,7 +60,6 @@
:before-upload="beforeAvatarUpload"
:file-list="ruleForm.recordCode"
:headers="importHeaders"
:on-exceed="handleExceed"
>
<el-icon><Plus /></el-icon>
</el-upload>
@@ -118,7 +114,6 @@ const rules = reactive({
});
const handleAvatarSuccess = (res, file, fileList, type) => {
if (ruleForm.hasOwnProperty(type)) {
let imageUrl = null;
@@ -133,8 +128,21 @@ const handleAvatarSuccess = (res, file, fileList, type) => {
}
if (imageUrl) {
ruleForm[type] = [{ url: imageUrl, uid: file.uid, name: file.name }];
// 行驶证和备案码支持多图片,其他字段保持单图片
if (type === 'drivingLicensePhoto' || type === 'recordCode') {
// 多图片:更新整个文件列表
ruleForm[type] = fileList.map(item => {
// 如果是新上传的文件,使用新的 URL
if (item.uid === file.uid) {
return { url: imageUrl, uid: file.uid, name: file.name };
}
// 保持原有文件信息
return item;
});
} else {
// 单图片:只保留最新的一张
ruleForm[type] = [{ url: imageUrl, uid: file.uid, name: file.name }];
}
} else {
console.error('无法解析图片URL:', res);
ElMessage.error('上传失败无法获取图片URL');
@@ -153,21 +161,21 @@ const handlePreview = (file) => {
const beforeAvatarUpload = (file) => {
const isImage = file.type.startsWith('image/');
const isLt2M = file.size / 1024 / 1024 < 2;
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isImage) {
ElMessage.error('上传文件只能是图片格式!');
return false;
}
if (!isLt2M) {
ElMessage.error('上传图片大小不能超过 2MB');
if (!isLt10M) {
ElMessage.error('上传图片大小不能超过 10MB');
return false;
}
return true;
};
const handleExceed = () => {
ElMessage.warning('最多只能上传一张图片!');
// 已移除单图片限制,此方法不再需要
};
const onClickSave = async () => {
@@ -176,13 +184,21 @@ const onClickSave = async () => {
data.saveLoading = true;
try {
// 将行驶证和备案码的图片数组转换为 JSON 字符串
const drivingLicensePhotoJson = ruleForm.drivingLicensePhoto.length > 0
? JSON.stringify(ruleForm.drivingLicensePhoto.map(item => item.url))
: null;
const recordCodeJson = ruleForm.recordCode.length > 0
? JSON.stringify(ruleForm.recordCode.map(item => item.url))
: null;
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,
drivingLicensePhoto: drivingLicensePhotoJson,
recordCode: recordCodeJson,
remark: ruleForm.remark,
};
@@ -229,8 +245,83 @@ const open = (row = null) => {
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 }] : [];
// 行驶证照片:尝试解析 JSON 数组,如果失败则按单图片处理
if (row.drivingLicensePhoto) {
const drivingLicenseValue = row.drivingLicensePhoto;
console.log('[VEHICLE] 行驶证照片原始数据:', drivingLicenseValue, typeof drivingLicenseValue);
// 检查是否是 JSON 字符串格式(以 [ 开头)
if (typeof drivingLicenseValue === 'string' && drivingLicenseValue.trim().startsWith('[')) {
try {
const parsed = JSON.parse(drivingLicenseValue);
console.log('[VEHICLE] 行驶证照片解析结果:', parsed);
if (Array.isArray(parsed) && parsed.length > 0) {
ruleForm.drivingLicensePhoto = parsed.map((url, index) => {
// 确保 url 是字符串类型
const urlStr = typeof url === 'string' ? url : String(url);
console.log(`[VEHICLE] 行驶证照片 ${index}:`, urlStr);
return {
url: urlStr,
uid: Date.now() + index,
name: `行驶证${index + 1}.jpg`
};
});
console.log('[VEHICLE] 行驶证照片最终数据:', ruleForm.drivingLicensePhoto);
} else {
ruleForm.drivingLicensePhoto = [];
}
} catch (e) {
console.error('解析行驶证照片 JSON 失败:', e, drivingLicenseValue);
// 解析失败,按单图片处理(兼容旧数据)
ruleForm.drivingLicensePhoto = [{ url: drivingLicenseValue }];
}
} else {
// 不是 JSON 格式,按单图片处理(兼容旧数据)
console.log('[VEHICLE] 行驶证照片不是 JSON 格式,按单图片处理');
ruleForm.drivingLicensePhoto = [{ url: drivingLicenseValue }];
}
} else {
ruleForm.drivingLicensePhoto = [];
}
// 牧运通备案码:尝试解析 JSON 数组,如果失败则按单图片处理
if (row.recordCode) {
const recordCodeValue = row.recordCode;
console.log('[VEHICLE] 备案码原始数据:', recordCodeValue, typeof recordCodeValue);
// 检查是否是 JSON 字符串格式(以 [ 开头)
if (typeof recordCodeValue === 'string' && recordCodeValue.trim().startsWith('[')) {
try {
const parsed = JSON.parse(recordCodeValue);
console.log('[VEHICLE] 备案码解析结果:', parsed);
if (Array.isArray(parsed) && parsed.length > 0) {
ruleForm.recordCode = parsed.map((url, index) => {
// 确保 url 是字符串类型
const urlStr = typeof url === 'string' ? url : String(url);
console.log(`[VEHICLE] 备案码 ${index}:`, urlStr);
return {
url: urlStr,
uid: Date.now() + index + 1000,
name: `备案码${index + 1}.jpg`
};
});
console.log('[VEHICLE] 备案码最终数据:', ruleForm.recordCode);
} else {
ruleForm.recordCode = [];
}
} catch (e) {
console.error('解析备案码 JSON 失败:', e, recordCodeValue);
// 解析失败,按单图片处理(兼容旧数据)
ruleForm.recordCode = [{ url: recordCodeValue }];
}
} else {
// 不是 JSON 格式,按单图片处理(兼容旧数据)
console.log('[VEHICLE] 备案码不是 JSON 格式,按单图片处理');
ruleForm.recordCode = [{ url: recordCodeValue }];
}
} else {
ruleForm.recordCode = [];
}
ruleForm.remark = row.remark || '';
}
};

View File

@@ -948,6 +948,11 @@
"resolved" "https://registry.npmmirror.com/acorn/-/acorn-8.12.1.tgz"
"version" "8.12.1"
"adler-32@~1.3.0":
"integrity" "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A=="
"resolved" "https://registry.npmmirror.com/adler-32/-/adler-32-1.3.1.tgz"
"version" "1.3.1"
"ajv@^6.12.4":
"integrity" "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="
"resolved" "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz"
@@ -1433,6 +1438,14 @@
"tslib" "^2.0.3"
"upper-case-first" "^2.0.2"
"cfb@~1.2.1":
"integrity" "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA=="
"resolved" "https://registry.npmmirror.com/cfb/-/cfb-1.2.2.tgz"
"version" "1.2.2"
dependencies:
"adler-32" "~1.3.0"
"crc-32" "~1.2.0"
"chalk@^1.1.3":
"integrity" "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A=="
"resolved" "https://registry.npmmirror.com/chalk/-/chalk-1.1.3.tgz"
@@ -1601,6 +1614,11 @@
"resolved" "https://registry.npmmirror.com/clone/-/clone-2.1.2.tgz"
"version" "2.1.2"
"codepage@~1.15.0":
"integrity" "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA=="
"resolved" "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz"
"version" "1.15.0"
"collection-visit@^1.0.0":
"integrity" "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw=="
"resolved" "https://registry.npmmirror.com/collection-visit/-/collection-visit-1.0.0.tgz"
@@ -1830,6 +1848,11 @@
"parse-json" "^5.2.0"
"path-type" "^4.0.0"
"crc-32@~1.2.0", "crc-32@~1.2.1":
"integrity" "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="
"resolved" "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz"
"version" "1.2.2"
"create-require@^1.1.0":
"integrity" "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
"resolved" "https://registry.npmmirror.com/create-require/-/create-require-1.1.1.tgz"
@@ -3016,6 +3039,11 @@
"combined-stream" "^1.0.8"
"mime-types" "^2.1.12"
"frac@~1.1.2":
"integrity" "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA=="
"resolved" "https://registry.npmmirror.com/frac/-/frac-1.1.2.tgz"
"version" "1.1.2"
"fragment-cache@^0.2.1":
"integrity" "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA=="
"resolved" "https://registry.npmmirror.com/fragment-cache/-/fragment-cache-0.2.1.tgz"
@@ -5898,6 +5926,13 @@
dependencies:
"readable-stream" "^3.0.0"
"ssf@~0.11.2":
"integrity" "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g=="
"resolved" "https://registry.npmmirror.com/ssf/-/ssf-0.11.2.tgz"
"version" "0.11.2"
dependencies:
"frac" "~1.1.2"
"ssr-window@^3.0.0-alpha.1":
"integrity" "sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA=="
"resolved" "https://registry.npmmirror.com/ssr-window/-/ssr-window-3.0.0.tgz"
@@ -6848,11 +6883,21 @@
"resolved" "https://registry.npmmirror.com/windicss/-/windicss-3.5.6.tgz"
"version" "3.5.6"
"wmf@~1.0.1":
"integrity" "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw=="
"resolved" "https://registry.npmmirror.com/wmf/-/wmf-1.0.2.tgz"
"version" "1.0.2"
"word-wrap@^1.0.3", "word-wrap@^1.2.3", "word-wrap@^1.2.5":
"integrity" "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="
"resolved" "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz"
"version" "1.2.5"
"word@~0.3.0":
"integrity" "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA=="
"resolved" "https://registry.npmmirror.com/word/-/word-0.3.0.tgz"
"version" "0.3.0"
"wrap-ansi@^6.2.0":
"integrity" "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="
"resolved" "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz"
@@ -6876,6 +6921,19 @@
"resolved" "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz"
"version" "1.0.2"
"xlsx@^0.18.5":
"integrity" "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ=="
"resolved" "https://registry.npmmirror.com/xlsx/-/xlsx-0.18.5.tgz"
"version" "0.18.5"
dependencies:
"adler-32" "~1.3.0"
"cfb" "~1.2.1"
"codepage" "~1.15.0"
"crc-32" "~1.2.1"
"ssf" "~0.11.2"
"wmf" "~1.0.1"
"word" "~0.3.0"
"xml-name-validator@^4.0.0":
"integrity" "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw=="
"resolved" "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz"