Files
cattleTransportation/pc-cattle-transportation/src/views/entry/details.vue
2025-11-28 17:12:36 +08:00

1450 lines
58 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<!-- 参数缺失时的友好提示 -->
<div v-if="!route.query.id" class="error-container">
<el-result icon="warning" title="参数缺失" sub-title="缺少必要的参数无法加载详情页面">
<template #extra>
<el-button type="primary" @click="goBack">返回上一页</el-button>
<el-button @click="goToList">前往列表页面</el-button>
</template>
</el-result>
</div>
<!-- 正常内容 -->
<div v-else class="details-container">
<!-- 头部导航与操作 -->
<div class="page-header">
<div class="header-left">
<span class="page-title">运单详情</span>
<el-tag :type="getStatusType(data.baseInfo.status)" class="status-tag" size="large" effect="dark">
{{ getStatusText(data.baseInfo.status) }}
</el-tag>
</div>
<div class="header-right">
<el-button @click="goBack">返回</el-button>
</div>
</div>
<!-- 基础信息卡片 -->
<el-card class="section-card" shadow="hover">
<template #header>
<div class="card-header">
<span class="header-title">基础信息</span>
</div>
</template>
<el-descriptions :column="4" border size="large">
<el-descriptions-item label="运单号">{{ data.baseInfo.deliveryNumber || '-' }}</el-descriptions-item>
<el-descriptions-item label="卖方">{{ getSupplierName() }}</el-descriptions-item>
<el-descriptions-item label="买方">{{ getBuyerName() }}</el-descriptions-item>
<el-descriptions-item label="车牌号">{{ data.baseInfo.licensePlate || '-' }}</el-descriptions-item>
<el-descriptions-item label="司机姓名">{{ data.baseInfo.driverName || '-' }}</el-descriptions-item>
<el-descriptions-item label="起始地">{{ data.baseInfo.startLocation || '-' }}</el-descriptions-item>
<el-descriptions-item label="送达目的地">{{ data.baseInfo.endLocation || '-' }}</el-descriptions-item>
<el-descriptions-item label="预计送达时间">{{ data.baseInfo.estimatedDeliveryTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ data.baseInfo.createTime || '' }}</el-descriptions-item>
<el-descriptions-item label="司机运费">{{ data.baseInfo.freight ? data.baseInfo.freight + ' 元' : '-' }}</el-descriptions-item>
<el-descriptions-item label="登记设备数量">{{ totalRegisteredDevices }} </el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 预警信息卡片 -->
<el-card v-if="data.warnInfo && data.warnInfo.length > 0" class="section-card warning-card" shadow="hover">
<template #header>
<div class="card-header">
<span class="header-title warning-text">预警信息</span>
</div>
</template>
<div class="warning-list">
<el-descriptions :column="4" border v-for="(item, index) in data.warnInfo" :key="index" class="warning-item">
<template v-if="item.warningType == 2">
<el-descriptions-item label="预警原因">
<span class="red">{{ item.warningTypeDesc }}</span>
</el-descriptions-item>
<el-descriptions-item label="预警时间">{{ item.warningTime || '--' }}</el-descriptions-item>
<el-descriptions-item label="智能耳标数"> {{ totalRegisteredDevices }} </el-descriptions-item>
<el-descriptions-item label="车内盘点数量"> {{ item.inventoryJbqCount || '--' }} </el-descriptions-item>
</template>
<template v-if="item.warningType == 3">
<el-descriptions-item label="预警原因">
<span class="red">{{ item.warningTypeDesc || '' }}</span>
</el-descriptions-item>
<el-descriptions-item label="预警时间">{{ item.warningTime || '' }}</el-descriptions-item>
<el-descriptions-item label="应行驶距离"> {{ item.expectedDistance || '' }} km </el-descriptions-item>
<el-descriptions-item label="实际行驶距离"> {{ item.actualDistance || '' }} km </el-descriptions-item>
</template>
</el-descriptions>
</div>
</el-card>
<!-- 装车信息卡片 -->
<el-card class="section-card" shadow="hover">
<template #header>
<div class="card-header">
<span class="header-title">装车信息</span>
</div>
</template>
<!-- 重量信息分组 -->
<div v-if="hasValue(data.baseInfo.emptyWeight) || hasValue(data.baseInfo.entruckWeight) || hasValue(data.baseInfo.landingEntruckWeight)" class="info-group">
<div class="card-header">
<span class="header-title">重量信息</span>
</div>
<div class="weight-info-container">
<!-- 基础重量信息 -->
<div class="weight-basic-info">
<el-descriptions :column="3" border size="large">
<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 class="weight-calculated-info">
<el-descriptions :column="3" border size="large">
<el-descriptions-item v-if="departureCattleWeight !== null" label="发车牛只重量">
<span class="calculated-value">{{ departureCattleWeight }}kg</span>
</el-descriptions-item>
<el-descriptions-item v-if="landingCattleWeight !== null" label="落地牛只重量">
<span class="calculated-value">{{ landingCattleWeight }}kg</span>
</el-descriptions-item>
<el-descriptions-item v-if="weightLoss !== null" label="损耗">
<span class="calculated-value loss-value" :class="{ 'positive-loss': weightLoss < 0, 'negative-loss': weightLoss > 0 }">
{{ weightLoss > 0 ? '-' : weightLoss < 0 ? '+' : '' }}{{ Math.abs(weightLoss) }}kg
</span>
</el-descriptions-item>
</el-descriptions>
</div>
</div>
</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="sub-title">照片信息</div>
<div class="media-grid">
<div class="media-item" v-if="data.baseInfo.carFrontPhoto || data.baseInfo.carBehindPhoto">
<div class="media-label">车身照片</div>
<div class="media-content">
<el-image
v-if="data.baseInfo.carFrontPhoto"
class="photo-img"
:src="data.baseInfo.carFrontPhoto"
fit="cover"
:preview-src-list="[data.baseInfo.carFrontPhoto]"
preview-teleported
/>
<el-image
v-if="data.baseInfo.carBehindPhoto"
class="photo-img"
:src="data.baseInfo.carBehindPhoto"
fit="cover"
:preview-src-list="[data.baseInfo.carBehindPhoto]"
preview-teleported
/>
</div>
</div>
<div class="media-item" v-if="hasValue(data.baseInfo.quarantineTickeyUrl)">
<div class="media-label">检疫票</div>
<el-image
class="photo-img"
:src="data.baseInfo.quarantineTickeyUrl"
fit="cover"
:preview-src-list="[data.baseInfo.quarantineTickeyUrl]"
preview-teleported
/>
</div>
<div class="media-item" v-if="hasValue(data.baseInfo.poundListImg)">
<div class="media-label">纸质磅单</div>
<el-image
class="photo-img"
:src="data.baseInfo.poundListImg"
fit="cover"
:preview-src-list="[data.baseInfo.poundListImg]"
preview-teleported
/>
</div>
<div class="media-item" v-if="hasValue(data.baseInfo.emptyVehicleFrontPhoto)">
<div class="media-label">空磅车头照片</div>
<el-image
class="photo-img"
:src="data.baseInfo.emptyVehicleFrontPhoto"
fit="cover"
:preview-src-list="[data.baseInfo.emptyVehicleFrontPhoto]"
preview-teleported
/>
</div>
<div class="media-item" v-if="hasValue(data.baseInfo.loadedVehicleFrontPhoto)">
<div class="media-label">过重磅车头照片</div>
<el-image
class="photo-img"
:src="data.baseInfo.loadedVehicleFrontPhoto"
fit="cover"
:preview-src-list="[data.baseInfo.loadedVehicleFrontPhoto]"
preview-teleported
/>
</div>
<div class="media-item" v-if="hasValue(data.baseInfo.loadedVehicleWeightPhoto)">
<div class="media-label">车辆重磅照片</div>
<el-image
class="photo-img"
:src="data.baseInfo.loadedVehicleWeightPhoto"
fit="cover"
:preview-src-list="[data.baseInfo.loadedVehicleWeightPhoto]"
preview-teleported
/>
</div>
<div class="media-item" v-if="hasValue(data.baseInfo.driverIdCardPhoto)">
<div class="media-label">驾驶员手持身份证</div>
<el-image
class="photo-img"
:src="data.baseInfo.driverIdCardPhoto"
fit="cover"
:preview-src-list="[data.baseInfo.driverIdCardPhoto]"
preview-teleported
/>
</div>
<div class="media-item" v-if="hasValue(data.baseInfo.destinationPoundListImg)">
<div class="media-label">落地纸质磅单</div>
<el-image
class="photo-img"
:src="data.baseInfo.destinationPoundListImg"
fit="cover"
:preview-src-list="[data.baseInfo.destinationPoundListImg]"
preview-teleported
/>
</div>
<div class="media-item" v-if="hasValue(data.baseInfo.destinationVehicleFrontPhoto)">
<div class="media-label">落地过重磅车头</div>
<el-image
class="photo-img"
:src="data.baseInfo.destinationVehicleFrontPhoto"
fit="cover"
:preview-src-list="[data.baseInfo.destinationVehicleFrontPhoto]"
preview-teleported
/>
</div>
</div>
</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="sub-title">视频信息</div>
<div class="media-grid">
<div class="media-item video-item" v-if="hasValue(data.baseInfo.emptyWeightVideo)">
<div class="media-label">空车过磅视频</div>
<video controls :src="data.baseInfo.emptyWeightVideo" />
</div>
<div class="media-item video-item" v-if="hasValue(data.baseInfo.entruckWeightVideo)">
<div class="media-label">装车过磅视频</div>
<video controls :src="data.baseInfo.entruckWeightVideo" />
</div>
<div class="media-item video-item" v-if="hasValue(data.baseInfo.entruckVideo)">
<div class="media-label">装车视频</div>
<video controls :src="data.baseInfo.entruckVideo" />
</div>
<div class="media-item video-item" v-if="hasValue(data.baseInfo.controlSlotVideo)">
<div class="media-label">控槽视频</div>
<video controls :src="data.baseInfo.controlSlotVideo" />
</div>
<div class="media-item video-item" v-if="hasValue(data.baseInfo.cattleLoadingCircleVideo)">
<div class="media-label">装完牛绕车一圈</div>
<video controls :src="data.baseInfo.cattleLoadingCircleVideo" />
</div>
<div class="media-item video-item" v-if="hasValue(data.baseInfo.unloadCattleVideo)">
<div class="media-label">卸牛视频</div>
<video controls :src="data.baseInfo.unloadCattleVideo" />
</div>
<div class="media-item video-item" v-if="hasValue(data.baseInfo.destinationWeightVideo)">
<div class="media-label">落地过磅视频</div>
<video controls :src="data.baseInfo.destinationWeightVideo" />
</div>
</div>
</div>
</el-card>
<!-- 设备信息卡片 -->
<el-card class="section-card" shadow="hover">
<template #header>
<div class="card-header">
<span class="header-title">设备信息</span>
</div>
</template>
<el-tabs v-model="activeDeviceTab" type="border-card">
<el-tab-pane label="智能主机" name="host">
<el-table
:data="data.hostRows"
border
stripe
v-loading="data.hostDataListLoading"
element-loading-text="数据加载中..."
style="width: 100%"
>
<el-table-column label="智能主机编号" prop="deviceId"></el-table-column>
<el-table-column label="设备电量" prop="battery">
<template #default="scope"> {{ scope.row.battery || scope.row.deviceVoltage || '-' }}% </template>
</el-table-column>
<el-table-column label="步数" prop="steps">
<template #default="scope"> {{ scope.row.steps || scope.row.walkSteps || '-' }}</template>
</el-table-column>
<el-table-column label="设备温度" prop="temperature">
<template #default="scope"> {{ scope.row.temperature || scope.row.deviceTemp || '-' }} </template>
</el-table-column>
<el-table-column label="数据最后更新时间" prop="time">
<template #default="scope"> {{ scope.row.time || scope.row.updateTime || scope.row.createTime || '-' }}</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="scope">
<el-button link type="primary" @click="hostLogClick(scope.row)">日志</el-button>
<el-button link type="primary" @click="hostTrackClick(scope.row)">运动轨迹</el-button>
<el-button link type="primary" @click="hostLocationClick(scope.row)">定位</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="智能项圈" name="collar">
<el-table
:data="data.collarRows"
border
stripe
v-loading="data.collarDataListLoading"
element-loading-text="数据加载中..."
style="width: 100%"
>
<el-table-column label="智能项圈编号" prop="deviceId"></el-table-column>
<el-table-column label="设备电量" prop="battery">
<template #default="scope"> {{ scope.row.battery || scope.row.deviceVoltage || '-' }}% </template>
</el-table-column>
<el-table-column label="步数" prop="steps">
<template #default="scope"> {{ scope.row.steps || scope.row.walkSteps || '-' }}</template>
</el-table-column>
<el-table-column label="设备温度" prop="deviceTemp">
<template #default="scope"> {{ scope.row.deviceTemp || scope.row.temperature || '-' }} </template>
</el-table-column>
<el-table-column label="数据最后更新时间" prop="time">
<template #default="scope"> {{ scope.row.time || scope.row.updateTime || scope.row.createTime || '-' }}</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="scope">
<el-button link type="primary" @click="collarLogClick(scope.row)">日志</el-button>
<el-button link type="primary" @click="collarTrackClick(scope.row)">运动轨迹</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<pagination
v-model:limit="collarForm.pageSize"
v-model:page="collarForm.pageNum"
:total="data.collarTotal"
@pagination="getCollarList"
/>
</div>
</el-tab-pane>
<el-tab-pane label="智能耳标" name="ear">
<el-table :data="data.rows" border stripe v-loading="data.dataListLoading" element-loading-text="数据加载中..." style="width: 100%">
<el-table-column label="智能耳标编号" prop="deviceId"></el-table-column>
<el-table-column label="设备电量" prop="battery">
<template #default="scope"> {{ scope.row.battery || scope.row.deviceVoltage || '-' }}% </template>
</el-table-column>
<el-table-column label="步数" prop="walkSteps">
<template #default="scope"> {{ scope.row.walkSteps || scope.row.steps || '-' }}</template>
</el-table-column>
<el-table-column label="设备温度" prop="deviceTemp">
<template #default="scope"> {{ scope.row.deviceTemp || scope.row.temperature || '-' }} </template>
</el-table-column>
<el-table-column label="数据最后更新时间" prop="updateTime">
<template #default="scope"> {{ scope.row.updateTime || scope.row.createTime || '-' }}</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="scope">
<el-button link type="primary" @click="earLogClick(scope.row)">日志</el-button>
<el-button link type="primary" @click="earTrackClick(scope.row)">运动轨迹</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<pagination v-model:limit="form.pageSize" v-model:page="form.pageNum" :total="data.total" @pagination="getEarList" />
</div>
</el-tab-pane>
</el-tabs>
</el-card>
<!-- 查看主机定位 -->
<el-dialog v-model="data.dialogVisible" title="查看定位" style="width: 650px; padding-bottom: 20px" destroy-on-close>
<div>
<baidu-map style="height: 500px" class="map" :zoom="15" :center="data.center" :scroll-wheel-zoom="true" v-if="data.dialogVisible">
<bm-map-type :map-types="['BMAP_NORMAL_MAP', 'BMAP_HYBRID_MAP']"></bm-map-type>
<bm-marker :position="data.center" :dragging="true" animation="BMAP_ANIMATION_BOUNCE">
<bm-label
:content="data.updateTime"
:labelStyle="{
color: '#67c23a',
fontSize: '12px',
borderColor: '#fff',
borderRadius: 10,
}"
:offset="{ width: -60, height: 10 }"
/>
</bm-marker>
</baidu-map>
</div>
</el-dialog>
<!-- 查看轨迹 -->
<el-dialog v-model="data.trackDialogVisible" title="查看运动轨迹" style="width: 650px; padding-bottom: 20px" destroy-on-close>
<div
v-loading="data.trackLoading"
element-loading-text="正在加载中..."
style="height: 450px"
element-loading-background="rgba(255, 255, 255,1)"
>
<div class="empty-box" v-if="data.trajectoryStatus">
<img style="width: 50%" src="../../assets/images/wuguiji.png" />
</div>
<baidu-map
v-else
class="map"
@ready="handler"
:center="data.centerPoint"
:zoom="data.trackZoom"
:dragging="true"
:auto-resize="true"
:scroll-wheel-zoom="true"
style="height: 450px"
>
<bm-polyline stroke-color="blue" :path="data.path" :stroke-opacity="0.5" :stroke-weight="3" :editing="false"></bm-polyline>
<bm-marker :icon="startMarkIcon" :position="{ lng: data.startMark.lng, lat: data.startMark.lat }"></bm-marker>
<bm-marker :icon="endMarkIcon" :position="{ lng: data.endMark.lng, lat: data.endMark.lat }"></bm-marker>
</baidu-map>
</div>
</el-dialog>
<!-- 日志弹窗耳标 -->
<el-dialog v-model="data.earLogDialogVisible" title="设备日志" width="900px" append-to-body destroy-on-close>
<el-table :data="data.earLogRows" border stripe v-loading="data.logListLoading" element-loading-text="数据加载中..." style="width: 100%">
<el-table-column label="智能耳标编号" prop="deviceId"></el-table-column>
<el-table-column label="设备电量" prop="deviceVoltage">
<template #default="scope"> {{ scope.row.deviceVoltage || scope.row.battery || '-' }}% </template>
</el-table-column>
<el-table-column label="步数" prop="walkSteps">
<template #default="scope"> {{ scope.row.walkSteps || scope.row.steps || '-' }}</template>
</el-table-column>
<el-table-column label="设备温度" prop="deviceTemp">
<template #default="scope"> {{ scope.row.deviceTemp || scope.row.temperature || '-' }} </template>
</el-table-column>
<el-table-column label="数据最后更新时间" prop="updateTime" width="180">
<template #default="scope"> {{ scope.row.updateTime || scope.row.createTime || '-' }}</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="scope">
<el-button link type="primary" @click="locateClick(scope.row)">定位</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<pagination v-model:limit="earLogForm.pageSize" v-model:page="earLogForm.pageNum" :total="data.earLogTotal" @pagination="getEarLogList" />
</div>
</el-dialog>
<!-- 日志弹窗项圈 -->
<el-dialog v-model="data.collarDialogVisible" title="设备日志" width="900px" append-to-body destroy-on-close>
<el-table :data="data.collarLogRows" border stripe v-loading="data.logListLoading" element-loading-text="数据加载中..." style="width: 100%">
<el-table-column label="智能项圈编号" prop="deviceId"></el-table-column>
<el-table-column label="设备电量" prop="battery">
<template #default="scope"> {{ scope.row.battery || scope.row.deviceVoltage || '-' }}% </template>
</el-table-column>
<el-table-column label="步数" prop="steps">
<template #default="scope"> {{ scope.row.steps || scope.row.walkSteps || '-' }}</template>
</el-table-column>
<el-table-column label="设备温度" prop="temperature">
<template #default="scope"> {{ scope.row.temperature || scope.row.deviceTemp || '-' }} </template>
</el-table-column>
<el-table-column label="数据最后更新时间" prop="time" width="180">
<template #default="scope"> {{ scope.row.time || scope.row.updateTime || scope.row.createTime || '-' }}</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="scope">
<el-button link type="primary" @click="collarlocateClick(scope.row)">定位</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<pagination
v-model:limit="collarLogForm.pageSize"
v-model:page="collarLogForm.pageNum"
:total="data.collarLogTotal"
@pagination="getCollarLogList"
/>
</div>
</el-dialog>
<!-- 日志弹窗主机 -->
<el-dialog v-model="data.hostLogDialogVisible" title="智能主机日志" width="900px" append-to-body destroy-on-close>
<el-table :data="data.hostLogRows" border stripe v-loading="data.logListLoading" element-loading-text="数据加载中..." style="width: 100%">
<el-table-column label="智能主机编号" prop="deviceId"></el-table-column>
<el-table-column label="设备电量" prop="deviceVoltage">
<template #default="scope"> {{ scope.row.deviceVoltage || scope.row.battery || '-' }}% </template>
</el-table-column>
<el-table-column label="步数" prop="walkSteps">
<template #default="scope"> {{ scope.row.walkSteps || scope.row.steps || '-' }}</template>
</el-table-column>
<el-table-column label="设备温度" prop="deviceTemp">
<template #default="scope"> {{ scope.row.deviceTemp || scope.row.temperature || '-' }} </template>
</el-table-column>
<el-table-column label="小时时间" prop="hourTime" width="180">
<template #default="scope"> {{ scope.row.hourTime || '-' }}</template>
</el-table-column>
<el-table-column label="数据最后更新时间" prop="updateTime" width="180">
<template #default="scope"> {{ scope.row.updateTime || scope.row.createTime || '-' }}</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="scope">
<el-button link type="primary" @click="hostLocationClick(scope.row)">定位</el-button>
</template>
</el-table-column>
</el-table>
</el-dialog>
<TrackDialog ref="TrackDialogRef" />
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import { earList, hostLocation, hostTrack, waybillDetail, collarList, collarLogList, earLogList, testDeliveryDevices, pageDeviceList, getEarTagLogs, getCollarLogs, getHostLogs, getEarTagTrajectory, getCollarTrajectory, getHostTrajectory } from '@/api/abroad.js';
import { vehicleList } from '@/api/userManage.js';
import startIcon from '../../assets/images/qi.png';
import endIcon from '../../assets/images/zhong.png';
import TrackDialog from '../hardware/trackDialog.vue';
const route = useRoute();
const router = useRouter(); // 添加 router
const TrackDialogRef = ref();
const activeDeviceTab = ref('host'); // 新增设备Tabs激活项
const startMarkIcon = reactive({
url: startIcon,
size: { width: 32, height: 32 },
opts: { anchor: { width: 16, height: 16 } },
});
const endMarkIcon = reactive({
url: endIcon,
size: { width: 32, height: 32 },
opts: { anchor: { width: 16, height: 16 } },
});
const data = reactive({
status: '',
length: '',
imgArr: [],
baseInfo: {
id: 1,
createByDesc: '申报人',
createByPhone: '联系电话',
rgstJbqNum: '申报数量',
createTime: '创建时间',
deliverNo: '申报运单号',
carNo: '车牌号',
quarStatusDesc: '入场状态', // 根据status反显
status: '订单状态 1境外预检 2 已入境待隔离(检疫成功) 3 已入隔离场 4 隔离场出场 ',
deliverTime: '启运时间',
// 新增照片字段
destinationPoundListImg: '',
destinationVehicleFrontPhoto: '',
// 新增视频字段
unloadCattleVideo: '',
destinationWeightVideo: '',
},
warnInfo: [],
serverIds: '',
dataListLoading: false,
rows: [],
total: 0,
radio: '1',
checkStatus: '', // 车内盘点状态
hgCheckStatus: '', // 海关盘点状态
dialogVisible: false, // 查看主机弹窗
center: { lng: 0, lat: 0 },
updateTime: '', // 主机定位时间
// 轨迹
trackDialogVisible: false,
trackZoom: 15,
path: [],
centerPoint: { lng: 0, lat: 0 },
trackLoading: false,
trajectoryStatus: true,
startMark: {},
endMark: {},
confirmLoading: false,
collarDataListLoading: false,
collarRows: [],
collarTotal: 0,
logListLoading: false,
earLogRows: [],
earLogDialogVisible: false,
collarDialogVisible: false,
hostLogDialogVisible: false,
earLogTotal: 0,
collarLogRows: [],
collarLogTotal: 0,
hostLogRows: [],
hostLogTotal: 0,
deviceId: '', // 耳标编号
sn: '', // 项圈编号
// 智能主机相关
hostDataListLoading: false,
hostRows: [],
hostTotal: 0,
});
const form = reactive({
pageNum: 1,
pageSize: 10,
});
const collarForm = reactive({
pageNum: 1,
pageSize: 10,
});
// 耳标日志
const earLogForm = reactive({
pageNum: 1,
pageSize: 10,
});
// 项圈日志
const collarLogForm = reactive({
pageNum: 1,
pageSize: 10,
});
// 查详情
const getDetail = () => {
if (!route.query.id) {
console.warn('=== 警告deliveryId为空跳过运单详情查询');
return;
}
waybillDetail(route.query.id)
.then((res) => {
if (res.code === 200) {
data.baseInfo = res.data.delivery ? res.data.delivery : {};
data.warnInfo = res.data.warningLog ? res.data.warningLog : {};
data.serverIds = res.data.serverIds ? res.data.serverIds : [];
// 查询车辆照片
if (data.baseInfo.licensePlate) {
loadVehiclePhotos();
}
// 使用新的统一API获取智能主机信息
getHostDeviceInfo();
} else {
ElMessage.error(res.msg);
}
})
.catch(() => {});
};
// 查询车辆照片(从司机信息获取)
const loadVehiclePhotos = () => {
// 后端已经从delivery/driver信息中获取了车身照片无需额外前端查询
// carFrontPhoto和carBehindPhoto应该已经由后端的DeliveryServiceImpl填充
};
// 智能主机列表查询
const getHostList = () => {
if (!route.query.id) {
console.warn('=== 警告deliveryId为空跳过主机列表查询');
data.hostDataListLoading = false;
return;
}
data.hostDataListLoading = true;
pageDeviceList({
pageNum: 1,
pageSize: 100, // 获取所有主机设备
deliveryId: parseInt(route.query.id),
deviceType: 1, // 智能主机设备类型
})
.then((res) => {
data.hostDataListLoading = false;
if (res.code === 200) {
// 新API返回的是数组格式过滤出智能主机设备
const hostDevices = res.data.filter(device => device.deviceType === 1 || device.deviceType === '1');
data.hostRows = hostDevices || [];
data.hostTotal = hostDevices.length || 0;
if (hostDevices.length > 0) {
// 如果有主机设备,取第一个作为主要主机
data.serverIds = hostDevices[0].deviceId || hostDevices[0].sn || '';
} else {
data.serverIds = '';
}
} else {
console.warn('获取主机设备信息失败:', res.msg);
data.hostRows = [];
data.hostTotal = 0;
data.serverIds = '';
}
})
.catch((err) => {
console.error('获取主机设备信息异常:', err);
data.hostDataListLoading = false;
data.hostRows = [];
data.hostTotal = 0;
data.serverIds = '';
});
};
// 获取智能主机信息(保留原有功能)
const getHostDeviceInfo = () => {
// 现在直接调用getHostList来获取主机信息
getHostList();
};
// 查看主机定位
const locationClick = (item) => {
getHostLocation(item);
};
//
// 查询主机经纬度
const getHostLocation = (item) => {
hostLocation({
serverDeviceId: data.serverIds,
})
.then((res) => {
if (res.code === 200) {
data.center.lng = res.data.longitude;
data.center.lat = res.data.latitude;
data.updateTime = res.data.updateTime;
data.dialogVisible = true;
} else {
ElMessage.error(res.msg);
}
})
.catch(() => {});
};
// 查看运动轨迹
const trackClick = (item) => {
data.trackDialogVisible = true;
getHostTrack(item);
};
// 查询主机运动轨迹
const getHostTrack = (item) => {
data.trackLoading = true;
hostTrack({
deliveryId: data.baseInfo.id,
serverDeviceId: Number(item),
})
.then((res) => {
data.trackLoading = false;
if (res.code === 200) {
if (res.data.length > 0) {
data.trajectoryStatus = false;
data.path = [];
res.data.forEach((item, index) => {
data.path.unshift({
lng: item.longitude,
lat: item.latitude,
});
});
data.startMark = data.path[0]; // 起点
data.endMark = data.path[data.path.length - 1]; // 终点
} else {
data.trajectoryStatus = true;
data.path = [];
}
} else {
ElMessage.error(res.msg);
}
})
.catch(() => {
data.trackLoading = false;
data.trajectoryStatus = true;
});
};
// --------- 智能耳标 -----------
// 耳标列表查询
const getEarList = () => {
if (!route.query.id) {
console.warn('=== 警告deliveryId为空跳过耳标列表查询');
data.dataListLoading = false;
return;
}
data.dataListLoading = true;
pageDeviceList({
pageNum: form.pageNum,
pageSize: form.pageSize,
deliveryId: parseInt(route.query.id),
deviceType: 2, // 智能耳标设备类型
})
.then((res) => {
data.dataListLoading = false;
if (res.code === 200) {
// 新API返回的是数组格式需要过滤出智能耳标设备
const earDevices = res.data.filter(device => device.deviceType === 2 || device.deviceType === '2');
data.rows = earDevices || [];
data.total = earDevices.length || 0;
} else {
ElMessage.error(res.msg);
data.total = 0;
}
})
.catch(() => {
data.dataListLoading = false;
});
};
const earLogClick = (row) => {
data.deviceId = row.deviceId || row.sn || '';
data.earLogDialogVisible = true;
// 调用新的API获取60分钟间隔的日志数据
getEarTagLogs({
deviceId: data.deviceId,
deliveryId: parseInt(route.query.id)
}).then((res) => {
if (res.code === 200) {
// 新API返回的是按60分钟分组的日志数据
data.earLogRows = res.data || [];
data.earLogTotal = res.data.length || 0;
} else {
ElMessage.error(res.msg || '获取智能耳标日志失败');
data.earLogRows = [];
data.earLogTotal = 0;
}
}).catch((error) => {
console.error('获取智能耳标日志异常:', error);
ElMessage.error('获取智能耳标日志失败');
data.earLogRows = [];
data.earLogTotal = 0;
});
};
// 智能耳标运动轨迹
const earTrackClick = (row) => {
// 调用新的API获取60分钟间隔的轨迹数据
getEarTagTrajectory({
deviceId: row.deviceId || row.sn || '',
deliveryId: parseInt(route.query.id)
}).then((res) => {
if (res.code === 200 && res.data && res.data.length > 0) {
// 新API返回的是按60分钟分组的轨迹点数据
const trajectoryPoints = res.data;
// 使用TrackDialog显示轨迹
if (TrackDialogRef.value) {
const info = {
deliveryId: route.query.id,
deviceId: row.deviceId || row.sn || '',
type: 'order',
trajectoryPoints: trajectoryPoints // 传递轨迹点数据
};
TrackDialogRef.value.onShowTrackDialog(info);
}
} else {
ElMessage.warning('该设备暂无运动轨迹数据');
}
}).catch((error) => {
console.error('获取智能耳标轨迹异常:', error);
ElMessage.error('获取智能耳标运动轨迹失败');
});
};
// 智能项圈列表查询
const getCollarList = () => {
if (!route.query.id) {
console.warn('=== 警告deliveryId为空跳过项圈列表查询');
data.collarDataListLoading = false;
return;
}
data.collarDataListLoading = true;
pageDeviceList({
pageNum: collarForm.pageNum,
pageSize: collarForm.pageSize,
deliveryId: parseInt(route.query.id),
deviceType: 4, // 智能项圈设备类型
})
.then((res) => {
data.collarDataListLoading = false;
if (res.code === 200) {
// 新API返回的是数组格式需要过滤出智能项圈设备
const collarDevices = res.data.filter(device => device.deviceType === 4 || device.deviceType === '4');
data.collarRows = collarDevices || [];
data.collarTotal = collarDevices.length || 0;
} else {
ElMessage.error(res.msg);
data.collarTotal = 0;
}
})
.catch(() => {
data.collarDataListLoading = false;
data.collarTotal = 0; // 确保total有默认值
});
};
const collarLogClick = (row) => {
data.sn = row.sn || row.deviceId || '';
data.collarDialogVisible = true;
// 调用新的API获取60分钟间隔的日志数据
getCollarLogs({
deviceId: data.sn,
deliveryId: parseInt(route.query.id)
}).then((res) => {
if (res.code === 200) {
// 新API返回的是按60分钟分组的日志数据
data.collarLogRows = res.data || [];
data.collarLogTotal = res.data.length || 0;
} else {
ElMessage.error(res.msg || '获取智能项圈日志失败');
data.collarLogRows = [];
data.collarLogTotal = 0;
}
}).catch((error) => {
console.error('获取智能项圈日志异常:', error);
ElMessage.error('获取智能项圈日志失败');
data.collarLogRows = [];
data.collarLogTotal = 0;
});
};
const handler = ({ BMap, map }) => {
// 自动获取展示的比例
const view = map.getViewport(eval(data.path));
data.trackZoom = view.zoom;
data.centerPoint = view.center;
};
// 取消按钮
const cancelClick = () => {
window.history.go(-1);
};
// 返回上一页
const goBack = () => {
window.history.go(-1);
};
// 前往列表页面
const goToList = () => {
router.push('/entry/attestation');
};
// 弹层
const locateClick = (row) => {
data.center.lng = row.longitude;
data.center.lat = row.latitude;
data.updateTime = row.updateTime || row.createTime || '';
data.dialogVisible = true;
};
//
const collarlocateClick = (row) => {
data.center.lng = row.longitude;
data.center.lat = row.latitude;
data.updateTime = row.time || row.updateTime || row.createTime || '';
data.dialogVisible = true;
};
// 耳标日志列表
const getEarLogList = () => {
data.logListLoading = true;
earLogList({
pageNum: earLogForm.pageNum,
pageSize: earLogForm.pageSize,
deliveryId: route.query.id,
deviceId: data.deviceId,
})
.then((res) => {
data.logListLoading = false;
if (res.code === 200) {
data.earLogRows = res.data.rows;
data.earLogTotal = res.data.total;
} else {
ElMessage.error(res.msg);
}
})
.catch(() => {
data.logListLoading = false;
data.earLogTotal = 0; // 确保total有默认值
});
};
// 项圈日志列表
const getCollarLogList = () => {
data.logListLoading = true;
collarLogList({
pageNum: collarLogForm.pageNum,
pageSize: collarLogForm.pageSize,
deliveryId: route.query.id,
deviceId: data.sn,
})
.then((res) => {
data.logListLoading = false;
if (res.code === 200) {
data.collarLogRows = res.data.rows;
data.collarLogTotal = res.data.total;
} else {
ElMessage.error(res.msg);
}
})
.catch(() => {
data.logListLoading = false;
data.collarLogTotal = 0; // 确保total有默认值
});
};
// 查看运动轨迹
const collarTrackClick = (row) => {
// 调用新的API获取60分钟间隔的轨迹数据
getCollarTrajectory({
deviceId: row.sn || row.deviceId || '',
deliveryId: parseInt(route.query.id)
}).then((res) => {
if (res.code === 200 && res.data && res.data.length > 0) {
// 新API返回的是按60分钟分组的轨迹点数据
const trajectoryPoints = res.data;
// 使用TrackDialog显示轨迹
if (TrackDialogRef.value) {
const info = {
deliveryId: route.query.id,
deviceId: row.sn || row.deviceId || '',
type: 'order',
trajectoryPoints: trajectoryPoints // 传递轨迹点数据
};
TrackDialogRef.value.onShowTrackDialog(info);
}
} else {
ElMessage.warning('该设备暂无运动轨迹数据');
}
}).catch((error) => {
console.error('获取智能项圈轨迹异常:', error);
ElMessage.error('获取智能项圈运动轨迹失败');
});
};
// 智能主机操作函数
const hostLogClick = (row) => {
data.deviceId = row.deviceId || row.sn || '';
data.hostLogDialogVisible = true;
// 调用新的API获取60分钟间隔的日志数据
getHostLogs({
deviceId: data.deviceId,
deliveryId: parseInt(route.query.id)
}).then((res) => {
if (res.code === 200) {
// 新API返回的是按60分钟分组的日志数据
data.hostLogRows = res.data || [];
data.hostLogTotal = res.data.length || 0;
} else {
ElMessage.error(res.msg || '获取智能主机日志失败');
data.hostLogRows = [];
data.hostLogTotal = 0;
}
}).catch((error) => {
console.error('获取智能主机日志异常:', error);
ElMessage.error('获取智能主机日志失败');
data.hostLogRows = [];
data.hostLogTotal = 0;
});
};
const hostTrackClick = (row) => {
// 调用新的API获取60分钟间隔的轨迹数据
getHostTrajectory({
deviceId: row.deviceId || row.sn || '',
deliveryId: parseInt(route.query.id)
}).then((res) => {
if (res.code === 200 && res.data && res.data.length > 0) {
// 新API返回的是按60分钟分组的轨迹点数据
const trajectoryPoints = res.data;
// 使用TrackDialog显示轨迹
if (TrackDialogRef.value) {
const info = {
deliveryId: route.query.id,
deviceId: row.deviceId || row.sn || '',
type: 'order',
trajectoryPoints: trajectoryPoints // 传递轨迹点数据
};
TrackDialogRef.value.onShowTrackDialog(info);
}
} else {
ElMessage.warning('该设备暂无运动轨迹数据');
}
}).catch((error) => {
console.error('获取智能主机轨迹异常:', error);
ElMessage.error('获取智能主机运动轨迹失败');
});
};
const hostLocationClick = (row) => {
data.center.lng = row.longitude;
data.center.lat = row.latitude;
data.updateTime = row.updateTime || row.createTime || '';
data.dialogVisible = true;
};
// 状态文本转换
// 计算所有绑定设备的总数
const totalRegisteredDevices = computed(() => {
const hostCount = data.hostTotal || 0;
const earCount = data.total || 0;
const collarCount = data.collarTotal || 0;
const total = hostCount + earCount + collarCount;
return total;
});
const getStatusText = (status) => {
const statusMap = {
1: '准备中',
2: '运输中',
3: '已结束'
};
return statusMap[status] || '未知状态';
};
// 根据状态返回标签类型(颜色)
const getStatusType = (status) => {
const typeMap = {
1: 'info', // 准备中 - 灰色
2: 'warning', // 运输中 - 橙色
3: 'success' // 已结束 - 绿色
};
return typeMap[status] || 'info';
};
// 判断字段是否有有效值(用于隐藏空值字段)
// 计算发车牛只重量(装车过磅重量 - 空车过磅重量)
const departureCattleWeight = computed(() => {
const emptyWeight = parseFloat(data.baseInfo.emptyWeight);
const entruckWeight = parseFloat(data.baseInfo.entruckWeight);
if (!isNaN(emptyWeight) && !isNaN(entruckWeight) && emptyWeight > 0 && entruckWeight > 0) {
const result = entruckWeight - emptyWeight;
return result > 0 ? result.toFixed(2) : null;
}
return null;
});
// 计算落地牛只重量(落地过磅重量 - 空车过磅重量)
const landingCattleWeight = computed(() => {
const emptyWeight = parseFloat(data.baseInfo.emptyWeight);
const landingWeight = parseFloat(data.baseInfo.landingEntruckWeight);
if (!isNaN(emptyWeight) && !isNaN(landingWeight) && emptyWeight > 0 && landingWeight > 0) {
const result = landingWeight - emptyWeight;
return result > 0 ? result.toFixed(2) : null;
}
return null;
});
// 计算损耗(发车牛只重量 - 落地牛只重量)
// 正数表示损耗(减少),负数表示增重(增加)
const weightLoss = computed(() => {
const departure = departureCattleWeight.value;
const landing = landingCattleWeight.value;
if (departure !== null && landing !== null) {
const result = parseFloat(departure) - parseFloat(landing);
return result.toFixed(2);
}
return null;
});
const hasValue = (value) => {
if (value === null || value === undefined) {
return false;
}
if (typeof value === 'string') {
return value.trim() !== '';
}
return true;
};
// 获取卖方名称:如果是从订单页面进入的,使用订单的卖方;否则显示 '-'
const getSupplierName = () => {
// 检查是否从订单页面进入(通过路由参数 fromOrder 判断)
if (route.query.fromOrder === 'true' && route.query.sellerName) {
return route.query.sellerName;
}
// 不是从订单页面进入的,直接显示 '-'
return '-';
};
// 获取买方名称:如果是从订单页面进入的,使用订单的买方;否则显示 '-'
const getBuyerName = () => {
// 检查是否从订单页面进入(通过路由参数 fromOrder 判断)
if (route.query.fromOrder === 'true' && route.query.buyerName) {
return route.query.buyerName;
}
// 不是从订单页面进入的,直接显示 '-'
return '-';
};
onMounted(() => {
data.id = route.query.id;
data.status = route.query.status;
data.length = route.query.length;
// 检查deliveryId是否存在
if (!route.query.id) {
console.warn('=== 警告deliveryId为空无法加载详情页面');
ElMessage.error('缺少必要的参数,请从列表页面点击详情按钮进入');
return;
}
// 检查deliveryId是否存在存在时才测试设备关联情况
testDeliveryDevices({ deliveryId: route.query.id })
.then(res => {
})
.catch(err => {
console.error('=== 测试设备关联失败:', err);
});
getDetail(); // 查详情
getHostList(); // 主机列表查询
getEarList(); // 耳标列表查询
getCollarList(); // 项圈类别查询
});
</script>
<style lang="less" scoped>
.error-container {
padding: 50px;
text-align: center;
}
.details-container {
padding: 20px;
background-color: #f5f7fa;
min-height: 100vh;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
background-color: #fff;
padding: 16px 24px;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
.header-left {
display: flex;
align-items: center;
.page-title {
font-size: 20px;
font-weight: 600;
color: #303133;
margin-right: 16px;
}
.status-tag {
font-weight: bold;
}
}
}
.section-card {
margin-bottom: 20px;
:deep(.el-card__header) {
padding: 15px 20px;
border-bottom: 1px solid #ebeef5;
}
.card-header {
display: flex;
align-items: center;
.header-title {
font-size: 16px;
font-weight: 600;
color: #303133;
position: relative;
padding-left: 12px;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 16px;
background-color: #409eff;
border-radius: 2px;
}
&.warning-text::before {
background-color: #f56c6c;
}
}
}
}
.warning-card {
border-color: #fde2e2;
:deep(.el-card__header) {
background-color: #fef0f0;
}
}
.info-group {
margin-bottom: 30px;
&:last-child {
margin-bottom: 0;
}
.sub-title {
font-size: 15px;
font-weight: 600;
color: #606266;
margin-bottom: 15px;
padding-left: 8px;
border-left: 3px solid #909399;
}
}
.weight-info-container {
display: flex;
flex-direction: row;
gap: 20px;
flex-wrap: wrap;
}
.weight-basic-info,
.weight-calculated-info {
flex: 1;
min-width: 400px;
}
.weight-calculated-info {
.calculated-value {
font-weight: 600;
font-size: 16px;
color: #303133;
}
.loss-value {
&.positive-loss {
color: #67c23a; // 绿色表示增重
}
&.negative-loss {
color: #f56c6c; // 红色表示损耗
}
}
}
@media (max-width: 1200px) {
.weight-basic-info,
.weight-calculated-info {
min-width: 100%;
}
}
.media-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
.media-item {
background-color: #f5f7fa;
border-radius: 8px;
overflow: hidden;
border: 1px solid #ebeef5;
transition: all 0.3s;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.media-label {
padding: 8px 12px;
font-size: 13px;
color: #606266;
background-color: #fafafa;
border-bottom: 1px solid #ebeef5;
text-align: center;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.photo-img {
width: 100%;
height: 160px;
display: block;
cursor: pointer;
}
.media-content {
display: flex;
.photo-img {
width: 50%;
}
}
video {
width: 100%;
height: 160px;
object-fit: contain;
background-color: #000;
}
}
}
.pagination-container {
margin-top: 15px;
display: flex;
justify-content: flex-end;
}
.red {
color: #f56c6c;
font-weight: bold;
}
.empty-box {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
</style>