添加新的需求

This commit is contained in:
xuqiuyun
2025-10-20 17:32:09 +08:00
parent 9979e00b47
commit 361d5ab1ae
247 changed files with 34249 additions and 1 deletions

View File

@@ -0,0 +1,171 @@
<template>
<div>
<base-search :formItemList="formItemList" @search="searchFrom" ref="baseSearchRef" @change="searchChange"> </base-search>
<div class="main-container">
<el-table v-loading="dataListLoading" :data="form1.tableData" style="width: 100%" border>
<el-table-column prop="deliveryNumber" label="运单号" />
<el-table-column prop="licensePlate" label="车牌号" />
<el-table-column label="车身照片" prop="" width="160">
<template #default="scope">
<el-image
style="width: 50px; height: 50px; margin-right: 10px"
:src="scope.row.carFrontPhoto ? scope.row.carFrontPhoto : ''"
fit="cover"
:preview-src-list="[scope.row.carFrontPhoto] ? [scope.row.carFrontPhoto] : []"
preview-teleported
>
<template #error>
<div
style="width: 50px; height: 50px; display: flex; justify-content: center; align-items: center"
class="image-slot"
>
<el-icon style="font-size: 50px"><Picture /></el-icon>
</div>
</template>
</el-image>
<el-image
style="width: 50px; height: 50px; margin-right: 10px"
:src="scope.row.carBehindPhoto ? scope.row.carBehindPhoto : ''"
fit="cover"
:preview-src-list="[scope.row.carBehindPhoto] ? [scope.row.carBehindPhoto] : []"
preview-teleported
>
<template #error>
<div
style="width: 50px; height: 50px; display: flex; justify-content: center; align-items: center"
class="image-slot"
>
<el-icon style="font-size: 50px"><Picture /></el-icon>
</div>
</template>
</el-image>
</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 label="预警类型" prop="warningType">
<template #default="scope">
<el-tag type="warning" v-if="scope.row.warningType == 3">运输距离预警</el-tag>
<el-tag type="danger" v-if="scope.row.warningType == 2">数量盘单预警</el-tag>
</template>
</el-table-column>
<el-table-column prop="warningTime" label="预警时间" />
</el-table>
<pagination v-model:limit="form.pageSize" v-model:page="form.pageNum" :total="form1.total" @pagination="getList" />
</div>
</div>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue';
import baseSearch from '@/components/common/searchCustom/index.vue';
import { warningLogList } from '~/api/hardware.js';
const dataListLoading = ref(false);
const baseSearchRef = ref();
const form = reactive({
pageSize: 10,
pageNum: 1,
// deliveryNumber: '',
// licensePlate: '',
// warningType: '',
// createTime: '',
});
const form1 = reactive({
tableData: [],
total: 0,
});
const searchFrom = () => {
form.pageNum = 1;
getList();
};
const searchChange = (val) => {
console.log('Search change:', val);
// 在这里可以处理搜索条件变化的逻辑
};
const formItemList = reactive([
{
label: '运单号',
type: 'input',
param: 'deliveryNumber',
labelWidth: 65,
span: 7,
},
{
label: '车牌号',
type: 'input',
param: 'licensePlate',
labelWidth: 65,
span: 7,
},
{
label: '创建时间',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
type: 'daterange',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
param: 'myTimes',
span: 7,
},
{
label: '预警类型',
type: 'select',
selectOptions: [
{ value: 2, text: '数量盘单预警' },
{ value: 3, text: '运输距离预警' },
],
param: 'warningType',
span: 7,
},
]);
const getList = () => {
dataListLoading.value = true;
const params = {
...form,
...baseSearchRef.value.penetrateParams(),
};
if (baseSearchRef.value.penetrateParams().myTimes) {
if (baseSearchRef.value.penetrateParams().myTimes && baseSearchRef.value.penetrateParams().myTimes.length > 0) {
params.startTime = baseSearchRef.value.penetrateParams().myTimes[0];
params.endTime = baseSearchRef.value.penetrateParams().myTimes[1];
}
}
warningLogList(params)
.then((ret) => {
form1.tableData = ret.data.rows;
form1.total = ret.data.total;
dataListLoading.value = false;
})
.catch(() => {
dataListLoading.value = false;
});
};
onMounted(() => {
getList();
});
</script>
<style scoped lang="less">
.wrapper {
.search-wrap {
display: flex;
justify-content: space-between;
background-color: #fff;
align-items: flex-end;
padding: 12px 16px 0 16px;
}
.btn-group {
background-color: #fff;
margin: 10px 0;
padding: 10px;
}
}
</style>

View File

@@ -0,0 +1,256 @@
<template>
<div>
<base-search :formItemList="formItemList" @search="searchFrom" ref="baseSearchRef"> </base-search>
<div class="main-container">
<el-table v-loading="dataListLoading" :data="data.rows" border element-loading-text="数据加载中...">
<el-table-column label="运单号" prop="deliveryNumber"></el-table-column>
<el-table-column label="核验状态" prop="statusDesc" width="100">
<template #default="scope">
<el-tag :type="getStatusTagType(scope.row.status)">
{{ scope.row.statusDesc || getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="车牌号" prop="licensePlate"> </el-table-column>
<el-table-column label="车身照片" prop="" width="160">
<template #default="scope">
<el-image
style="width: 50px; height: 50px; margin-right: 10px"
:src="scope.row.carFrontPhoto ? scope.row.carFrontPhoto : ''"
fit="cover"
:preview-src-list="[scope.row.carFrontPhoto] ? [scope.row.carFrontPhoto] : []"
preview-teleported
>
<template #error>
<div
style="width: 50px; height: 50px; display: flex; justify-content: center; align-items: center"
class="image-slot"
>
<el-icon style="font-size: 50px"><Picture /></el-icon>
</div>
</template>
</el-image>
<el-image
style="width: 50px; height: 50px; margin-right: 10px"
:src="scope.row.carBehindPhoto ? scope.row.carBehindPhoto : ''"
fit="cover"
:preview-src-list="[scope.row.carBehindPhoto] ? [scope.row.carBehindPhoto] : []"
preview-teleported
>
<template #error>
<div
style="width: 50px; height: 50px; display: flex; justify-content: center; align-items: center"
class="image-slot"
>
<el-icon style="font-size: 50px"><Picture /></el-icon>
</div>
</template>
</el-image>
</template>
</el-table-column>
<el-table-column label="起始地" prop="startLocation"> </el-table-column>
<el-table-column label="送达目的地" prop="endLocation"> </el-table-column>
<el-table-column label="预计送达时间" prop="estimatedDeliveryTime"> </el-table-column>
<el-table-column label="登记设备数量" prop="registeredJbqCount" width="120">
<template #default="scope">
{{ scope.row.registeredJbqCount || '0' }}
</template>
</el-table-column>
<el-table-column label="车内盘点耳标数量" prop="earTagCount" width="140">
<template #default="scope">
<span v-if="scope.row.earTagCount == scope.row.registeredJbqCount">
{{ scope.row.earTagCount || '0' }}
</span>
<span style="color: red" v-else>
{{ scope.row.earTagCount || '0' }}
</span>
</template>
</el-table-column>
<el-table-column label="预警类型">
<template #default="scope">
<div v-for="(item, index) in scope.row.warningTypeList" :key="index">
<el-tag type="warning" v-if="item == '运输距离预警'" style="margin-top: 10px">运输距离预警</el-tag>
<el-tag type="danger" v-if="item == '数量盘点预警'" style="margin-top: 10px">数量盘点预警</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="司机姓名" prop="driverName"> </el-table-column>
<el-table-column label="创建时间" prop="createTime"></el-table-column>
<el-table-column label="创建人" prop="createByName"></el-table-column>
<el-table-column label="操作" width="194">
<template #default="scope">
<div class="table_column_operation">
<a v-hasPermi="['entry:view']" @click="details(scope.row, scope.row.warningTypeList ? scope.row.warningTypeList.length : 0)">详情</a>
<el-button
type="primary"
link
v-if="scope.row.status == 4 || scope.row.status == 5"
v-hasPermi="['entry:download']"
@click="download(scope.row.zipUrl)"
:loading="downLoading[scope.row.id]"
style="padding: 0"
>下载文件</el-button
>
</div>
</template>
</el-table-column>
<template #empty>
<div class="dataListOnEmpty">暂无数据</div>
</template>
</el-table>
<pagination v-model:limit="form.pageSize" v-model:page="form.pageNum" :total="data.total" @pagination="getDataList" />
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import baseSearch from '@/components/common/searchCustom/index.vue';
import { inspectionList, downloadZip } from '@/api/abroad.js';
const router = useRouter();
const route = useRoute();
const baseSearchRef = ref();
const dataListLoading = ref(false);
const downLoading = reactive({});
const form = reactive({
pageNum: 1,
pageSize: 20,
});
const data = reactive({
rows: [],
total: 20,
});
const formItemList = reactive([
{
label: '运单号',
type: 'input',
param: 'deliveryNumber',
labelWidth: 65,
span: 7,
},
{
label: '车牌号',
type: 'input',
param: 'licensePlate',
labelWidth: 65,
span: 7,
},
{
label: '创建时间',
type: 'daterange',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
param: 'myTimes',
span: 7,
},
{
label: '核验状态',
type: 'select',
selectOptions: [
{ value: 1, text: '待核验' },
{ value: 2, text: '已核验' },
],
param: 'status',
span: 7,
},
]);
const searchFrom = () => {
form.pageNum = 1;
getDataList();
};
const getDataList = () => {
dataListLoading.value = true;
// 安全获取搜索参数
let searchParams = {};
if (baseSearchRef.value && baseSearchRef.value.penetrateParams) {
searchParams = baseSearchRef.value.penetrateParams();
}
const params = {
...form,
...searchParams,
};
params.interfaceType = 2;
// 安全处理时间参数
if (searchParams.myTimes && Array.isArray(searchParams.myTimes) && searchParams.myTimes.length > 0) {
params.startTime = searchParams.myTimes[0];
params.endTime = searchParams.myTimes[1];
}
inspectionList(params)
.then((ret) => {
console.log('入境检疫列表返回结果:', ret);
data.rows = ret.data.rows;
data.total = ret.data.total;
dataListLoading.value = false;
// 调试:检查第一行数据的字段
if (ret.data.rows && ret.data.rows.length > 0) {
const firstRow = ret.data.rows[0];
console.log('入境检疫第一行数据完整字段:', firstRow);
console.log('入境检疫关键字段检查:', {
status: firstRow.status,
statusDesc: firstRow.statusDesc,
registeredJbqCount: firstRow.registeredJbqCount,
earTagCount: firstRow.earTagCount,
driverName: firstRow.driverName,
licensePlate: firstRow.licensePlate
});
}
})
.catch(() => {
dataListLoading.value = false;
});
};
// 详情
const details = (row, length) => {
router.push({
path: '/entry/details',
query: {
id: row.id,
status: row.status,
length,
},
});
};
// 下载文件
const download = (url) => {
window.location.href = url;
};
// 状态文本转换
const getStatusText = (status) => {
const statusMap = {
1: '待装车',
2: '已装车/待资金方付款',
3: '待核验/资金方已付款',
4: '已核验/待买家付款',
5: '买家已付款'
};
return statusMap[status] || '未知状态';
};
// 状态标签类型
const getStatusTagType = (status) => {
const typeMap = {
1: 'warning', // 待装车 - 橙色
2: 'info', // 已装车/待资金方付款 - 蓝色
3: 'warning', // 待核验/资金方已付款 - 橙色
4: 'success', // 已核验/待买家付款 - 绿色
5: 'success' // 买家已付款 - 绿色
};
return typeMap[status] || 'info';
};
onMounted(() => {
getDataList();
});
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,878 @@
<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>
<!-- 正常内容 -->
<section v-else>
<div class="main-container">
<div class="info-box">
<div class="title">基础信息</div>
<el-descriptions :column="4">
<el-descriptions-item label="运单号:">{{ data.baseInfo.deliveryNumber || '-' }}</el-descriptions-item>
<el-descriptions-item label="订单标题:">{{ data.baseInfo.deliveryTitle || '-' }}</el-descriptions-item>
<el-descriptions-item label="资金方:">{{ data.baseInfo.fundName || '-' }}</el-descriptions-item>
<el-descriptions-item label="采购商:">{{ data.baseInfo.buyerName || '-' }}</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.registeredJbqCount || '-' }} </el-descriptions-item>
<el-descriptions-item label="状态:">
<el-tag :type="data.baseInfo.status === 2 ? 'success' : 'warning'">{{ getStatusText(data.baseInfo.status) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="车身照片:">
<span style="vertical-align: top">
<el-image
v-if="data.baseInfo.carFrontPhoto"
style="width: 50px; height: 50px; margin-right: 10px"
:src="data.baseInfo.carFrontPhoto ? data.baseInfo.carFrontPhoto : ''"
fit="cover"
:preview-src-list="[data.baseInfo.carFrontPhoto] ? [data.baseInfo.carFrontPhoto] : []"
preview-teleported
/>
<el-image
v-if="data.baseInfo.carBehindPhoto"
style="width: 50px; height: 50px; margin-right: 10px"
:src="data.baseInfo.carBehindPhoto ? data.baseInfo.carBehindPhoto : ''"
fit="cover"
:preview-src-list="[data.baseInfo.carBehindPhoto] ? [data.baseInfo.carBehindPhoto] : []"
preview-teleported
/>
</span>
</el-descriptions-item>
</el-descriptions>
</div>
<div class="info-box" v-if="data.length != 0">
<div class="title">预警信息</div>
<el-descriptions :column="4">
<template v-for="(item, index) in data.warnInfo" :key="index">
<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="智能耳标数:"> {{ data.baseInfo.registeredJbqCount || '--' }} </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>
</template>
</el-descriptions>
</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="检疫票:">
<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" 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>
</div>
<div class="host-box" v-if="data.serverIds != ''">
<div class="title">智能主机</div>
<el-descriptions :column="1">
<el-descriptions-item label="主机编号:">
{{ data.serverIds }}
<el-button type="primary" style="margin-left: 20px" size="small" @click="locationClick(item)" v-if="data.serverIds"
>查看主机定位</el-button
>
<el-button type="primary" style="margin-left: 20px" size="small" @click="trackClick(data.serverIds)" v-if="data.serverIds"
>查看运动轨迹</el-button
>
</el-descriptions-item>
</el-descriptions>
</div>
<div class="ear-box">
<div class="title">智能项圈</div>
<el-table
:data="data.collarRows"
border
v-loading="data.collarDataListLoading"
element-loading-text="数据加载中..."
style="width: 100%"
>
<el-table-column label="智能项圈编号" prop="sn"></el-table-column>
<el-table-column label="设备电量" prop="battery">
<template #default="scope"> {{ scope.row.battery }}% </template>
</el-table-column>
<el-table-column label="步数" prop="steps">
<template #default="scope"> {{ scope.row.steps }}</template>
</el-table-column>
<el-table-column label="设备温度" prop="temperature">
<template #default="scope"> {{ scope.row.temperature }}/ </template>
</el-table-column>
<el-table-column label="数据最后更新时间" prop="time"></el-table-column>
<el-table-column label="操作" prop="">
<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>
<pagination
v-model:limit="collarForm.pageSize"
v-model:page="collarForm.pageNum"
:total="data.collarTotal"
@pagination="getCollarList"
/>
</div>
<div class="ear-box">
<div class="title">智能耳标</div>
<el-table :data="data.rows" border v-loading="data.dataListLoading" 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 }}% </template>
</el-table-column>
<el-table-column label="步数" prop="walkSteps">
<template #default="scope"> {{ scope.row.walkSteps }}</template>
</el-table-column>
<el-table-column label="设备温度" prop="deviceTemp">
<template #default="scope"> {{ scope.row.deviceTemp }}/ </template>
</el-table-column>
<el-table-column label="数据最后更新时间" prop="updateTime"></el-table-column>
<el-table-column label="操作" prop="">
<template #default="scope">
<el-button link type="primary" @click="earLogClick(scope.row)">日志</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-model:limit="form.pageSize" v-model:page="form.pageNum" :total="data.total" @pagination="getEarList" />
</div>
<!-- <div class="info-box" v-if="data.status == 4 || data.status == 5">
<div class="title">核验信息</div>
<el-descriptions :column="4">
<el-descriptions-item label="到货核验人:">{{ data.baseInfo.checkByName || '' }}</el-descriptions-item>
<el-descriptions-item label="核验时间:">{{ data.baseInfo.checkTime || '' }}</el-descriptions-item>
<el-descriptions-item label="核验空车过磅重量:">{{
data.baseInfo.checkEmptyWeight ? data.baseInfo.checkEmptyWeight + 'kg' : ''
}}</el-descriptions-item>
<el-descriptions-item label="核验满车过磅重量:">{{
data.baseInfo.checkEntruckWeight ? data.baseInfo.checkEntruckWeight + 'kg' : ''
}}</el-descriptions-item>
<el-descriptions-item label="核验空车过磅视频:">
<span style="vertical-align: top" v-if="data.baseInfo.checkEmptyWeightVideo">
<video
style="height: 250px; width: auto; border-radius: 5px; margin-left: 60px"
controls
:src="data.baseInfo.checkEmptyWeightVideo"
/>
</span>
</el-descriptions-item>
<el-descriptions-item label="核验装车过磅视频:">
<span style="vertical-align: top" v-if="data.baseInfo.checkEntruckWeightVideo">
<video
style="height: 250px; width: auto; border-radius: 5px; margin-left: 60px"
controls
:src="data.baseInfo.checkEntruckWeightVideo"
/>
</span>
</el-descriptions-item>
</el-descriptions>
</div> -->
<div class="btn-box">
<el-button @click="cancelClick">返回</el-button>
</div>
</div>
<!-- 查看主机定位 -->
<el-dialog v-model="data.dialogVisible" title="查看定位" style="width: 650px; padding-bottom: 20px">
<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>
<template #footer>
<span class="dialog-footer">
<!-- <el-button @click="cancelClick">取消</el-button> -->
</span>
</template>
</el-dialog>
<!-- 查看轨迹 -->
<el-dialog v-model="data.trackDialogVisible" title="查看运动轨迹" style="width: 650px; padding-bottom: 20px">
<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"
>
<!-- 运行轨迹的路线 stroke-weight边线的宽度stroke-opacity边线透明度-->
<bm-polyline stroke-color="blue" :path="data.path" :stroke-opacity="0.5" :stroke-weight="3" :editing="false"></bm-polyline>
<!-- marker 可以展示的图标 起点终点 -->
<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>
<!-- <bm-marker
v-for="(item, index) in data.path"
:key="index"
:icon="biaoMarkIcon"
:position="{ lng: item.lng, lat: item.lat }"
></bm-marker> -->
<!-- 暂停是根据你提供的坐标如果还没过第一个就会返回如果过了在第一个到第二个之间暂停他会跑掉第二个后面 -->
<!-- <bml-lushu @stop="reset" :path="data.path" :icon="iconss" :play="data.play" :rotation="true" :speed="1000"></bml-lushu> -->
</baidu-map>
</div>
</el-dialog>
<!-- 耳标日志 -->
<el-dialog v-model="data.earLogDialogVisible" title="设备日志" style="width: 900px; padding-bottom: 20px">
<el-table :data="data.earLogRows" border 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 }}% </template>
</el-table-column>
<el-table-column label="步数" prop="walkSteps">
<template #default="scope"> {{ scope.row.walkSteps }}</template>
</el-table-column>
<el-table-column label="设备温度" prop="deviceTemp">
<template #default="scope"> {{ scope.row.deviceTemp }}/ </template>
</el-table-column>
<el-table-column label="数据最后更新时间" prop="updateTime" width="200"></el-table-column>
<el-table-column label="操作" prop="">
<template #default="scope">
<el-button link type="primary" @click="locateClick(scope.row)">定位</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-model:limit="earLogForm.pageSize" v-model:page="earLogForm.pageNum" :total="data.earLogTotal" @pagination="getEarLogList" />
</el-dialog>
<!-- 项圈日志 -->
<el-dialog v-model="data.collarDialogVisible" title="设备日志" style="width: 900px; padding-bottom: 20px">
<el-table :data="data.collarLogRows" border v-loading="data.logListLoading" element-loading-text="数据加载中..." style="width: 100%">
<el-table-column label="智能项圈编号" prop="sn"></el-table-column>
<el-table-column label="设备电量" prop="battery">
<template #default="scope"> {{ scope.row.battery }}% </template>
</el-table-column>
<el-table-column label="步数" prop="steps">
<template #default="scope"> {{ scope.row.steps }}</template>
</el-table-column>
<el-table-column label="设备温度" prop="temperature">
<template #default="scope"> {{ scope.row.temperature }}/ </template>
</el-table-column>
<el-table-column label="数据最后更新时间" prop="time" width="200"></el-table-column>
<el-table-column label="操作" prop="">
<template #default="scope">
<el-button link type="primary" @click="collarlocateClick(scope.row)">定位</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-model:limit="collarLogForm.pageSize"
v-model:page="collarLogForm.pageNum"
:total="data.collarLogTotal"
@pagination="getCollarLogList"
/>
</el-dialog>
<TrackDialog ref="TrackDialogRef" />
</section>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { ElMessage } from 'element-plus';
import { earList, hostLocation, hostTrack, waybillDetail, collarList, collarLogList, earLogList, testDeliveryDevices } from '@/api/abroad.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 TrackDialogRef = ref();
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: '启运时间',
},
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,
earLogTotal: 0,
collarLogRows: [],
collarLogTotal: 0,
deviceId: '', // 耳标编号
sn: '', // 项圈编号
});
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 = () => {
console.log('查询运单详情, deliveryId:', route.query.id);
if (!route.query.id) {
console.warn('=== 警告deliveryId为空跳过运单详情查询');
return;
}
waybillDetail(route.query.id)
.then((res) => {
console.log('运单详情返回结果:', res);
if (res.code === 200) {
data.baseInfo = res.data.delivery ? res.data.delivery : {};
data.warnInfo = res.data.warningLog ? res.data.warningLog : {};
data.serverIds = res.data.serverIds ? res.data.serverIds : [];
console.log('基础信息:', {
driverName: data.baseInfo.driverName,
licensePlate: data.baseInfo.licensePlate,
carFrontPhoto: data.baseInfo.carFrontPhoto,
carBehindPhoto: data.baseInfo.carBehindPhoto,
driverId: data.baseInfo.driverId
});
} else {
ElMessage.error(res.msg);
}
})
.catch(() => {});
};
// 查看主机定位
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;
earList({
pageNum: form.pageNum,
pageSize: form.pageSize,
deliveryId: route.query.id,
})
.then((res) => {
console.log('=== 耳标设备API返回结果:', res);
data.dataListLoading = false;
if (res.code === 200) {
console.log('=== 耳标设备数据:', res.data);
data.rows = res.data.rows || [];
data.total = res.data.total || 0;
console.log('=== 设置后的rows:', data.rows);
console.log('=== 设置后的total:', data.total);
} else {
ElMessage.error(res.msg);
data.total = 0;
}
})
.catch(() => {
data.dataListLoading = false;
});
};
const earLogClick = (row) => {
data.deviceId = row.deviceId;
data.earLogDialogVisible = true;
getEarLogList();
};
// 智能项圈列表查询
const getCollarList = () => {
if (!route.query.id) {
console.warn('=== 警告deliveryId为空跳过项圈列表查询');
data.collarDataListLoading = false;
return;
}
data.collarDataListLoading = true;
collarList({
pageNum: collarForm.pageNum,
pageSize: collarForm.pageSize,
deliveryId: route.query.id,
})
.then((res) => {
console.log('=== 项圈设备API返回结果:', res);
data.collarDataListLoading = false;
if (res.code === 200) {
console.log('=== 项圈设备数据:', res.data);
data.collarRows = res.data.rows || [];
data.collarTotal = res.data.total || 0;
console.log('=== 设置后的collarRows:', data.collarRows);
console.log('=== 设置后的collarTotal:', data.collarTotal);
} else {
ElMessage.error(res.msg);
data.collarTotal = 0;
}
})
.catch(() => {
data.collarDataListLoading = false;
data.collarTotal = 0; // 确保total有默认值
});
};
const collarLogClick = (row) => {
data.sn = row.sn;
data.collarDialogVisible = true;
getCollarLogList();
};
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;
data.dialogVisible = true;
};
//
const collarlocateClick = (row) => {
data.center.lng = row.longitude;
data.center.lat = row.latitude;
data.updateTime = row.time;
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) => {
if (TrackDialogRef.value) {
const info = {
deliveryId: route.query.id,
deviceId: row.sn,
type: 'order',
};
TrackDialogRef.value.onShowTrackDialog(info);
}
};
// 状态文本转换
const getStatusText = (status) => {
const statusMap = {
1: '待装车',
2: '装车中',
3: '运输中',
4: '已送达',
5: '已完成'
};
return statusMap[status] || '未知状态';
};
onMounted(() => {
data.id = route.query.id;
data.status = route.query.status;
data.length = route.query.length;
console.log('=== 详情页面初始化deliveryId:', route.query.id);
console.log('=== 路由参数:', route.query);
// 检查deliveryId是否存在
if (!route.query.id) {
console.warn('=== 警告deliveryId为空无法加载详情页面');
ElMessage.error('缺少必要的参数,请从列表页面点击详情按钮进入');
return;
}
// 检查deliveryId是否存在存在时才测试设备关联情况
testDeliveryDevices({ deliveryId: route.query.id })
.then(res => {
console.log('=== 测试设备关联结果:', res);
})
.catch(err => {
console.error('=== 测试设备关联失败:', err);
});
getDetail(); // 查详情
getEarList(); // 耳标列表查询
getCollarList(); // 项圈类别查询
});
</script>
<style lang="less" scoped>
.error-container {
padding: 50px;
text-align: center;
}
.info-box {
margin-top: 10px;
}
.title {
font-weight: bold;
margin-bottom: 10px;
}
.quarantine-text {
margin-top: 20px;
}
:deep(.my-label) {
display: inline-block;
width: 120px;
text-align: right;
word-break: break-all;
}
.label {
width: 140px;
font-size: 14px;
}
.btn-box {
display: flex;
align-items: center;
justify-content: center;
}
::v-deep .anchorBL {
display: none;
visibility: hidden;
}
.img-box {
margin-bottom: 10px;
display: flex;
.img-label {
width: 120px;
font-size: 14px;
margin-right: 16px;
text-align: right;
}
}
.empty-box {
display: flex;
justify-content: center;
align-items: center;
}
.red {
color: #ff6332;
}
</style>

View File

@@ -0,0 +1,131 @@
<template>
<div class="wrapper">
<el-form :inline="true" class="search-wrap" :model="form" ref="formRef">
<div>
<el-form-item style="width: 280px" prop="sn" label="项圈编号:">
<el-input placeholder="请输入项圈编号" clearable v-model="form.sn"></el-input>
</el-form-item>
<el-form-item class="inline-form-item" prop="startNo" label="号段范围:">
<el-input placeholder="请输入开始号段" v-model="form.startNo" clearable style="width: 160px" />
-
<el-input placeholder="请输入结束号段" v-model="form.endNo" style="width: 160px" clearable />
</el-form-item>
</div>
<div style="min-width: 200px">
<el-form-item>
<el-button @click="resetClick(formRef)">重置</el-button>
<el-button type="primary" @click="searchClick">查询</el-button>
</el-form-item>
</div>
</el-form>
<div class="main-container" style="margin-top: 10px">
<el-table :data="form.tableData" style="width: 100%" border>
<el-table-column label="项圈编号" prop="sn" />
<el-table-column label="设备电量" prop="battery">
<template #default="scope"> {{ scope.row.battery }}% </template>
</el-table-column>
<el-table-column label="设备温度" prop="temperature">
<template #default="scope"> {{ scope.row.temperature }}°C</template>
</el-table-column>
<el-table-column prop="delivery_number" label="所属运单号" />
<el-table-column prop="license_plate" label="车牌号" />
<el-table-column prop="uptime" label="绑定时间" />
<el-table-column label="操作" width="180">
<template #default="scope">
<el-button link type="primary" @click="showLocationDialog(scope.row)">定位</el-button>
<el-button link type="primary" @click="showTrackDialog(scope.row)">运动轨迹</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-model:limit="form.pageSize" v-model:page="form.pageNum" :total="form.total" @pagination="getList" />
<LocationDialog ref="LocationDialogRef" />
<TrackDialog ref="TrackDialogRef" />
</div>
</div>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue';
import { collarList } from '~/api/hardware.js';
import LocationDialog from './locationDialog.vue';
import TrackDialog from './trackDialog.vue';
const LocationDialogRef = ref();
const TrackDialogRef = ref();
const formRef = ref();
const form = reactive({
tableData: [],
pageSize: 10,
pageNum: 1,
total: 0,
sn: '',
startNo: '',
endNo: '',
});
const getList = async () => {
const { pageSize, pageNum, sn, startNo, endNo } = form;
const params = {
pageSize,
pageNum,
sn,
startNo,
endNo,
};
const res = await collarList(params);
const { data = {}, code } = res;
const { rows = [], total = 0 } = data;
if (code === 200) {
form.tableData = rows;
form.total = total;
}
};
const searchClick = async () => {
form.pageNum = 1;
await getList();
};
const resetClick = async (el) => {
form.pageNum = 1;
form.endNo = '';
form.startNo = '';
el.resetFields();
await getList();
};
// 查看定位
const showLocationDialog = (row) => {
if (LocationDialogRef.value) {
const normalized = {
// 兼容后端返回字段命名
deliveryId: row.deliveryId || row.delivery_id || '',
deviceId: row.deviceId || row.sn || '',
};
LocationDialogRef.value.onShowLocationDialog(normalized);
}
};
// 查看轨迹轨迹
const showTrackDialog = (row) => {
if (TrackDialogRef.value) {
TrackDialogRef.value.onShowTrackDialog(row);
}
};
onMounted(() => {
getList();
});
</script>
<style scoped lang="less">
.wrapper {
.search-wrap {
display: flex;
justify-content: space-between;
background-color: #fff;
align-items: flex-end;
padding: 12px 16px 0 16px;
}
.btn-group {
background-color: #fff;
margin: 10px 0;
padding: 10px;
}
}
</style>

View File

@@ -0,0 +1,104 @@
<template>
<div class="wrapper">
<el-form :inline="true" class="search-wrap" :model="form" ref="formRef">
<div>
<el-form-item style="width: 280px" prop="deviceId" label="耳标编号:">
<el-input placeholder="请输入耳标编号" clearable v-model="form.deviceId"></el-input>
</el-form-item>
<el-form-item class="inline-form-item" prop="startNo" label="号段范围:">
<el-input placeholder="请输入开始号段" v-model="form.startNo" clearable style="width: 160px" />
-
<el-input placeholder="请输入结束号段" v-model="form.endNo" style="width: 160px" clearable />
</el-form-item>
</div>
<div style="min-width: 200px">
<el-form-item>
<el-button @click="resetClick(formRef)">重置</el-button>
<el-button type="primary" @click="searchClick">查询</el-button>
</el-form-item>
</div>
</el-form>
<div class="main-container" style="margin-top: 10px">
<el-table :data="form.tableData" style="width: 100%" border>
<el-table-column prop="deviceId" label="耳标编号" />
<el-table-column label="设备电量" prop="deviceVoltage">
<template #default="scope"> {{ scope.row.deviceVoltage }}% </template>
</el-table-column>
<el-table-column label="设备温度" prop="deviceTemp">
<template #default="scope"> {{ scope.row.deviceTemp }}°C</template>
</el-table-column>
<el-table-column prop="deliveryNumber" label="所属运单号" />
<el-table-column prop="licensePlate" label="车牌号" />
<el-table-column prop="deliveryCreateTime" label="绑定时间" />
</el-table>
<pagination v-model:limit="form.pageSize" v-model:page="form.pageNum" :total="form.total" @pagination="getList" />
</div>
</div>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue';
import { jbqClientList } from '~/api/hardware.js';
const formRef = ref();
const form = reactive({
tableData: [],
pageSize: 10,
pageNum: 1,
total: 0,
deviceId: '',
startNo: '',
endNo: '',
});
const getList = async () => {
const { pageSize, pageNum, deviceId, startNo, endNo } = form;
const params = {
pageSize,
pageNum,
deviceId,
startNo,
endNo,
};
const res = await jbqClientList(params);
const { data = {}, code } = res;
const { rows = [], total = 0 } = data;
if (code === 200) {
form.tableData = rows;
form.total = total;
}
};
const searchClick = async () => {
form.pageNum = 1;
await getList();
console.log('searchClick');
};
const resetClick = async (el) => {
form.pageNum = 1;
form.endNo = '';
form.startNo = '';
el.resetFields();
await getList();
};
onMounted(() => {
getList();
});
</script>
<style scoped lang="less">
.wrapper {
.search-wrap {
display: flex;
justify-content: space-between;
background-color: #fff;
align-items: flex-end;
padding: 12px 16px 0 16px;
}
.btn-group {
background-color: #fff;
margin: 10px 0;
padding: 10px;
}
}
</style>

View File

@@ -0,0 +1,94 @@
<template>
<div class="wrapper">
<el-form :inline="true" class="search-wrap" :model="form" ref="formRef">
<div>
<el-form-item style="width: 280px" prop="deviceId" label="主机编号:">
<el-input placeholder="请输入主机编号" clearable v-model="form.deviceId"></el-input>
</el-form-item>
</div>
<div style="min-width: 200px">
<el-form-item>
<el-button @click="resetClick(formRef)">重置</el-button>
<el-button type="primary" @click="searchClick">查询</el-button>
</el-form-item>
</div>
</el-form>
<div class="main-container" style="margin-top: 10px">
<el-table :data="form.tableData" style="width: 100%" border>
<el-table-column prop="deviceId" label="主机编号" />
<el-table-column label="设备电量" prop="deviceVoltage">
<template #default="scope"> {{ scope.row.deviceVoltage }}% </template>
</el-table-column>
<el-table-column label="设备温度" prop="deviceTemp">
<template #default="scope"> {{ scope.row.deviceTemp }}°C</template>
</el-table-column>
<el-table-column prop="onlineStatusDesc" label="联网状态" />
<el-table-column prop="deliveryNumber" label="所属运单号" />
<el-table-column prop="licensePlate" label="车牌号" />
<el-table-column prop="deliveryCreateTime" label="绑定时间" />
</el-table>
<pagination v-model:limit="form.pageSize" v-model:page="form.pageNum" :total="form.total" @pagination="getList" />
</div>
</div>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue';
import { jbqServerList } from '~/api/hardware.js';
const formRef = ref();
const form = reactive({
tableData: [],
pageSize: 10,
pageNum: 1,
total: 0,
deviceId: '',
});
const getList = async () => {
const { pageSize, pageNum, deviceId } = form;
const params = {
pageSize,
pageNum,
deviceId,
};
const res = await jbqServerList(params);
const { data = {}, code } = res;
const { rows = [], total = 0 } = data;
if (code === 200) {
form.tableData = rows;
form.total = total;
}
};
const searchClick = async () => {
form.pageNum = 1;
await getList();
console.log('searchClick');
};
const resetClick = async (el) => {
form.pageNum = 1;
el.resetFields();
await getList();
};
onMounted(() => {
getList();
});
</script>
<style scoped lang="less">
.wrapper {
.search-wrap {
display: flex;
justify-content: space-between;
background-color: #fff;
align-items: flex-end;
padding: 12px 16px 0 16px;
}
.btn-group {
background-color: #fff;
margin: 10px 0;
padding: 10px;
}
}
</style>

View File

@@ -0,0 +1,103 @@
<template>
<el-dialog title="查看定位" v-model="data.dialogVisible" :before-close="handleClose" style="width: 700px; padding-bottom: 20px">
<div
v-loading="data.loactionLoading"
element-loading-text="正在加载中..."
element-loading-background="rgba(255, 255, 255,1)"
style="height: 500px"
>
<div class="empty-box" v-if="data.loactionStatus">
<img style="width: 50%" src="../../assets/images/wudingwei.png" />
</div>
<baidu-map style="height: 500px" class="map" :zoom="15" :center="data.center" :scroll-wheel-zoom="true" v-if="data.mapShow">
<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.time"
:labelStyle="{
color: '#67c23a',
fontSize: '12px',
borderColor: '#fff',
borderRadius: 10,
}"
:offset="{ width: -96, height: 10 }"
/>
</bm-marker>
</baidu-map>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { collarLocation } from '~/api/hardware.js';
const data = reactive({
dialogVisible: false,
loactionLoading: false,
loactionStatus: false,
mapShow: false,
center: { lng: 0, lat: 0 },
time: '',
deliveryId: '',
xqDeviceId: '',
});
// 查询定位
const getLocation = () => {
data.loactionLoading = true;
collarLocation({
deliveryId: data.deliveryId,
xqDeviceId: data.xqDeviceId,
})
.then((res) => {
data.loactionLoading = false;
if (res.code === 200) {
data.mapShow = true;
data.center.lng = res.data.longitude;
data.center.lat = res.data.latitude;
data.time = `最后定位时间:${res.data.updateTime}`;
} else {
ElMessage.error(res.msg);
data.loactionStatus = true;
}
})
.catch(() => {
data.loactionLoading = false;
data.loactionStatus = true;
});
};
const handleClose = () => {
data.dialogVisible = false;
};
const onShowLocationDialog = (row) => {
data.dialogVisible = true;
if (row) {
data.deliveryId = row.deliveryId;
data.xqDeviceId = row.deviceId;
data.mapShow = false;
data.loactionStatus = false;
getLocation();
}
};
defineExpose({
onShowLocationDialog,
});
</script>
<style lang="less" scoped>
::v-deep .anchorBL {
display: none;
visibility: hidden;
}
.empty-box {
display: flex;
align-items: center;
justify-content: center;
height: 500px;
}
</style>

View File

@@ -0,0 +1,268 @@
<template>
<el-dialog title="查看运动轨迹" v-model="data.dialogVisible" :before-close="handleClose" style="width: 700px; padding-bottom: 20px">
<el-form ref="formDataRef" :inline="true" :model="formData" class="demo-form-inline">
<el-form-item label="日期">
<!-- <el-date-picker
v-model="formData.time"
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
placeholder="请选择日期"
:disabled-date="disabledDate"
></el-date-picker> -->
<el-date-picker
v-model="formData.time"
type="datetimerange"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择日期"
:disabled-date="disabledDate"
@change="dateChange"
></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getPath">查询</el-button>
<el-button type="warning" @click="playPoints" :disabled="!data.mapShow">{{ data.play ? '暂停' : '播放' }}</el-button>
</el-form-item>
</el-form>
<div
v-loading="data.trackLoading"
element-loading-text="正在加载中..."
style="height: 500px"
element-loading-background="rgba(255, 255, 255,1)"
>
<div class="empty-box" v-if="data.noTrack">
<img style="width: 50%" src="../../assets/images/wuguiji.png" />
</div>
<baidu-map
class="map"
@ready="handler"
:center="data.centerPoint"
:zoom="data.zoom"
:dragging="true"
:auto-resize="true"
:scroll-wheel-zoom="true"
style="height: 500px"
v-if="data.mapShow"
>
<!-- 运行轨迹的路线 stroke-weight边线的宽度stroke-opacity边线透明度-->
<bm-polyline stroke-color="blue" :path="data.path" :stroke-opacity="0.5" :stroke-weight="3" :editing="false"></bm-polyline>
<!-- marker 可以展示的图标 起点终点 -->
<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>
<bm-marker
v-for="(item, index) in data.path"
:key="index"
:icon="biaoMarkIcon"
:position="{ lng: item.lng, lat: item.lat }"
></bm-marker>
<!-- 暂停是根据你提供的坐标如果还没过第一个就会返回如果过了在第一个到第二个之间暂停他会跑掉第二个后面 -->
<bml-lushu @stop="reset" :path="data.path" :icon="iconss" :play="data.play" :rotation="true" :speed="900"></bml-lushu>
</baidu-map>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { BmlLushu } from 'vue-baidu-map-3x';
import { collarTrack, collarTrackOrder } from '~/api/hardware.js';
import startIcon from '../../assets/images/qi.png';
import endIcon from '../../assets/images/zhong.png';
import biaoIcon from '../../assets/images/biaozhu.png';
import goIcon from '../../assets/images/yuan.png';
const formDataRef = ref(null);
const data = reactive({
dialogVisible: false,
play: false, // 是否自动播放轨迹动画
zoom: 15,
path: [],
centerPoint: { lng: 116.404, lat: 39.915 },
startMark: { lng: 116.404, lat: 39.915 },
endMark: { lng: 116.404, lat: 116.404 },
trackLoading: false,
noTrack: false,
mapShow: false,
type: '',
});
const formData = reactive({
time: '',
});
const handleClose = () => {
data.dialogVisible = false;
};
// 查询当前时间
const getNowDate = () => {
const year = new Date().getFullYear();
let month = new Date().getMonth() + 1;
let day = new Date().getDate();
let hours = new Date().getHours();
let minutes = new Date().getMinutes();
let seconds = new Date().getSeconds();
month = month < 10 ? `0${month}` : month;
day = day < 10 ? `0${day}` : day;
hours = hours < 10 ? `0${hours}` : hours;
minutes = minutes < 10 ? `0${minutes}` : minutes;
seconds = seconds < 10 ? `0${seconds}` : seconds;
// formData.time = year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds;
formData.time = [`${year}-${month}-${day} ` + `00` + `:` + `00` + `:` + `00`, `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`];
};
// 禁用今天之后的日期
const disabledDate = (time) => {
return time.getTime() > Date.now();
};
const dateChange = () => {
if (!formData.time) {
formData.time = '';
}
};
// 起点图标
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 iconss = reactive({
url: goIcon,
size: { width: 18, height: 18 },
opts: { anchor: { width: 9, height: 9 } },
});
const biaoMarkIcon = reactive({
url: biaoIcon,
size: { width: 32, height: 32 },
opts: { anchor: { width: 15, height: 32 } },
});
const handler = ({ BMap, map }) => {
// 自动获取展示的比例
const view = map.getViewport(eval(data.path));
data.zoom = view.zoom;
data.centerPoint = view.center;
};
// 查询
const getPath = () => {
data.mapShow = false;
data.noTrack = false;
getTrack();
};
// 播放/暂停 运行轨迹
const playPoints = () => {
if (data.noTrack) {
ElMessage.warning('暂无定位轨迹');
return;
}
data.play = !data.play;
};
// 停止播放运行轨迹
const reset = () => {
data.play = false;
};
// 查询定位
const getTrack = () => {
data.trackLoading = true;
if (data.type == 'order') {
collarTrack({
deliveryId: data.deliveryId,
xqDeviceId: data.xqDeviceId,
trackTime: formData.time[0] ? formData.time[0] : '',
trackEndTime: formData.time[1] ? formData.time[1] : '',
})
.then((res) => {
data.trackLoading = false;
if (res.code === 200) {
data.mapShow = true;
if (res.data.length > 0) {
data.path = [];
res.data.forEach((item) => {
data.path.push({
lng: item.longitude,
lat: item.latitude,
});
});
data.startMark = data.path[0]; // 起点
data.endMark = data.path[data.path.length - 1]; // 终点
}
} else {
ElMessage.error(res.msg);
data.noTrack = true;
}
})
.catch(() => {
data.trackLoading = false;
data.noTrack = true;
});
} else {
collarTrackOrder({
deliveryId: data.deliveryId,
xqDeviceId: data.xqDeviceId,
trackTime: formData.time[0] ? formData.time[0] : '',
trackEndTime: formData.time[1] ? formData.time[1] : '',
})
.then((res) => {
data.trackLoading = false;
if (res.code === 200) {
data.mapShow = true;
if (res.data.length > 0) {
data.path = [];
res.data.forEach((item) => {
data.path.push({
lng: item.longitude,
lat: item.latitude,
});
});
data.startMark = data.path[0]; // 起点
data.endMark = data.path[data.path.length - 1]; // 终点
}
} else {
ElMessage.error(res.msg);
data.noTrack = true;
}
})
.catch(() => {
data.trackLoading = false;
data.noTrack = true;
});
}
};
const onShowTrackDialog = (row) => {
data.dialogVisible = true;
getNowDate();
if (row) {
data.deliveryId = row.deliveryId;
data.xqDeviceId = row.deviceId;
data.type = row.type ? row.type : '';
data.mapShow = false;
data.noTrack = false;
data.formData = '';
getTrack();
}
};
defineExpose({
onShowTrackDialog,
});
</script>
<style lang="less" scoped>
::v-deep .anchorBL {
display: none;
visibility: hidden;
}
.empty-box {
display: flex;
align-items: center;
justify-content: center;
height: 500px;
}
</style>

View File

@@ -0,0 +1,4 @@
<template>
<div>后续添加首页内容</div>
</template>
<script setup></script>

View File

@@ -0,0 +1,381 @@
<template>
<section class="h-full flex items-center login-bg">
<div class="login-container">
<div class="pig-tit">
<p>牛只运输跟踪系统</p>
</div>
<div class="pig-container">
<el-form ref="formRef" :model="form" :rules="rules">
<div class="con-tab" @click="toggleLoginMode">
<span :class="{ cur: form.loginType === 0 }" @click="changeLoginType(0)">用户名密码登录</span>
<!-- <em style="font-style: normal;">|</em> -->
<span :class="{ cur: form.loginType === 1 }" @click="changeLoginType(1)">手机验证码登录</span>
<em :class="{ cur: form.loginType === 1 }"></em>
</div>
<template v-if="form.loginType === 0">
<p class="info-title">手机号</p>
<el-form-item prop="mobile">
<el-input v-model="form.mobile" class="no-border-input" placeholder="请输入手机号" @keyup.enter="onSubmit" />
</el-form-item>
<p class="info-title">密码</p>
<el-form-item prop="password">
<el-input
placeholder="请输入密码"
v-model="form.password"
show-password
type="password"
@keyup.enter="onSubmit"
></el-input>
</el-form-item>
</template>
<template v-if="form.loginType === 1">
<p class="info-title">手机号</p>
<el-form-item prop="mobile">
<el-input v-model="form.mobile" class="no-border-input" placeholder="请输入手机号" @keyup.enter="onSubmit" />
</el-form-item>
<p class="info-title">验证码</p>
<el-form-item prop="password">
<p class="info-yan" v-loading="getSmsCodeLoading" @click="getSmsCode">{{ title }}</p>
<el-input v-model="form.password" class="no-border-input info-fa" placeholder="请输入验证码" @keyup.enter="onSubmit">
</el-input>
</el-form-item>
</template>
<el-form-item class="login_Submitbtn">
<el-button :loading="loading" class="login_btn" type="primary" @click="onSubmit">登录</el-button>
</el-form-item>
</el-form>
</div>
</div>
</section>
</template>
<script setup>
import { ElMessage } from 'element-plus';
import { reactive, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { login, getSmsCodeByPhone } from '~/api/sys.js';
import { useUserStore } from '~/store/user';
import { setToken } from '@/utils/auth';
import { checkMobile } from '@/utils/validateFuns.js';
import { getUserMenu } from '@/api/sys.js';
const router = useRouter();
const route = useRoute();
const userStore = useUserStore();
const title = ref('获取验证码');
const time = ref(0);
const getSmsCodeLoading = ref(false);
const form = reactive({
mobile: '',
password: '',
loginType: 0,
});
const loading = ref(false);
const formRef = ref();
// 表单校验
const rules = {
mobile: [
{
required: true,
validator: (rule, value, callback) => {
if (form.loginType === 0) {
if (!value) {
callback(new Error('登录用户名不能为空'));
}
callback();
} else {
if (!checkMobile(form.mobile)) {
callback(new Error('请输入正确的手机号码'));
}
callback();
}
callback();
},
trigger: 'blur',
},
],
password: [
{
required: true,
validator(rule, value, callback) {
if (form.loginType === 0) {
if (!value) {
callback(new Error('登录密码不能为空'));
}
callback();
} else {
if (!value || value.length !== 6) {
callback(new Error('短信验证码输入有误'));
}
callback();
}
callback();
},
trigger: 'blur',
},
],
};
const resetForm = () => {
form.mobile = '';
form.password = '';
form.loginType = 0;
formRef.value.clearValidate();
};
const changeLoginType = (type) => {
resetForm();
form.loginType = type;
};
// eslint-disable-next-line consistent-return
const getSmsCode = () => {
if (getSmsCodeLoading.value) return false;
if (time.value > 0) return false;
if (!checkMobile(form.mobile)) {
ElMessage.error('请输入正确的手机号码');
formRef.value.clearValidate();
return false;
}
getSmsCodeLoading.value = true;
title.value = '';
getSmsCodeByPhone(form.mobile)
.then(() => {
title.value = `60s`;
getSmsCodeLoading.value = false;
time.value = 59;
const timer = setInterval(() => {
if (time.value > 0) {
title.value = `${time.value}s`;
// eslint-disable-next-line no-plusplus
time.value--;
} else {
clearInterval(timer);
title.value = '获取验证码';
}
}, 1000);
})
.catch(() => {
getSmsCodeLoading.value = false;
title.value = '获取验证码';
});
};
const onSubmit = async (val) => {
formRef.value.validate((valid) => {
if (valid) {
loading.value = true;
const params = { ...form };
login(params)
.then((ret) => {
// 写入用户登录信息
userStore.updateToken(ret.data.token);
userStore.updateUserName(ret.data.mobile);
userStore.updateLoginUser(ret.data.username);
userStore.updateLoginUserType(ret.data.userType);
userStore.updateRoleId(ret.data.roleId);
userStore.updatePermissions(ret.data.permissions || []); // 保存权限列表
userStore.updateRoles(ret.data.roles || []); // 保存角色列表
setToken(ret.data.token);
let redirect = route.query.redirect || '/';
if (redirect.indexOf('login') > -1) {
redirect = '/';
}
// router.push({ path: redirect }); 此方法弃用,因为企业端和海关端没有相同系统模块,这样会导致页面空白
ElMessage.success('登录成功!');
// 更新为以下跳转方法 userType=1为监管端userType=2为企业端根据不同端来跳转不同页面
// if (ret.data.userType == 1) {
// router.push({ path: '/system/post' });
// } else {
// router.push({ path: '/personnel/role' });
// }
loading.value = false;
generateRoutes();
})
.catch((error) => {
loading.value = false;
});
}
});
};
const generateRoutes = async () => {
try {
const ret = await getUserMenu();
console.log('=== 获取用户菜单 ===', ret.data);
// 检查用户权限
const userStore = useUserStore();
const isSuperAdmin = userStore.permissions.includes('*:*:*') || userStore.roles.includes('admin');
console.log('=== 用户权限检查 ===', {
permissions: userStore.permissions,
roles: userStore.roles,
isSuperAdmin: isSuperAdmin
});
// 查找第一个有pageUrl的菜单项type=1表示菜单
const findFirstMenuWithUrl = (menus) => {
// 按sort排序确保按顺序查找
const sortedMenus = menus.sort((a, b) => (a.sort || 0) - (b.sort || 0));
for (const menu of sortedMenus) {
// 查找type=1菜单且有pageUrl的项目
if (menu.type === 1 && menu.pageUrl) {
console.log('=== 找到第一个菜单页面 ===', menu);
return menu;
}
}
return null;
};
const firstMenu = findFirstMenuWithUrl(ret.data);
// 根据用户类型和权限决定跳转页面
let targetPath = '/';
if (isSuperAdmin) {
// 超级管理员优先跳转到系统管理页面
targetPath = '/system/post';
console.log('=== 超级管理员,跳转到系统管理页面 ===', targetPath);
} else if (firstMenu && firstMenu.pageUrl) {
// 普通用户跳转到第一个有权限的菜单页面
targetPath = firstMenu.pageUrl;
console.log('=== 普通用户,跳转到第一个菜单页面 ===', targetPath);
} else {
// 默认跳转到装车订单页面
targetPath = '/shipping/loadingOrder';
console.log('=== 没有找到有效菜单,跳转到默认页面 ===', targetPath);
}
// 执行跳转
try {
await router.push({ path: targetPath });
console.log('=== 成功跳转到目标页面 ===', targetPath);
} catch (error) {
console.warn('Failed to navigate to', targetPath, 'error:', error);
// 如果跳转失败,尝试跳转到首页
try {
await router.push({ path: '/' });
console.log('=== 跳转到首页 ===');
} catch (homeError) {
console.error('Failed to navigate to home:', homeError);
}
}
} catch (error) {
console.error('Failed to get user menu:', error);
// 获取菜单失败时跳转到首页
try {
await router.push({ path: '/' });
console.log('=== 获取菜单失败,跳转到首页 ===');
} catch (navError) {
console.error('Failed to navigate to home:', navError);
}
}
};
</script>
<style scoped lang="scss">
.login-bg {
background: url(../assets/login-bg.png) center no-repeat;
background-size: cover;
height: 100vh;
width: 100%;
position: relative;
}
.login-container {
position: absolute;
top: 50%;
transform: translate(0%, -50%);
right: 15%;
width: 550px;
// height: 400px;
}
.pig-container {
padding: 40px 56px 40px;
border-radius: 9px;
background-color: rgb(255, 255, 255);
opacity: 0.98;
box-shadow: 0px 8px 24px 0px rgba(104, 114, 124, 0.35);
}
.pig-tit {
font-weight: bolder;
font-size: 45px;
text-align: center;
color: #3b74ff;
margin-bottom: 30px;
}
.con-tab {
// font-weight: 600;
font-size: 24px;
color: #999;
line-height: 20px;
padding: 10px 0;
margin-bottom: 40px;
span {
display: inline-block;
cursor: pointer;
&:first-child {
padding-right: 20px;
margin-right: 20px;
// border-right: #ccc 2px solid;
}
}
span.cur {
color: #000;
}
.info {
font-weight: 300;
font-size: 25px;
margin-top: 20px;
}
}
.info-title {
font-size: 18px;
color: #000;
line-height: 28px;
margin-bottom: 4px;
}
.login-container .el-input {
width: 500px;
height: 60px;
line-height: 60px;
border-radius: 12px;
margin-bottom: 8px;
}
.info-fa {
position: relative;
}
.info-yan {
position: absolute;
z-index: 9;
top: 9px;
right: 10px;
width: 120px;
height: 42px;
text-align: center;
background: #3b74ff;
border: 1px solid #dbf1ec;
border-radius: 11px;
font-size: 16px;
color: #fff;
cursor: pointer;
}
.login_Submitbtn {
margin-top: 36px;
}
.login-container .login_btn {
width: 500px;
height: 60px;
line-height: 60px;
text-align: center;
font-size: 18px;
color: #fff;
border-radius: 5px;
background: #3b74ff;
}
:deep(.el-input .el-input__icon) {
color: #fff;
}
</style>

View File

@@ -0,0 +1,379 @@
<template>
<div class="permission-container">
<el-row :gutter="20">
<!-- 左侧用户列表 -->
<el-col :span="8">
<el-card class="role-card">
<template #header>
<div class="card-header">
<span class="card-title">用户列表</span>
</div>
</template>
<el-table
:data="paginatedRoleList"
highlight-current-row
@current-change="handleRoleChange"
v-loading="roleLoading"
style="width: 100%"
max-height="500"
>
<el-table-column prop="name" label="用户名称" width="120" />
<el-table-column prop="mobile" label="手机号" />
</el-table>
<!-- 分页器 -->
<div style="margin-top: 15px; text-align: center">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="roleList.length"
layout="total, sizes, prev, pager, next"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
</el-col>
<!-- 右侧菜单权限分配 -->
<el-col :span="16">
<el-card class="permission-card">
<template #header>
<div class="card-header">
<span class="card-title">
{{ currentRole ? `${currentRole.name} - 菜单访问权限` : '请选择角色' }}
</span>
<el-button
type="primary"
size="small"
@click="handleSaveMenuPermissions"
:disabled="!currentRole"
:loading="saveLoading"
>
保存菜单权限
</el-button>
<el-button
type="success"
size="small"
@click="handleQuickAssignAll"
:disabled="!currentRole"
:loading="quickAssignLoading"
style="margin-left: 10px"
>
一键分配全部权限
</el-button>
</div>
</template>
<div v-if="currentRole" v-loading="permissionLoading">
<el-alert
title="提示"
type="info"
:closable="false"
style="margin-bottom: 20px"
>
勾选菜单和按钮后该用户登录系统时可以访问这些菜单页面和执行相应的操作
</el-alert>
<el-tree
ref="menuTreeRef"
:data="menuTree"
show-checkbox
node-key="id"
:default-expand-all="true"
:props="treeProps"
:check-strictly="false"
:default-checked-keys="checkedMenuIds"
>
<template #default="{ node, data }">
<span class="tree-node">
<el-icon v-if="data.icon" style="margin-right: 5px">
<component :is="data.icon" />
</el-icon>
<span>{{ node.label }}</span>
<el-tag
v-if="data.type === 1"
type="success"
size="small"
style="margin-left: 10px"
>
菜单
</el-tag>
<el-tag
v-if="data.type === 2"
type="warning"
size="small"
style="margin-left: 10px"
>
按钮
</el-tag>
<span
v-if="data.authority"
style="margin-left: 10px; color: #999; font-size: 12px"
>
{{ data.authority }}
</span>
</span>
</template>
</el-tree>
</div>
<el-empty
v-else
description="请从左侧选择一个角色,为其分配菜单访问权限"
:image-size="100"
/>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, nextTick, computed } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import {
getUserList,
getMenuTree,
getRoleMenuIds,
assignRoleMenus,
getMenuList,
} from '@/api/permission.js';
// 角色相关数据
const roleLoading = ref(false);
const roleList = ref([]);
const currentRole = ref(null);
// 分页数据
const pagination = reactive({
currentPage: 1,
pageSize: 10,
});
// 计算分页后的用户列表
const paginatedRoleList = computed(() => {
const start = (pagination.currentPage - 1) * pagination.pageSize;
const end = start + pagination.pageSize;
return roleList.value.slice(start, end);
});
// 权限相关数据
const permissionLoading = ref(false);
const saveLoading = ref(false);
const quickAssignLoading = ref(false);
const menuTree = ref([]);
const menuTreeRef = ref(null);
const checkedMenuIds = ref([]);
const treeProps = {
children: 'children',
label: 'label',
};
// 初始化
onMounted(() => {
loadRoleList();
});
// 加载用户列表
const loadRoleList = async () => {
roleLoading.value = true;
try {
const res = await getUserList();
if (res.code === 200) {
roleList.value = res.data || [];
}
} catch (error) {
console.error('加载用户列表失败:', error);
ElMessage.error('加载用户列表失败');
} finally {
roleLoading.value = false;
}
};
// 角色选择改变
const handleRoleChange = async (row) => {
if (!row) return;
currentRole.value = row;
await loadMenuTree();
await loadRoleMenus(row.roleId);
};
// 加载菜单树(显示所有菜单,包括菜单和按钮)
const loadMenuTree = async () => {
permissionLoading.value = true;
try {
const res = await getMenuTree();
if (res.code === 200) {
// 显示所有菜单和按钮,不进行过滤
menuTree.value = res.data || [];
}
} catch (error) {
console.error('加载菜单树失败:', error);
ElMessage.error('加载菜单树失败');
} finally {
permissionLoading.value = false;
}
};
// 加载角色已分配的菜单
const loadRoleMenus = async (roleId) => {
try {
const res = await getRoleMenuIds(roleId);
if (res.code === 200) {
checkedMenuIds.value = res.data || [];
await nextTick();
if (menuTreeRef.value) {
menuTreeRef.value.setCheckedKeys(checkedMenuIds.value);
}
}
} catch (error) {
console.error('加载角色菜单失败:', error);
ElMessage.error('加载角色菜单失败');
}
};
// 分页处理
const handleSizeChange = (size) => {
pagination.pageSize = size;
pagination.currentPage = 1;
};
const handleCurrentChange = (page) => {
pagination.currentPage = page;
};
// 保存菜单权限
const handleSaveMenuPermissions = async () => {
if (!currentRole.value) {
ElMessage.warning('请先选择用户');
return;
}
// 获取选中的节点(包括半选中的父节点)
const checkedKeys = menuTreeRef.value.getCheckedKeys();
const halfCheckedKeys = menuTreeRef.value.getHalfCheckedKeys();
const allKeys = [...checkedKeys, ...halfCheckedKeys];
saveLoading.value = true;
try {
const res = await assignRoleMenus({
roleId: currentRole.value.roleId,
menuIds: allKeys,
});
if (res.code === 200) {
ElMessage.success('菜单权限保存成功');
} else {
ElMessage.error(res.msg || '保存失败');
}
} catch (error) {
console.error('保存菜单权限失败:', error);
ElMessage.error('保存失败');
} finally {
saveLoading.value = false;
}
};
// 一键分配全部权限
const handleQuickAssignAll = async () => {
if (!currentRole.value) {
ElMessage.warning('请先选择用户');
return;
}
try {
// 确认操作
const confirmed = await ElMessageBox.confirm(
`确定要为用户 ${currentRole.value.name} (${currentRole.value.mobile}) 分配所有菜单权限吗?`,
'确认分配权限',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
);
if (!confirmed) {
return;
}
quickAssignLoading.value = true;
// 获取所有菜单
const menuListRes = await getMenuList();
if (menuListRes.code !== 200) {
throw new Error('获取菜单列表失败');
}
const allMenus = menuListRes.data || [];
const allMenuIds = allMenus.map(menu => menu.id);
console.log('=== 一键分配全部权限 ===', {
user: currentRole.value,
totalMenus: allMenus.length,
menuIds: allMenuIds
});
// 分配所有菜单权限
const res = await assignRoleMenus({
roleId: currentRole.value.roleId,
menuIds: allMenuIds,
});
if (res.code === 200) {
ElMessage.success(`成功为用户 ${currentRole.value.name} 分配了 ${allMenuIds.length} 个菜单权限`);
// 重新加载权限显示
await loadRoleMenus(currentRole.value.roleId);
} else {
ElMessage.error(res.msg || '分配失败');
}
} catch (error) {
if (error !== 'cancel') {
console.error('一键分配权限失败:', error);
ElMessage.error(`分配失败: ${error.message || error}`);
}
} finally {
quickAssignLoading.value = false;
}
};
</script>
<style scoped>
.permission-container {
padding: 20px;
}
.role-card,
.permission-card {
border-radius: 8px;
min-height: 600px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-title {
font-size: 16px;
font-weight: bold;
}
.tree-node {
display: flex;
align-items: center;
flex: 1;
}
:deep(.el-tree-node__content) {
height: 36px;
}
</style>

View File

@@ -0,0 +1,339 @@
<template>
<div class="operation-permission-container">
<el-row :gutter="20">
<!-- 左侧用户列表 -->
<el-col :span="8">
<el-card class="role-card">
<template #header>
<div class="card-header">
<span class="card-title">用户列表</span>
</div>
</template>
<el-table
:data="paginatedRoleList"
highlight-current-row
@current-change="handleRoleChange"
v-loading="roleLoading"
style="width: 100%"
max-height="500"
>
<el-table-column prop="name" label="用户名称" width="120" />
<el-table-column prop="mobile" label="手机号" />
</el-table>
<!-- 分页器 -->
<div style="margin-top: 15px; text-align: center">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="roleList.length"
layout="total, sizes, prev, pager, next"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
</el-col>
<!-- 右侧操作权限分配 -->
<el-col :span="16">
<el-card class="permission-card">
<template #header>
<div class="card-header">
<span class="card-title">
{{ currentRole ? `${currentRole.name} - 操作权限分配` : '请选择角色' }}
</span>
<el-button
type="primary"
size="small"
v-hasPermi="['permission:operation:assign']"
@click="handleSavePermissions"
:disabled="!currentRole"
:loading="saveLoading"
>
保存操作权限
</el-button>
</div>
</template>
<div v-if="currentRole" v-loading="permissionLoading">
<el-alert
title="提示"
type="info"
:closable="false"
style="margin-bottom: 20px"
>
勾选操作权限后该角色可以执行相应的按钮操作新增编辑删除等
</el-alert>
<el-tree
ref="permissionTreeRef"
:data="permissionTree"
show-checkbox
node-key="id"
:default-expand-all="true"
:props="treeProps"
:check-strictly="true"
>
<template #default="{ node, data }">
<span class="tree-node">
<el-icon v-if="data.icon" style="margin-right: 5px">
<component :is="data.icon" />
</el-icon>
<span>{{ node.label }}</span>
<el-tag
v-if="data.type === 1"
type="info"
size="small"
style="margin-left: 10px"
>
菜单
</el-tag>
<el-tag
v-if="data.type === 2"
type="warning"
size="small"
style="margin-left: 10px"
>
按钮
</el-tag>
<span
v-if="data.authority"
style="margin-left: 10px; color: #999; font-size: 12px"
>
{{ data.authority }}
</span>
</span>
</template>
</el-tree>
</div>
<el-empty
v-else
description="请从左侧选择一个角色,为其分配操作权限"
:image-size="100"
/>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, nextTick, computed } from 'vue';
import { ElMessage } from 'element-plus';
import {
getUserList,
getMenuTree,
getRoleMenuIds,
assignRoleMenus,
} from '@/api/permission.js';
// 用户相关数据
const roleLoading = ref(false);
const roleList = ref([]);
const currentRole = ref(null);
// 分页数据
const pagination = reactive({
currentPage: 1,
pageSize: 10,
});
// 计算分页后的用户列表
const paginatedRoleList = computed(() => {
const start = (pagination.currentPage - 1) * pagination.pageSize;
const end = start + pagination.pageSize;
return roleList.value.slice(start, end);
});
// 权限相关数据
const permissionLoading = ref(false);
const saveLoading = ref(false);
const permissionTree = ref([]);
const permissionTreeRef = ref(null);
const checkedPermissionIds = ref([]);
const treeProps = {
children: 'children',
label: 'label',
};
// 初始化
onMounted(() => {
loadRoleList();
});
// 加载用户列表
const loadRoleList = async () => {
roleLoading.value = true;
try {
const res = await getUserList();
if (res.code === 200) {
roleList.value = res.data || [];
}
} catch (error) {
console.error('加载用户列表失败:', error);
ElMessage.error('加载用户列表失败');
} finally {
roleLoading.value = false;
}
};
// 角色选择改变
const handleRoleChange = async (row) => {
if (!row) return;
console.log('=== 用户选择改变 ===');
console.log('选择的用户:', row);
console.log('用户roleId:', row.roleId);
currentRole.value = row;
await loadPermissionTree();
await loadRolePermissions(row.roleId);
};
// 分页处理
const handleSizeChange = (size) => {
pagination.pageSize = size;
pagination.currentPage = 1;
};
const handleCurrentChange = (page) => {
pagination.currentPage = page;
};
// 加载权限树(包含所有菜单和按钮)
const loadPermissionTree = async () => {
permissionLoading.value = true;
try {
const res = await getMenuTree();
if (res.code === 200) {
permissionTree.value = res.data;
}
} catch (error) {
console.error('加载权限树失败:', error);
ElMessage.error('加载权限树失败');
} finally {
permissionLoading.value = false;
}
};
// 加载角色已分配的权限
const loadRolePermissions = async (roleId) => {
console.log('=== 加载角色权限 ===');
console.log('roleId:', roleId);
try {
const res = await getRoleMenuIds(roleId);
console.log('权限API响应:', res);
if (res.code === 200) {
checkedPermissionIds.value = res.data || [];
console.log('已分配的权限IDs:', checkedPermissionIds.value);
await nextTick();
if (permissionTreeRef.value) {
permissionTreeRef.value.setCheckedKeys(checkedPermissionIds.value);
console.log('权限树已设置选中状态');
// 验证权限树的实际选中状态
setTimeout(() => {
const actualCheckedKeys = permissionTreeRef.value.getCheckedKeys();
const actualHalfCheckedKeys = permissionTreeRef.value.getHalfCheckedKeys();
console.log('=== 权限树实际选中状态验证 ===');
console.log('实际选中的权限IDs:', actualCheckedKeys);
console.log('实际半选中的权限IDs:', actualHalfCheckedKeys);
console.log('期望的权限IDs:', checkedPermissionIds.value);
console.log('选中状态是否一致:', JSON.stringify(actualCheckedKeys.sort()) === JSON.stringify(checkedPermissionIds.value.sort()));
}, 100);
}
} else {
console.error('权限API返回错误:', res);
}
} catch (error) {
console.error('加载角色权限失败:', error);
ElMessage.error('加载角色权限失败');
}
};
// 保存操作权限
const handleSavePermissions = async () => {
if (!currentRole.value) {
ElMessage.warning('请先选择用户');
return;
}
console.log('=== 保存操作权限 ===');
console.log('当前用户:', currentRole.value);
console.log('用户roleId:', currentRole.value.roleId);
// 获取选中的节点(包括半选中的父节点)
const checkedKeys = permissionTreeRef.value.getCheckedKeys();
const halfCheckedKeys = permissionTreeRef.value.getHalfCheckedKeys();
const allKeys = [...checkedKeys, ...halfCheckedKeys];
console.log('选中的权限IDs:', checkedKeys);
console.log('半选中的权限IDs:', halfCheckedKeys);
console.log('所有权限IDs:', allKeys);
const saveData = {
roleId: currentRole.value.roleId,
menuIds: allKeys,
};
console.log('保存数据:', saveData);
saveLoading.value = true;
try {
const res = await assignRoleMenus(saveData);
console.log('保存API响应:', res);
if (res.code === 200) {
ElMessage.success('操作权限保存成功');
console.log('权限保存成功');
} else {
ElMessage.error(res.msg || '保存失败');
console.error('权限保存失败:', res);
}
} catch (error) {
console.error('保存操作权限失败:', error);
ElMessage.error('保存失败');
} finally {
saveLoading.value = false;
}
};
</script>
<style scoped>
.operation-permission-container {
padding: 20px;
}
.role-card,
.permission-card {
border-radius: 8px;
min-height: 600px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-title {
font-size: 16px;
font-weight: bold;
}
.tree-node {
display: flex;
align-items: center;
flex: 1;
}
:deep(.el-tree-node__content) {
height: 36px;
}
</style>

View File

@@ -0,0 +1,315 @@
<template>
<el-dialog v-model="data.dialogVisible" title="分配设备" style="width: 800px; padding-bottom: 20px">
<div class="top-box">
<div class="top-item">
<div class="label">装车数量</div>
<el-input v-model="data.ratedQuantity" style="max-width: 300px" disabled> <template #append></template></el-input>
</div>
<div class="top-item">
<div class="label1">本次已选择装车设备</div>
<el-input v-model="data.selectTotal" style="max-width: 300px" disabled> <template #append></template></el-input>
</div>
</div>
<div class="search-box">
<el-select v-model="data.deviceType" placeholder="选择设备类型" style="width: 150px; margin-right: 10px;" @change="onDeviceTypeChange">
<el-option label="全部设备" value="" />
<el-option label="智能耳标" value="2" />
<el-option label="智能项圈" value="3" />
</el-select>
<el-input v-model="data.deviceId" placeholder="请输入设备编号" style="width: 200px" />
<div class="search-right">
<el-button @click="resetClick">重置</el-button>
<el-button type="primary" @click="searchClick">搜索</el-button>
</div>
</div>
<el-table
ref="multipleTableRef"
v-loading="data.dataListLoading"
:data="data.rows"
border
element-loading-text="数据加载中..."
@selection-change="handleSelectionChange"
row-key="deviceId"
>
<el-table-column type="selection" width="55" reserve-selection />
<el-table-column label="设备编号" prop="deviceId"></el-table-column>
<el-table-column label="设备类型" prop="deviceType">
<template #default="scope">
<el-tag v-if="scope.row.deviceType === 2" type="success">智能耳标</el-tag>
<el-tag v-else-if="scope.row.deviceType === 3" type="primary">智能项圈</el-tag>
<el-tag v-else type="info">未知类型</el-tag>
</template>
</el-table-column>
<el-table-column label="设备状态" prop="status">
<template #default>
<el-tag type="warning">未分配</el-tag>
</template>
</el-table-column>
</el-table>
<pagination v-model:limit="form.pageSize" v-model:page="form.pageNum" :total="data.total" @pagination="getDataList" />
<template #footer>
<span class="dialog-footer">
<el-button :loading="data.saveLoading" type="primary" @click="onClickSave">保存</el-button>
<el-button @click="handleClose">取消</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { deviceList, deviceAssign } from '@/api/shipping.js';
import request from '@/utils/request.js';
const emits = defineEmits();
const multipleTableRef = ref(null);
const data = reactive({
dialogVisible: false,
deliveryId: '',
dataListLoading: false,
rows: [],
total: 0,
ratedQuantity: 0, // 装车数量
selectTotal: 0,
deviceId: '',
deviceType: '', // 设备类型2=智能耳标3=智能项圈
deviceIds: [],
licensePlate: '', // 车牌号
});
const form = reactive({
pageNum: 1,
pageSize: 10,
});
// 设备类型变化
const onDeviceTypeChange = () => {
form.pageNum = 1;
getDataList();
};
// 重置
const resetClick = () => {
data.deviceId = '';
data.deviceType = '';
form.pageNum = 1;
getDataList();
};
// 搜索
const searchClick = () => {
form.pageNum = 1;
getDataList();
};
const getDataList = async () => {
data.dataListLoading = true;
const allDevices = [];
try {
console.log('开始查询设备列表...');
// 查询智能耳标(如果选择了全部设备或智能耳标)
if (!data.deviceType || data.deviceType === '2') {
const earTagParams = {
pageNum: form.pageNum,
pageSize: form.pageSize,
deviceId: data.deviceId || '',
};
console.log('查询智能耳标参数:', earTagParams);
try {
const earTagRes = await request({
url: '/jbqClient/list',
method: 'POST',
data: earTagParams,
});
console.log('智能耳标查询结果:', earTagRes);
if (earTagRes.code === 200 && earTagRes.data && earTagRes.data.rows) {
// 过滤出未分配的设备
const unassignedEarTags = earTagRes.data.rows.filter(item => {
return !item.deliveryNumber ||
item.deliveryNumber === '' ||
item.deliveryNumber === '未分配' ||
item.deliveryNumber === <><CEB4><EFBFBD><EFBFBD>';
});
console.log('智能耳标原始数量:', earTagRes.data.rows.length);
console.log('智能耳标未分配数量:', unassignedEarTags.length);
unassignedEarTags.forEach(item => {
allDevices.push({
...item,
deviceType: 2,
deviceTypeName: '智能耳标',
deliveryNumber: '未分配',
licensePlate: '未分配'
});
});
}
} catch (error) {
console.error('智能耳标查询失败:', error);
}
}
// 查询智能项圈(如果选择了全部设备或智能项圈)
if (!data.deviceType || data.deviceType === '3') {
const collarParams = {
pageNum: form.pageNum,
pageSize: form.pageSize,
sn: data.deviceId || '',
};
console.log('查询智能项圈参数:', collarParams);
try {
const collarRes = await request({
url: '/xqClient/list',
method: 'POST',
data: collarParams,
});
console.log('智能项圈查询结果:', collarRes);
if (collarRes.code === 200 && collarRes.data && collarRes.data.rows) {
// 过滤出未分配的设备
const unassignedCollars = collarRes.data.rows.filter(item => {
return !item.delivery_number ||
item.delivery_number === '' ||
item.delivery_number === '未分配' ||
item.delivery_number === <><CEB4><EFBFBD><EFBFBD>';
});
console.log('智能项圈原始数量:', collarRes.data.rows.length);
console.log('智能项圈未分配数量:', unassignedCollars.length);
unassignedCollars.forEach(item => {
allDevices.push({
...item,
deviceId: item.sn || item.deviceId, // 优先使用sn字段
deviceType: 3,
deviceTypeName: '智能项圈',
deliveryNumber: '未分配',
licensePlate: '未分配'
});
});
}
} catch (error) {
console.error('智能项圈查询失败:', error);
}
}
// 去重处理确保deviceId唯一
const uniqueRows = [];
const deviceIdSet = new Set();
allDevices.forEach(item => {
if (!deviceIdSet.has(item.deviceId)) {
deviceIdSet.add(item.deviceId);
uniqueRows.push(item);
}
});
data.rows = uniqueRows;
data.total = uniqueRows.length;
data.dataListLoading = false;
console.log('最终未分配设备列表:', uniqueRows);
console.log('总设备数量:', uniqueRows.length);
} catch (error) {
console.error('查询设备列表失败:', error);
data.dataListLoading = false;
data.rows = [];
data.total = 0;
}
};
// 勾选
const handleSelectionChange = (val) => {
data.selectTotal = val.length;
data.deviceIds = val.map((item) => {
return {
deviceId: item.deviceId,
deviceTypeId: item.deviceType, // 使用deviceType字段
};
});
};
// 保存按钮
const onClickSave = () => {
if (data.deviceIds.length == 0) {
ElMessage.error('请选择设备编号');
return;
}
const params = {
deliveryId: data.deliveryId,
deviceIds: data.deviceIds,
licensePlate: data.licensePlate, // 添加车牌号
};
data.saveLoading = true;
deviceAssign(params)
.then((res) => {
data.saveLoading = false;
if (res.code === 200) {
ElMessage({
message: res.msg,
type: 'success',
});
emits('success');
data.dialogVisible = false;
if (multipleTableRef.value) {
multipleTableRef.value.clearSelection();
}
data.deviceIds = [];
data.deliveryId = '';
data.ratedQuantity = '';
data.licensePlate = '';
} else {
ElMessage.error(res.msg);
}
})
.catch((err) => {
data.saveLoading = false;
});
};
const handleClose = () => {
data.dialogVisible = false;
};
const onShowAssignDialog = (row) => {
data.dialogVisible = true;
if (row) {
data.deliveryId = row.id;
data.ratedQuantity = row.ratedQuantity ? row.ratedQuantity : 0;
data.licensePlate = row.licensePlate || ''; // 获取车牌号
getDataList();
if (multipleTableRef.value) {
multipleTableRef.value.clearSelection();
}
}
};
defineExpose({
onShowAssignDialog,
});
</script>
<style lang="less" scoped>
.top-box {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
.top-item {
display: flex;
align-items: center;
.label {
width: 75px;
margin-right: 10px;
}
.label1 {
width: 140px;
margin-right: 10px;
}
}
}
.search-box {
margin-bottom: 20px;
display: flex;
justify-content: space-between;
}
</style>

View File

@@ -0,0 +1,356 @@
<template>
<el-dialog
v-model="dialogVisible"
title="新增运送清单"
width="800px"
:close-on-click-modal="false"
@close="handleClose"
>
<el-form
ref="formRef"
:model="formData"
:rules="rules"
label-width="120px"
>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="发货方" prop="shipper">
<el-input v-model="formData.shipper" placeholder="请输入发货方" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="采购方" prop="buyer">
<el-input v-model="formData.buyer" placeholder="请输入采购方" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="车牌号" prop="plateNumber">
<el-input v-model="formData.plateNumber" placeholder="如京A12345" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="司机姓名" prop="driverName">
<el-input v-model="formData.driverName" placeholder="请输入司机姓名" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="司机电话" prop="driverPhone">
<el-input v-model="formData.driverPhone" placeholder="请输入司机电话" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="主机设备" prop="serverId">
<el-select
v-model="formData.serverId"
placeholder="请选择主机设备"
clearable
filterable
style="width: 100%"
>
<el-option
v-for="item in serverList"
:key="item.id"
:label="item.deviceNo || item.deviceId"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="耳标设备" prop="eartagIds">
<el-select
v-model="formData.eartagIds"
placeholder="请选择耳标设备"
multiple
clearable
filterable
style="width: 100%"
>
<el-option
v-for="item in eartagList"
:key="item.id"
:label="item.deviceNo || item.deviceId"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项圈设备" prop="collarIds">
<el-select
v-model="formData.collarIds"
placeholder="请选择项圈设备"
multiple
clearable
filterable
style="width: 100%"
>
<el-option
v-for="item in collarList"
:key="item.id"
:label="item.deviceNo || item.deviceId"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="预计出发时间" prop="estimatedDepartureTime">
<el-date-picker
v-model="formData.estimatedDepartureTime"
type="datetime"
placeholder="请选择预计出发时间"
style="width: 100%"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="预计到达时间" prop="estimatedArrivalTime">
<el-date-picker
v-model="formData.estimatedArrivalTime"
type="datetime"
placeholder="请选择预计到达时间"
style="width: 100%"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="起点地址" prop="startLocation">
<el-input v-model="formData.startLocation" placeholder="请输入起点地址" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="目的地地址" prop="endLocation">
<el-input v-model="formData.endLocation" placeholder="请输入目的地地址" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="牛只数量" prop="cattleCount">
<el-input-number
v-model="formData.cattleCount"
:min="1"
:max="9999"
placeholder="请输入牛只数量"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="预估重量(kg)" prop="estimatedWeight">
<el-input-number
v-model="formData.estimatedWeight"
:min="0.01"
:precision="2"
placeholder="请输入预估重量"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="检疫证号" prop="quarantineCertNo">
<el-input v-model="formData.quarantineCertNo" placeholder="请输入检疫证号(可选)" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注" prop="remark">
<el-input
v-model="formData.remark"
type="textarea"
:rows="3"
maxlength="500"
show-word-limit
placeholder="请输入备注信息(可选)"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
提交
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive } from 'vue';
import { ElMessage } from 'element-plus';
import { createDelivery, getAvailableServers, getAvailableEartags, getAvailableCollars } from '@/api/shipping.js';
const dialogVisible = ref(false);
const formRef = ref(null);
const submitLoading = ref(false);
const serverList = ref([]);
const eartagList = ref([]);
const collarList = ref([]);
const formData = reactive({
shipper: '',
buyer: '',
plateNumber: '',
driverName: '',
driverPhone: '',
serverId: null,
eartagIds: [],
collarIds: [],
estimatedDepartureTime: '',
estimatedArrivalTime: '',
startLocation: '',
endLocation: '',
cattleCount: 1,
estimatedWeight: null,
quarantineCertNo: '',
remark: '',
});
// 车牌号校验
const validatePlateNumber = (rule, value, callback) => {
const plateReg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-Z0-9]{5}[A-Z0-9挂学警港澳]$/;
if (!value) {
callback(new Error('请输入车牌号'));
} else if (!plateReg.test(value)) {
callback(new Error('车牌号格式不正确'));
} else {
callback();
}
};
// 手机号校验
const validatePhone = (rule, value, callback) => {
const phoneReg = /^1[3-9]\d{9}$/;
if (!value) {
callback(new Error('请输入司机电话'));
} else if (!phoneReg.test(value)) {
callback(new Error('手机号格式不正确'));
} else {
callback();
}
};
// 时间校验
const validateArrivalTime = (rule, value, callback) => {
if (!value) {
callback(new Error('请选择预计到达时间'));
} else if (formData.estimatedDepartureTime && value <= formData.estimatedDepartureTime) {
callback(new Error('预计到达时间必须晚于出发时间'));
} else {
callback();
}
};
const rules = {
shipper: [{ required: true, message: '请输入发货方', trigger: 'blur' }],
buyer: [{ required: true, message: '请输入采购方', trigger: 'blur' }],
plateNumber: [{ required: true, validator: validatePlateNumber, trigger: 'blur' }],
driverName: [{ required: true, message: '请输入司机姓名', trigger: 'blur' }],
driverPhone: [{ required: true, validator: validatePhone, trigger: 'blur' }],
estimatedDepartureTime: [{ required: true, message: '请选择预计出发时间', trigger: 'change' }],
estimatedArrivalTime: [{ required: true, validator: validateArrivalTime, trigger: 'change' }],
startLocation: [{ required: true, message: '请输入起点地址', trigger: 'blur' }],
endLocation: [{ required: true, message: '请输入目的地地址', trigger: 'blur' }],
cattleCount: [{ required: true, message: '请输入牛只数量', trigger: 'blur' }],
estimatedWeight: [{ required: true, message: '请输入预估重量', trigger: 'blur' }],
};
// 打开弹窗
const open = () => {
dialogVisible.value = true;
loadDeviceOptions();
};
// 加载设备选项
const loadDeviceOptions = async () => {
try {
// 加载主机设备
const serverRes = await getAvailableServers({ pageNum: 1, pageSize: 9999 });
if (serverRes.code === 200) {
serverList.value = serverRes.data?.rows || serverRes.data || [];
}
// 加载耳标设备
const eartagRes = await getAvailableEartags({ pageNum: 1, pageSize: 9999 });
if (eartagRes.code === 200) {
eartagList.value = eartagRes.data?.rows || eartagRes.data || [];
}
// 加载项圈设备
const collarRes = await getAvailableCollars({ pageNum: 1, pageSize: 9999 });
if (collarRes.code === 200) {
collarList.value = collarRes.data?.rows || collarRes.data || [];
}
} catch (error) {
console.error('加载设备列表失败', error);
}
};
// 提交表单
const handleSubmit = () => {
formRef.value.validate(async (valid) => {
if (valid) {
submitLoading.value = true;
try {
const res = await createDelivery(formData);
if (res.code === 200) {
ElMessage.success('创建成功');
dialogVisible.value = false;
emit('success');
} else {
ElMessage.error(res.msg || '创建失败');
}
} catch (error) {
ElMessage.error('创建失败,请稍后重试');
} finally {
submitLoading.value = false;
}
}
});
};
// 关闭弹窗
const handleClose = () => {
formRef.value?.resetFields();
dialogVisible.value = false;
};
// 暴露方法给父组件
defineExpose({
open,
});
const emit = defineEmits(['success']);
</script>
<style scoped>
.dialog-footer {
display: flex;
justify-content: flex-end;
}
</style>

View File

@@ -0,0 +1,181 @@
<template>
<el-dialog v-model="data.dialogVisible" title="详情" style="width: 1000px; padding-bottom: 20px">
<el-row :gutter="30" class="el-row">
<el-col :span="12"
><div class="label">订单标题</div>
<div class="info">{{ data.info.deliveryTitle || '--' }}</div></el-col
>
<el-col :span="12"
><div class="label">装车数量</div>
<div class="info">{{ data.info.ratedQuantity || '0' }}</div></el-col
>
</el-row>
<el-row :gutter="30">
<el-col :span="12"
><div class="label">供应商</div>
<div class="info">{{ data.info.supplierName || data.info.supplierId || '--' }}</div></el-col
>
<el-col :span="12"
><div class="label">资金方</div>
<div class="info">{{ data.info.fundName || data.info.fundId || '--' }}</div></el-col
>
</el-row>
<el-row :gutter="30">
<el-col :span="12"
><div class="label">司机</div>
<div class="info">{{ data.info.driverName || '--' }}</div></el-col
>
<el-col :span="12"
><div class="label">采购商</div>
<div class="info">{{ data.info.buyerName || data.info.buyerId || '--' }}</div></el-col
>
</el-row>
<el-row :gutter="30">
<el-col :span="12"
><div class="label">采购单价</div>
<div class="info">{{ data.info.buyerPrice || '--' }}/公斤</div></el-col
>
<el-col :span="12"
><div class="label">销售单价</div>
<div class="info">{{ data.info.salePrice || '--' }}/公斤</div></el-col
>
</el-row>
<el-row :gutter="30">
<el-col :span="12"
><div class="label">约定单价</div>
<div class="info">{{ data.info.firmPrice || '--' }}/公斤</div></el-col
>
</el-row>
<el-row :gutter="30">
<el-col :span="12"
><div class="label">起始地</div>
<div class="info">{{ data.info.startLocation || '--' }}</div></el-col
>
<el-col :span="12"
><div class="label">目的地</div>
<div class="info">{{ data.info.endLocation || '--' }}</div></el-col
>
</el-row>
<!-- 新增更多字段 -->
<el-row :gutter="30">
<el-col :span="12"
><div class="label">装车订单编号</div>
<div class="info">{{ data.info.deliveryNumber || '--' }}</div></el-col
>
<el-col :span="12"
><div class="label">车牌号</div>
<div class="info">{{ data.info.licensePlate || '--' }}</div></el-col
>
</el-row>
<el-row :gutter="30">
<el-col :span="12"
><div class="label">司机手机</div>
<div class="info">{{ data.info.driverMobile || '--' }}</div></el-col
>
<el-col :span="12"
><div class="label">预计送达时间</div>
<div class="info">{{ data.info.estimatedDeliveryTime || '--' }}</div></el-col
>
</el-row>
<el-row :gutter="30">
<el-col :span="12"
><div class="label">已注册设备数量</div>
<div class="info">{{ data.info.registeredJbqCount || '0' }}</div></el-col
>
<el-col :span="12"
><div class="label">订单状态</div>
<div class="info">{{ getStatusText(data.info.status) }}</div></el-col
>
</el-row>
<el-row :gutter="30">
<el-col :span="12"
><div class="label">创建人</div>
<div class="info">{{ data.info.createByName || '--' }}</div></el-col
>
<el-col :span="12"
><div class="label">创建时间</div>
<div class="info">{{ data.info.createTime || '--' }}</div></el-col
>
</el-row>
<!-- <el-row :gutter="30">
<el-col :span="12"
><div class="label">服务费比例</div>
<div class="info">0.11%</div></el-col
>
</el-row> -->
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { orderDetail } from '@/api/shipping.js';
const data = reactive({
dialogVisible: false,
deliveryId: '',
info: {},
});
// 查详情
const getDetail = () => {
orderDetail(data.deliveryId).then((res) => {
if (res.code === 200) {
// 后端返回的数据结构是 {delivery: {...}, warningLog: [...]}
data.info = res.data.delivery || res.data;
} else {
ElMessage.error(res.msg);
}
});
};
const handleClose = () => {
data.dialogVisible = false;
};
const onShowDetailDialog = (row) => {
data.dialogVisible = true;
if (row) {
data.deliveryId = row.id;
getDetail();
}
};
// 状态文本转换
const getStatusText = (status) => {
const statusMap = {
1: '待装车',
2: '装车中',
3: '运输中',
4: '已送达',
5: '已完成'
};
return statusMap[status] || '未知状态';
};
defineExpose({
onShowDetailDialog,
});
</script>
<style lang="less" scoped>
.el-row {
margin-bottom: 20px;
}
.el-col {
display: flex;
}
.label {
width: 90px;
text-align: right;
}
.info {
flex: 1;
}
</style>

View File

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

View File

@@ -0,0 +1,624 @@
<template>
<el-dialog
v-model="data.dialogVisible"
title="装车"
:before-close="handleClose"
style="width: 850px; padding-bottom: 20px"
:close-on-click-modal="false"
>
<el-form ref="formDataRef" :model="ruleForm" :rules="rules" label-width="auto" v-loading="data.dialogLoading" element-loading-text="上传中">
<el-row>
<el-col :span="12">
<el-form-item label="预计送达时间" prop="estimatedDeliveryTime">
<el-date-picker
v-model="ruleForm.estimatedDeliveryTime"
type="datetime"
placeholder="请选择预计送达时间"
value-format="YYYY-MM-DD HH:mm:ss"
format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="智能主机" prop="serverDeviceSn">
<el-select
v-model="ruleForm.serverDeviceSn"
clearable
filterable
remote
:remote-method="hostRemoteMethod"
:loading="data.hostLoading"
placeholder="请选择智能主机"
>
<el-option v-for="item in data.hostOptions" :key="item.id" :label="item.deviceId" :value="item.deviceId"></el-option>
<el-pagination
style="padding: 0px 20px"
@current-change="hostHandleCurrentChange"
:page-size="10"
:current-page="data.hostPageNum"
layout="total, prev, pager, next"
:total="data.hostTotal"
>
</el-pagination>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="智能耳标编号">
<el-table :data="data.deliveryDevices" border style="width: 100%">
<el-table-column prop="deviceId" label="设备编号"></el-table-column>
<el-table-column prop="bindWeight" label="体重">
<template #default="scope">
<el-input v-model="scope.row.bindWeight" placeholder="请输入体重" clearable>
<template #append>kg</template>
</el-input>
</template>
</el-table-column>
</el-table>
</el-form-item>
<el-form-item label="智能项圈编号">
<el-table :data="data.xqDevices" border style="width: 100%">
<el-table-column prop="deviceId" label="设备编号"></el-table-column>
<el-table-column prop="bindWeight" label="体重">
<template #default="scope">
<el-input v-model="scope.row.bindWeight" placeholder="请输入体重" clearable>
<template #append>kg</template>
</el-input>
</template>
</el-table-column>
</el-table>
</el-form-item>
<el-row>
<el-col :span="12">
<el-form-item label="空车过磅重量" prop="emptyWeight">
<el-input v-model="ruleForm.emptyWeight" placeholder="请输入空车过磅重量" clearable>
<template #append>kg</template></el-input
>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="装车过磅重量" prop="entruckWeight">
<el-input v-model="ruleForm.entruckWeight" placeholder="请输入装车过磅重量" clearable>
<template #append>kg</template></el-input
>
</el-form-item>
</el-col>
</el-row>
<!-- 照片上传区域 -->
<el-divider content-position="left">
<el-icon><Picture /></el-icon>
<span style="margin-left: 8px; font-weight: bold; color: #409EFF;">照片上传</span>
</el-divider>
<el-row>
<el-col :span="12">
<el-form-item label="检疫票" prop="quarantineTickeyUrl">
<el-upload
class="avatar-uploader"
action="/api/common/upload"
:show-file-list="false"
:on-success="jyHandleAvatarSuccess"
:headers="importHeaders"
>
<img v-if="ruleForm.quarantineTickeyUrl" :src="ruleForm.quarantineTickeyUrl" class="avatar" />
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="传纸质磅单(双章)" prop="poundListImg">
<el-upload
class="avatar-uploader"
action="/api/common/upload"
:show-file-list="false"
:on-success="szHandleAvatarSuccess"
:headers="importHeaders"
>
<img v-if="ruleForm.poundListImg" :src="ruleForm.poundListImg" class="avatar" />
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="车辆空磅上磅车头照片" prop="emptyVehicleFrontPhoto">
<el-upload
class="avatar-uploader"
action="/api/common/upload"
:show-file-list="false"
:on-success="emptyVehicleFrontPhotoSuccess"
:headers="importHeaders"
>
<img v-if="ruleForm.emptyVehicleFrontPhoto" :src="ruleForm.emptyVehicleFrontPhoto" class="avatar" />
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="车辆过重磅车头照片" prop="loadedVehicleFrontPhoto">
<el-upload
class="avatar-uploader"
action="/api/common/upload"
:show-file-list="false"
:on-success="loadedVehicleFrontPhotoSuccess"
:headers="importHeaders"
>
<img v-if="ruleForm.loadedVehicleFrontPhoto" :src="ruleForm.loadedVehicleFrontPhoto" class="avatar" />
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="车辆重磅照片" prop="loadedVehicleWeightPhoto">
<el-upload
class="avatar-uploader"
action="/api/common/upload"
:show-file-list="false"
:on-success="loadedVehicleWeightPhotoSuccess"
:headers="importHeaders"
>
<img v-if="ruleForm.loadedVehicleWeightPhoto" :src="ruleForm.loadedVehicleWeightPhoto" class="avatar" />
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="驾驶员手持身份证站车头照片" prop="driverIdCardPhoto">
<el-upload
class="avatar-uploader"
action="/api/common/upload"
:show-file-list="false"
:on-success="driverIdCardPhotoSuccess"
:headers="importHeaders"
>
<img v-if="ruleForm.driverIdCardPhoto" :src="ruleForm.driverIdCardPhoto" class="avatar" />
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
</el-form-item>
</el-col>
</el-row>
<!-- 视频上传区域 -->
<el-divider content-position="left">
<el-icon><VideoPlay /></el-icon>
<span style="margin-left: 8px; font-weight: bold; color: #67C23A;">视频上传</span>
</el-divider>
<el-row>
<el-col :span="12">
<el-form-item label="装车过磅视频" prop="entruckWeightVideo">
<video
v-if="ruleForm.entruckWeightVideo"
style="width: 100%; height: 260px; margin-bottom: 10px"
:src="ruleForm.entruckWeightVideo"
controls
></video>
<el-upload
accept=".mp4,.avi,.rmvb,.mkv,.MP4,.AVI,.RMVB,.MKV"
class="upload-demo"
action="/api/common/upload"
style="width: 100%"
:headers="importHeaders"
:limit="1"
:on-remove="
(file, fileList) => {
return handleRemoveVideo(file, fileList, 'entruckWeightVideo');
}
"
:on-progress="loadvideo"
:on-success="
(response, file, fileList) => {
return uploadSuccessVideo(response, file, fileList, 'entruckWeightVideo');
}
"
>
<el-button type="primary"
>上传<el-icon><UploadFilled /></el-icon
></el-button>
</el-upload>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="空车过磅视频" prop="emptyWeightVideo">
<video
v-if="ruleForm.emptyWeightVideo"
style="width: 100%; height: 260px; margin-bottom: 10px"
:src="ruleForm.emptyWeightVideo"
controls
></video>
<el-upload
accept=".mp4,.avi,.rmvb,.mkv,.MP4,.AVI,.RMVB,.MKV"
class="upload-demo"
action="/api/common/upload"
style="width: 100%"
:headers="importHeaders"
:limit="1"
:on-remove="
(file, fileList) => {
return handleRemoveVideo(file, fileList, 'emptyWeightVideo');
}
"
:on-progress="loadvideo"
:on-success="
(response, file, fileList) => {
return uploadSuccessVideo(response, file, fileList, 'emptyWeightVideo');
}
"
>
<el-button type="primary"
>上传<el-icon><UploadFilled /></el-icon
></el-button>
</el-upload>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="上传装牛视频" prop="entruckVideo">
<video
v-if="ruleForm.entruckVideo"
style="width: 100%; height: 260px; margin-bottom: 10px"
:src="ruleForm.entruckVideo"
controls
></video>
<el-upload
accept=".mp4,.avi,.rmvb,.mkv,.MP4,.AVI,.RMVB,.MKV"
class="upload-demo"
action="/api/common/upload"
style="width: 100%"
:headers="importHeaders"
:limit="1"
:on-remove="
(file, fileList) => {
return handleRemoveVideo(file, fileList, 'entruckVideo');
}
"
:on-progress="loadvideo"
:on-success="
(response, file, fileList) => {
return uploadSuccessVideo(response, file, fileList, 'entruckVideo');
}
"
>
<el-button type="primary"
>上传<el-icon><UploadFilled /></el-icon
></el-button>
</el-upload>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="控槽视频" prop="controlSlotVideo">
<video
v-if="ruleForm.controlSlotVideo"
style="width: 100%; height: 260px; margin-bottom: 10px"
:src="ruleForm.controlSlotVideo"
controls
></video>
<el-upload
accept=".mp4,.avi,.rmvb,.mkv,.MP4,.AVI,.RMVB,.MKV"
class="upload-demo"
action="/api/common/upload"
style="width: 100%"
:headers="importHeaders"
:limit="1"
:on-remove="
(file, fileList) => {
return handleRemoveVideo(file, fileList, 'controlSlotVideo');
}
"
:on-progress="loadvideo"
:on-success="
(response, file, fileList) => {
return uploadSuccessVideo(response, file, fileList, 'controlSlotVideo');
}
"
>
<el-button type="primary"
>上传<el-icon><UploadFilled /></el-icon
></el-button>
</el-upload>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="装完牛绕车一圈视频" prop="cattleLoadingCircleVideo">
<video
v-if="ruleForm.cattleLoadingCircleVideo"
style="width: 100%; height: 260px; margin-bottom: 10px"
:src="ruleForm.cattleLoadingCircleVideo"
controls
></video>
<el-upload
accept=".mp4,.avi,.rmvb,.mkv,.MP4,.AVI,.RMVB,.MKV"
class="upload-demo"
action="/api/common/upload"
style="width: 100%"
:headers="importHeaders"
:limit="1"
:on-remove="
(file, fileList) => {
return handleRemoveVideo(file, fileList, 'cattleLoadingCircleVideo');
}
"
:on-progress="loadvideo"
:on-success="
(response, file, fileList) => {
return uploadSuccessVideo(response, file, fileList, 'cattleLoadingCircleVideo');
}
"
>
<el-button type="primary"
>上传<el-icon><UploadFilled /></el-icon
></el-button>
</el-upload>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button type="primary" @click="onClickSave" :loading="data.saveLoading">确 定</el-button>
<el-button @click="handleClose">取 消</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { hostList, orderLoadDetail, orderLoadSave } from '@/api/shipping.js';
import { useUserStore } from '../../store/user';
const userStore = useUserStore();
const importHeaders = reactive({ Authorization: userStore.$state.token });
const formDataRef = ref();
const emits = defineEmits(['success']);
const data = reactive({
dialogVisible: false,
hostLoading: false,
hostNumber: '',
hostOptions: [],
hostPageNum: 1,
hostTotal: 0,
deliveryDevices: [],
xqDevices: [],
deliveryId: '',
dialogLoading: false,
saveLoading: false,
});
const ruleForm = reactive({
deliveryId: '',
estimatedDeliveryTime: '', // 预计送达时间,
serverDeviceSn: '', // 主机id
emptyWeight: '', // 空车过磅重量
entruckWeight: '', // 装车过磅重量
quarantineTickeyUrl: '', // 检疫票
poundListImg: '', // 传纸质磅单(双章)
entruckWeightVideo: '', // 装车过磅视频
emptyWeightVideo: '', // 空车过磅视频
entruckVideo: '', // 上传装牛视频
controlSlotVideo: '', // 控槽视频
emptyVehicleFrontPhoto: '', // 车辆空磅上磅车头照片
cattleLoadingCircleVideo: '', // 装完牛绕车一圈视频
loadedVehicleFrontPhoto: '', // 车辆过重磅车头照片
loadedVehicleWeightPhoto: '', // 车辆重磅照片
driverIdCardPhoto: '', // 驾驶员手持身份证站车头照片
deliveryDevices: [], // 耳标编号集合
xqDevices: [],
});
const rules = reactive({});
// 查询详情
const getOrderDetail = () => {
orderLoadDetail({
deliveryId: data.deliveryId,
}).then((res) => {
if (res.code === 200) {
const ear = res.data && res.data.deliveryDevices;
const collar = res.data && res.data.xqDevices;
// 兼容后端返回数组或 { rows, total } 两种格式
data.deliveryDevices = Array.isArray(ear) ? ear : (ear && ear.rows ? ear.rows : []);
data.xqDevices = Array.isArray(collar) ? collar : (collar && collar.rows ? collar.rows : []);
}
});
};
// 智能主机远程搜索
const hostRemoteMethod = (e) => {
data.hostNumber = e;
data.hostPageNum = 1;
getHostList();
};
// 主机列表
const getHostList = () => {
data.hostLoading = true;
const params = {
pageNum: data.hostPageNum,
pageSize: 10,
deviceId: data.hostNumber,
};
hostList(params)
.then((res) => {
data.hostLoading = false;
data.hostOptions = res.data.rows;
data.hostTotal = res.data.total;
})
.catch(() => {
data.hostLoading = false;
});
};
// 选择智能主机分页
const hostHandleCurrentChange = (val) => {
data.hostPageNum = val;
getHostList();
};
// 检疫票上传
const jyHandleAvatarSuccess = (res) => {
if (res.code === 200) {
ElMessage({
message: res.msg,
type: 'success',
});
ruleForm.quarantineTickeyUrl = res.data.src;
} else {
ElMessage.error(res.msg);
}
};
// 传纸质磅单(双章)
const szHandleAvatarSuccess = (res) => {
if (res.code === 200) {
ElMessage({
message: res.msg,
type: 'success',
});
ruleForm.poundListImg = res.data.src;
} else {
ElMessage.error(res.msg);
}
};
// 车辆空磅上磅车头照片上传
const emptyVehicleFrontPhotoSuccess = (res) => {
if (res.code === 200) {
ElMessage({
message: res.msg,
type: 'success',
});
ruleForm.emptyVehicleFrontPhoto = res.data.src;
} else {
ElMessage.error(res.msg);
}
};
// 车辆过重磅车头照片上传
const loadedVehicleFrontPhotoSuccess = (res) => {
if (res.code === 200) {
ElMessage({
message: res.msg,
type: 'success',
});
ruleForm.loadedVehicleFrontPhoto = res.data.src;
} else {
ElMessage.error(res.msg);
}
};
// 车辆重磅照片上传
const loadedVehicleWeightPhotoSuccess = (res) => {
if (res.code === 200) {
ElMessage({
message: res.msg,
type: 'success',
});
ruleForm.loadedVehicleWeightPhoto = res.data.src;
} else {
ElMessage.error(res.msg);
}
};
// 驾驶员手持身份证站车头照片上传
const driverIdCardPhotoSuccess = (res) => {
if (res.code === 200) {
ElMessage({
message: res.msg,
type: 'success',
});
ruleForm.driverIdCardPhoto = res.data.src;
} else {
ElMessage.error(res.msg);
}
};
// 视频上传时
const loadvideo = (event, file, fileLis) => {
data.dialogLoading = true;
};
// 上传视频
const uploadSuccessVideo = (res, file, fileList, type) => {
data.dialogLoading = false;
if (res.code === 200) {
ElMessage({
message: res.msg,
type: 'success',
});
if (ruleForm.hasOwnProperty(type)) {
ruleForm[type] = res.data.src;
}
} else {
ElMessage.error(res.msg);
}
};
// 删除视频
const handleRemoveVideo = (file, fileList, type) => {
if (ruleForm.hasOwnProperty(type)) {
ruleForm[type] = '';
}
};
// 保存按钮
const onClickSave = () => {
data.saveLoading = true;
ruleForm.deliveryDevices = data.deliveryDevices;
ruleForm.xqDevices = data.xqDevices;
orderLoadSave(ruleForm).then((res) => {
data.saveLoading = false;
if (res.code === 200) {
ElMessage({
message: res.msg,
type: 'success',
});
emits('success');
if (formDataRef.value) {
formDataRef.value.resetFields();
}
data.dialogVisible = false;
} else {
ElMessage.error(res.msg);
}
});
};
// 取消
const handleClose = () => {
data.dialogVisible = false;
if (formDataRef.value) {
formDataRef.value.resetFields();
}
};
const onShowDialog = (row) => {
data.dialogVisible = true;
if (formDataRef.value) {
formDataRef.value.resetFields();
}
if (row) {
nextTick(() => {
data.deliveryId = row.id;
ruleForm.deliveryId = row.id;
getOrderDetail();
getHostList();
});
}
};
defineExpose({
onShowDialog,
});
</script>
<style lang="less" scoped>
.avatar {
width: 178px;
height: 178px;
}
:deep(.avatar-uploader .el-upload) {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
:deep(.avatar-uploader .el-upload:hover) {
border-color: var(--el-color-primary);
}
:deep(.el-icon.avatar-uploader-icon) {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
}
</style>

View File

@@ -0,0 +1,567 @@
<template>
<div>
<base-search :formItemList="formItemList" @search="searchFrom" ref="baseSearchRef"> </base-search>
<div style="display: flex; padding: 10px; background: #fff; margin-bottom: 10px">
<el-button type="primary" v-hasPermi="['loading:create']" @click="showAddDialog(null)">创建装车订单</el-button>
<!-- <el-button
type="primary"
v-hasPermi="['loading:add']"
@click="showCreateDeliveryDialog"
style="margin-left: 10px"
>
新增运送清单
</el-button> -->
</div>
<div class="main-container">
<el-table :data="rows" :key="data.tableKey" border v-loading="data.dataListLoading" element-loading-text="数据加载中..." style="width: 100%">
<el-table-column label="装车订单编号" prop="deliveryNumber">
<template #default="scope">
{{ scope.row.deliveryNumber || '--' }}
</template>
</el-table-column>
<el-table-column label="订单标题" prop="deliveryTitle">
<template #default="scope">
{{ scope.row.deliveryTitle || '--' }}
</template>
</el-table-column>
<el-table-column label="起始地" prop="startLocation">
<template #default="scope">
{{ scope.row.startLocation || '--' }}
</template>
</el-table-column>
<el-table-column label="目的地" prop="endLocation">
<template #default="scope">
{{ scope.row.endLocation || '--' }}
</template>
</el-table-column>
<el-table-column label="采购单价(元/公斤)" prop="buyerPrice">
<template #default="scope">
{{ scope.row.buyerPrice || '0' }}
</template>
</el-table-column>
<el-table-column label="销售单价(元/公斤)" prop="salePrice">
<template #default="scope">
{{ scope.row.salePrice || '0' }}
</template>
</el-table-column>
<el-table-column label="约定单价(元/公斤)" prop="firmPrice">
<template #default="scope">
{{ scope.row.firmPrice || '0' }}
</template>
</el-table-column>
<el-table-column label="装车数量" prop="ratedQuantity">
<template #default="scope">
{{ scope.row.ratedQuantity || '0' }}
</template>
</el-table-column>
<el-table-column label="已分配设备数量" prop="bindJbqCount">
<template #default="scope">
{{ scope.row.bindJbqCount || '0' }}
</template>
</el-table-column>
<el-table-column label="已佩戴设备数量" prop="wareCount">
<template #default="scope">
<span :style="{ color: scope.row.bindJbqCount == scope.row.wareCount ? '' : 'red' }">
{{ scope.row.wareCount || '0' }}
</span>
</template>
</el-table-column>
<el-table-column label="创建人" prop="createByName">
<template #default="scope">
{{ scope.row.createByName || '--' }}
</template>
</el-table-column>
<el-table-column label="创建时间" prop="createTime">
<template #default="scope">
{{ scope.row.createTime || '--' }}
</template>
</el-table-column>
<el-table-column label="核验状态" prop="statusDesc" width="100" :key="`status-${data.forceUpdate}`">
<template #default="scope">
<!-- 调试信息 -->
<div style="display: none;">{{ console.log('核验状态调试:', { status: scope.row.status, statusDesc: scope.row.statusDesc, rowId: scope.row.id }) }}</div>
<el-tag :type="getStatusTagType(scope.row.status)" :key="`tag-${scope.row.id}-${data.forceUpdate}`">
{{ scope.row.statusDesc || getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="登记设备数量" prop="registeredJbqCount" width="120" :key="`count-${data.forceUpdate}`">
<template #default="scope">
<!-- 调试信息 -->
<div style="display: none;">{{ console.log('设备数量调试:', { registeredJbqCount: scope.row.registeredJbqCount }) }}</div>
<span :key="`count-span-${scope.row.id}-${data.forceUpdate}`">{{ scope.row.registeredJbqCount || '0' }}</span>
</template>
</el-table-column>
<el-table-column label="车内盘点耳标数量" prop="earTagCount" width="140" :key="`ear-tag-${data.forceUpdate}`">
<template #default="scope">
<!-- 调试信息 -->
<div style="display: none;">{{ console.log('耳标数量调试:', { earTagCount: scope.row.earTagCount }) }}</div>
<span :key="`ear-tag-span-${scope.row.id}-${data.forceUpdate}`">{{ scope.row.earTagCount || '0' }}</span>
</template>
</el-table-column>
<el-table-column label="车牌号" prop="licensePlate" width="120">
<template #default="scope">
{{ scope.row.licensePlate || '--' }}
</template>
</el-table-column>
<el-table-column label="司机姓名" prop="driverName" width="120">
<template #default="scope">
{{ scope.row.driverName || '--' }}
</template>
</el-table-column>
<el-table-column label="车身照片" width="200">
<template #default="scope">
<div style="display: flex; flex-wrap: wrap; gap: 5px;">
<!-- 使用前端分割逻辑处理车身照片 -->
<template v-if="getProcessedCarPhotos(scope.row).length > 0">
<el-image
v-for="(img, index) in getProcessedCarPhotos(scope.row)"
:key="'car-' + index"
style="width: 60px; height: 60px; border-radius: 4px; border: 1px solid #dcdfe6;"
:src="img"
fit="cover"
:lazy="true"
:preview-src-list="getProcessedCarPhotos(scope.row)"
preview-teleported
@error="handleImageError(img, index)"
>
<template #error>
<div style="width: 60px; height: 60px; display: flex; justify-content: center; align-items: center; background: #f5f7fa;">
<el-icon style="font-size: 24px; color: #c0c4cc;"><Picture /></el-icon>
</div>
</template>
</el-image>
</template>
<!-- 无照片时显示占位 -->
<span v-else style="color: #999; font-size: 12px; display: flex; align-items: center;">暂无照片</span>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="420">
<template #default="scope">
<el-button link type="primary" :disabled="scope.row.status != 1" v-hasPermi="['loading:edit']" @click="showEditDialog(scope.row)">编辑</el-button>
<el-button link type="primary" :disabled="scope.row.status != 1" v-hasPermi="['loading:assign']" @click="showAssignDialog(scope.row)">分配设备</el-button>
<el-button link type="primary" v-hasPermi="['loading:view']" @click="showDetailDialog(scope.row)">详情</el-button>
<el-button link type="primary" v-hasPermi="['loading:device']" @click="showLookDialog(scope.row)">查看设备</el-button>
<el-button link type="primary" v-hasPermi="['loading:status']" @click="editStatus(scope.row)">编辑状态</el-button>
<el-button link type="primary" :disabled="scope.row.status != 1" v-hasPermi="['loading:delete']" @click="del(scope.row.id)">删除</el-button>
<el-button link type="primary" :disabled="scope.row.status != 1" v-hasPermi="['loading:load']" @click="loadClick(scope.row)">装车</el-button>
</template>
</el-table-column>
<template #empty>
<div class="dataListOnEmpty">暂无数据</div>
</template>
</el-table>
<pagination v-model:limit="form.pageSize" v-model:page="form.pageNum" :total="data.total" @pagination="getDataList" />
<OrderDialog ref="OrderDialogRef" @success="getDataList" />
<LookDialog ref="LookDialogRef" />
<AssignDialog ref="AssignDialogRef" @success="getDataList" />
<DetailDialog ref="DetailDialogRef" />
<editDialog ref="editDialogRef" @success="getDataList" />
<LoadDialog ref="LoadDialogRef" @success="getDataList" />
<CreateDeliveryDialog ref="CreateDeliveryDialogRef" @success="getDataList" />
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, nextTick, watch } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Picture } from '@element-plus/icons-vue';
import baseSearch from '@/components/common/searchCustom/index.vue';
import { orderList, orderDel, updateDeliveryStatus } from '@/api/shipping.js';
import { getImageList, handleImageError } from '@/utils/imageUtils.js';
import OrderDialog from './orderDialog.vue';
import LookDialog from './lookDialog.vue';
import AssignDialog from './assignDialog.vue';
import DetailDialog from './detailDialog.vue';
import editDialog from './editDialog.vue';
import LoadDialog from './loadDialog.vue';
import CreateDeliveryDialog from './createDeliveryDialog.vue';
const baseSearchRef = ref();
const DetailDialogRef = ref();
const OrderDialogRef = ref();
const LookDialogRef = ref();
const editDialogRef = ref();
const AssignDialogRef = ref();
const LoadDialogRef = ref();
const CreateDeliveryDialogRef = ref();
const formItemList = reactive([
{
label: '运单号',
type: 'input',
param: 'deliveryNumber',
span: 6,
placeholder: '请输入运单号',
},
{
label: '订单标题',
type: 'input',
param: 'deliveryTitle',
span: 6,
placeholder: '请输入订单标题',
},
{
label: '目的地',
type: 'input',
param: 'endLocation',
span: 6,
placeholder: '请输入目的地',
},
{
label: '车牌号',
type: 'input',
param: 'licensePlate',
span: 6,
placeholder: '请输入车牌号',
},
{
label: '核验状态',
type: 'select',
param: 'status',
span: 6,
placeholder: '请选择核验状态',
selectOptions: [
{ text: '待装车', value: 1 },
{ text: '已装车/待资金方付款', value: 2 },
{ text: '待核验/资金方已付款', value: 3 },
{ text: '已核验/待买家付款', value: 4 },
{ text: '买家已付款', value: 5 }
],
labelKey: 'text',
valueKey: 'value',
},
{
label: '创建时间',
type: 'daterange',
param: 'createTimeRange',
span: 6,
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
},
]);
const data = reactive({
total: 0,
dataListLoading: false,
tableKey: 0, // 用于强制重新渲染表格
forceUpdate: 0, // 用于强制更新
});
// 使用ref确保响应式更新
const rows = ref([]);
const form = reactive({
pageNum: 1,
pageSize: 10,
});
const searchFrom = () => {
console.log('=== 搜索功能被触发 ===');
form.pageNum = 1;
getDataList();
};
// 列表
const getDataList = () => {
data.dataListLoading = true;
const searchParams = baseSearchRef.value.penetrateParams();
// 处理日期范围参数
const params = {
...form,
...searchParams,
};
// 如果存在日期范围需要拆分为startTime和endTime
if (searchParams.createTimeRange && Array.isArray(searchParams.createTimeRange) && searchParams.createTimeRange.length === 2) {
params.startTime = searchParams.createTimeRange[0];
params.endTime = searchParams.createTimeRange[1];
delete params.createTimeRange; // 删除原始参数
}
console.log('运送清单列表查询参数:', params);
console.log('=== 前端搜索参数调试 ===');
console.log('deliveryNumber:', params.deliveryNumber);
console.log('licensePlate:', params.licensePlate);
console.log('deliveryTitle:', params.deliveryTitle);
console.log('endLocation:', params.endLocation);
console.log('status:', params.status);
console.log('startTime:', params.startTime);
console.log('endTime:', params.endTime);
orderList(params)
.then((res) => {
console.log('运送清单列表返回结果:', res);
data.dataListLoading = false;
// 调试:检查所有数据
console.log('=== 装车订单数据调试 ===');
console.log('API响应:', res);
console.log('数据行数:', res.data.rows ? res.data.rows.length : 0);
// 前端精确搜索在API返回的数据中根据车牌号精确搜索
if (searchParams.licensePlate && res.data.rows && res.data.rows.length > 0) {
console.log('=== 前端精确搜索车牌号 ===');
console.log('搜索车牌号:', searchParams.licensePlate);
console.log('API返回的所有车牌号:');
res.data.rows.forEach((row, index) => {
console.log(`${index + 1}行车牌号:`, row.licensePlate);
});
// 精确匹配车牌号
const filteredRows = res.data.rows.filter(row => row.licensePlate === searchParams.licensePlate);
console.log('精确匹配结果:', filteredRows.length, '条');
if (filteredRows.length > 0) {
console.log('找到匹配的车牌号数据:', filteredRows);
res.data.rows = filteredRows;
res.data.total = filteredRows.length;
} else {
console.log('未找到匹配的车牌号,显示空结果');
res.data.rows = [];
res.data.total = 0;
}
}
if (res.data.rows && res.data.rows.length > 0) {
// 调试所有行的基本信息
res.data.rows.forEach((row, index) => {
console.log(`=== 第${index + 1}行数据 ===`);
console.log('车牌号:', row.licensePlate);
console.log('司机姓名:', row.driverName);
console.log('carFrontPhoto:', row.carFrontPhoto);
console.log('carBehindPhoto:', row.carBehindPhoto);
console.log('driverId:', row.driverId);
// 特别关注车牌号wwwww
if (row.licensePlate === 'wwwww') {
console.log('*** 找到车牌号wwwww的数据 ***');
console.log('完整行数据:', row);
// 前端测试分割逻辑
console.log('=== 前端测试分割逻辑 ===');
const testCarImg = "https://smart-1251449951.cos.ap-guangzhou.myqcloud.com/iotPlateform/2025/10/16/4c4e20251016142427.jpg,https://smart-1251449951.cos.ap-guangzhou.myqcloud.com/iotPlateform/2025/10/16/4c4e20251016142429.jpg";
console.log('测试car_img:', testCarImg);
const carImgUrls = testCarImg.split(',');
console.log('分割后数组:', carImgUrls);
console.log('分割后长度:', carImgUrls.length);
if (carImgUrls.length >= 2) {
const carBehindPhoto = carImgUrls[0].trim();
const carFrontPhoto = carImgUrls[1].trim();
console.log('车尾照片 (carBehindPhoto):', carBehindPhoto);
console.log('车头照片 (carFrontPhoto):', carFrontPhoto);
// 测试getImageList函数
console.log('车尾照片getImageList结果:', getImageList(carBehindPhoto));
console.log('车头照片getImageList结果:', getImageList(carFrontPhoto));
}
}
});
} else {
console.log('没有数据行');
}
// 使用setTimeout确保DOM完全更新
setTimeout(() => {
// 强制重新赋值,确保响应式更新
rows.value = [...res.data.rows];
data.total = res.data.total;
// 更新表格key强制重新渲染
data.tableKey = Date.now();
// 强制更新
data.forceUpdate = Date.now();
console.log('数据更新完成当前rows长度:', rows.value.length);
if (rows.value.length > 0) {
console.log('更新后的第一行数据:', rows.value[0]);
}
// 再次延迟更新,确保表格重新渲染
setTimeout(() => {
data.forceUpdate = Date.now();
console.log('二次强制更新完成');
}, 100);
}, 50);
})
.catch(() => {
data.dataListLoading = false;
});
};
// 新增装车订单
const showAddDialog = (row) => {
if (OrderDialogRef.value) {
OrderDialogRef.value.onShowDialog(row);
}
};
// 编辑装车订单
const showEditDialog = (row) => {
if (editDialogRef.value) {
editDialogRef.value.onShowDialog(row);
}
};
// 新增运送清单
const showCreateDeliveryDialog = () => {
if (CreateDeliveryDialogRef.value) {
CreateDeliveryDialogRef.value.open();
}
};
// 查看耳标设备
const showLookDialog = (row) => {
if (LookDialogRef.value) {
LookDialogRef.value.onShowLookDialog(row);
}
};
// 分配耳标设备
const showAssignDialog = (row) => {
if (AssignDialogRef.value) {
AssignDialogRef.value.onShowAssignDialog(row);
}
};
// 详情
const showDetailDialog = (row) => {
if (DetailDialogRef.value) {
DetailDialogRef.value.onShowDetailDialog(row);
}
};
// 删除
const del = (id) => {
ElMessageBox.confirm('请确认是否删除', '提示', {
cancelButtonText: '取消',
confirmButtonText: '确定',
type: 'warning',
}).then(() => {
orderDel(id).then(() => {
ElMessage.success('操作成功');
getDataList();
});
});
};
// 装车
const loadClick = (row) => {
if (LoadDialogRef.value) {
LoadDialogRef.value.onShowDialog(row);
}
};
// 编辑状态
const editStatus = (row) => {
ElMessageBox.prompt('请输入状态(1-待装车 2-已装车/待资金方付款 3-待核验/资金方已付款 4-已核验/待买家付款 5-买家已付款)', '修改状态', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /^[12345]$/,
inputErrorMessage: '请输入1、2、3、4或5',
inputValue: String(row.status || 1)
}).then(({ value }) => {
updateDeliveryStatus({ id: row.id, status: parseInt(value) })
.then(res => {
if (res.code === 200) {
ElMessage.success('状态更新成功');
getDataList();
} else {
ElMessage.error(res.msg || '状态更新失败');
}
})
.catch(() => {
ElMessage.error('状态更新失败');
});
}).catch(() => {
// 用户取消
});
};
// 状态文本转换
const getStatusText = (status) => {
const statusMap = {
1: '待装车',
2: '已装车/待资金方付款',
3: '待核验/资金方已付款',
4: '已核验/待买家付款',
5: '买家已付款'
};
return statusMap[status] || '未知状态';
};
// 状态标签类型
const getStatusTagType = (status) => {
const typeMap = {
1: 'warning', // 待装车 - 橙色
2: 'info', // 已装车/待资金方付款 - 蓝色
3: 'warning', // 待核验/资金方已付款 - 橙色
4: 'success', // 已核验/待买家付款 - 绿色
5: 'success' // 买家已付款 - 绿色
};
return typeMap[status] || 'info';
};
// 前端处理车身照片分割逻辑
const getProcessedCarPhotos = (row) => {
console.log('=== 前端处理车身照片 ===');
console.log('输入行数据:', row);
console.log('carFrontPhoto:', row.carFrontPhoto);
console.log('carBehindPhoto:', row.carBehindPhoto);
// 检查是否有车身照片数据
if (!row.carFrontPhoto && !row.carBehindPhoto) {
console.log('没有车身照片数据');
return [];
}
// 如果后端已经正确分割,直接使用
if (row.carFrontPhoto && row.carBehindPhoto &&
!row.carFrontPhoto.includes(',') && !row.carBehindPhoto.includes(',')) {
console.log('后端已正确分割,直接使用');
return [row.carBehindPhoto, row.carFrontPhoto]; // 车尾在前,车头在后
}
// 如果后端没有正确分割,使用前端分割逻辑
let carImgUrls = [];
// 尝试从carFrontPhoto获取完整数据
if (row.carFrontPhoto && row.carFrontPhoto.includes(',')) {
console.log('从carFrontPhoto分割:', row.carFrontPhoto);
carImgUrls = row.carFrontPhoto.split(',').map(url => url.trim()).filter(url => url !== '');
}
// 尝试从carBehindPhoto获取完整数据
else if (row.carBehindPhoto && row.carBehindPhoto.includes(',')) {
console.log('从carBehindPhoto分割:', row.carBehindPhoto);
carImgUrls = row.carBehindPhoto.split(',').map(url => url.trim()).filter(url => url !== '');
}
// 如果都没有逗号,尝试合并两个字段
else if (row.carFrontPhoto && row.carBehindPhoto) {
console.log('合并两个字段:', row.carFrontPhoto, row.carBehindPhoto);
carImgUrls = [row.carBehindPhoto, row.carFrontPhoto].filter(url => url && url.trim() !== '');
}
// 单个字段
else if (row.carFrontPhoto) {
carImgUrls = [row.carFrontPhoto];
}
else if (row.carBehindPhoto) {
carImgUrls = [row.carBehindPhoto];
}
console.log('分割结果:', carImgUrls);
console.log('分割后长度:', carImgUrls.length);
return carImgUrls;
};
// 监听rows变化强制更新表格
watch(rows, (newRows) => {
console.log('rows数据变化:', newRows);
if (newRows && newRows.length > 0) {
console.log('第一行数据详情:', newRows[0]);
console.log('关键字段:', {
statusDesc: newRows[0].statusDesc,
registeredJbqCount: newRows[0].registeredJbqCount,
earTagCount: newRows[0].earTagCount
});
}
}, { deep: true, immediate: true });
onMounted(() => {
console.log('=== 装车订单页面已加载 ===');
getDataList();
});
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,178 @@
<template>
<el-dialog v-model="data.dialogVisible" title="查看设备" style="width: 1000px; padding-bottom: 20px">
<el-table v-loading="data.dataListLoading" :data="data.rows" border element-loading-text="数据加载中...">
<el-table-column label="设备编号" prop="deviceId">
<template #default="scope">
{{ scope.row.deviceId || scope.row.sn || '--' }}
</template>
</el-table-column>
<el-table-column label="设备类型" prop="deviceType">
<template #default="scope">
{{
scope.row.deviceType == '2'
? '智能耳标'
: scope.row.deviceType == '1'
? '智能主机'
: scope.row.deviceType == '3'
? '智能项圈'
: '--'
}}
</template>
</el-table-column>
<el-table-column label="所属用户" prop="deviceUserName">
<template #default="scope">
{{ scope.row.deviceUserName || '--' }}
</template>
</el-table-column>
<el-table-column label="是否佩戴" prop="gpsState">
<template #default="scope">
<el-tag :type="scope.row.isWare == '1' || scope.row.is_wear == 1 ? 'danger' : 'primary'" effect="light">
{{ scope.row.isWare == '1' || scope.row.is_wear == 1 ? '已佩戴' : '未佩戴' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="牛只图片" prop="gpsState" width="350">
<template #default="scope">
<div style="display: flex" v-if="scope.row">
<!-- 正面图片 -->
<div v-if="scope.row.frontImg">
<el-image
v-for="(img, index) in getImageList(scope.row.frontImg)"
:key="'front-' + index"
:src="img"
style="width: 80px; height: 80px; margin-right: 10px"
fit="cover"
:preview-src-list="getImageList(scope.row.frontImg)"
preview-teleported
>
<template #error>
<div
style="width: 80px; height: 80px; display: flex; justify-content: center; align-items: center"
class="image-slot"
>
<el-icon style="font-size: 50px"><Picture /></el-icon>
</div>
</template>
</el-image>
</div>
<!-- 侧面图片 -->
<div v-if="scope.row.sideImg">
<el-image
v-for="(img, index) in getImageList(scope.row.sideImg)"
:key="'side-' + index"
:src="img"
style="width: 80px; height: 80px; margin-right: 10px"
fit="cover"
:preview-src-list="getImageList(scope.row.sideImg)"
preview-teleported
>
<template #error>
<div
style="width: 80px; height: 80px; display: flex; justify-content: center; align-items: center"
class="image-slot"
>
<el-icon style="font-size: 50px"><Picture /></el-icon>
</div>
</template>
</el-image>
</div>
<!-- 臀部图片 -->
<div v-if="scope.row.hipImg">
<el-image
v-for="(img, index) in getImageList(scope.row.hipImg)"
:key="'hip-' + index"
:src="img"
style="width: 80px; height: 80px; margin-right: 10px"
fit="cover"
:preview-src-list="getImageList(scope.row.hipImg)"
preview-teleported
>
<template #error>
<div
style="width: 80px; height: 80px; display: flex; justify-content: center; align-items: center"
class="image-slot"
>
<el-icon style="font-size: 50px"><Picture /></el-icon>
</div>
</template>
</el-image>
</div>
<!-- 无图片时显示占位 -->
<div v-if="!scope.row.frontImg && !scope.row.sideImg && !scope.row.hipImg" style="color: #999; display: flex; align-items: center;">
暂无图片
</div>
</div>
</template>
</el-table-column>
</el-table>
<pagination :limit="form.pageSize" :page="form.pageNum" :total="data.total" @pagination="getDataList" />
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { deviceAllList } from '@/api/shipping.js';
const data = reactive({
dialogVisible: false,
deliveryId: '',
dataListLoading: false,
rows: [],
total: 0,
});
const form = reactive({
pageNum: 1,
pageSize: 10,
});
const getDataList = () => {
data.dataListLoading = true;
const params = {
deliveryId: data.deliveryId,
};
deviceAllList(params)
.then((res) => {
data.dataListLoading = false;
if (res.code === 200) {
data.rows = res.data || [];
data.total = data.rows.length || 0;
} else {
data.rows = [];
data.total = 0;
}
})
.catch(() => {
data.dataListLoading = false;
data.rows = [];
data.total = 0;
});
};
// 处理逗号分隔的图片URL
const getImageList = (imageUrl) => {
if (!imageUrl || imageUrl.trim() === '') {
return [];
}
// 按逗号分割并过滤空字符串
return imageUrl.split(',').map(url => url.trim()).filter(url => url !== '');
};
const handleClose = () => {
data.dialogVisible = false;
};
const onShowLookDialog = (row) => {
data.dialogVisible = true;
if (row) {
data.deliveryId = row.id;
getDataList();
}
};
defineExpose({
onShowLookDialog,
});
</script>
<style lang="less" scoped></style>

View File

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

View File

@@ -0,0 +1,86 @@
<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-item>
<el-form-item label="岗位描述" prop="description">
<el-input maxlength="100" v-model="form.data.description" placeholder="请输入岗位描述" style="width: 320px" />
</el-form-item>
</el-form>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button :loading="submitting" type="primary" @click="onClickSave">保存</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { sysPositionSave } from '~/api/sys.js';
const dialogVisible = ref(false);
const FormDataRef = ref(null);
const submitting = ref(false);
const title = ref('');
const userId = ref('');
const emits = defineEmits();
const form = reactive({
data: {
name: '',
description: '',
type: '',
},
rules: {
name: [{ required: true, message: '岗位名称不能为空', trigger: 'blur' }],
description: [{ required: true, message: '岗位描述不能为空', trigger: 'blur' }],
},
});
const onShowDialog = (val, page) => {
title.value = !val ? '新增岗位' : '编辑岗位';
dialogVisible.value = true;
if (val) {
userId.value = val.id;
form.data = val;
}
};
const onClickSave = () => {
FormDataRef.value.validate((valid) => {
if (valid) {
submitting.value = true;
const params = { ...form.data };
if (!userId.value) {
params.id = userId.value;
}
sysPositionSave(params)
.then(() => {
ElMessage.success('操作成功');
handleClose();
emits('success');
submitting.value = false;
})
.catch(() => {
submitting.value = false;
});
}
});
};
const handleClose = () => {
form.data = {};
dialogVisible.value = false;
if (FormDataRef.value) {
FormDataRef.value.clearValidate();
}
};
defineExpose({
onShowDialog,
});
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,156 @@
<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" :disabled="pageNum == 2" />
</el-form-item>
<el-form-item label="岗位" prop="roleId">
<el-select v-model="form.data.roleId" placeholder="请选择岗位" style="width: 320px" :disabled="pageNum == 2">
<el-option v-for="item in options" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="手机号" prop="mobile">
<el-input v-model="form.data.mobile" placeholder="请输入手机号" style="width: 320px" :disabled="pageNum == 2" />
</el-form-item>
<el-form-item label="初始密码" prop="password" v-if="!pageNum">
<el-input v-model="form.data.password" placeholder="请输入初始密码" style="width: 320px" />
<p style="color: #dedfe0">提示信息如果初始密码为空则默认初始密码为123456</p>
</el-form-item>
<el-form-item label="新密码" prop="password" v-else>
<el-input v-model="form.data.password" placeholder="请输入新密码" style="width: 320px" />
</el-form-item>
</el-form>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button :loading="submitting" type="primary" @click="onClickSave">保存</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { sysPositionList, sysUserSave, updatePassword } from '@/api/sys.js';
const dialogVisible = ref(false);
const FormDataRef = ref(null);
const submitting = ref(false);
const title = ref('');
const pageNum = ref('');
const options = ref([]);
const userId = ref('');
const emits = defineEmits();
const form = reactive({
data: {
name: '',
mobile: '',
password: '',
roleId: '',
type: 1,
},
rules: {
name: [{ required: true, message: '员工姓名不能为空', trigger: 'blur' }],
mobile: [
{ required: true, message: '手机号不能为空', trigger: 'blur' },
{
pattern: /^1[3456789]\d{9}$/,
message: '请输入正确的手机号',
trigger: 'blur',
},
],
roleId: [{ required: true, message: '岗位不能为空', trigger: 'change' }],
},
});
const getOption = () => {
const params = {
pageNum: 1,
pageSize: 10000,
};
sysPositionList(params).then((res) => {
options.value = res.data.rows;
});
};
const onShowDialog = (val, page) => {
if (page) {
pageNum.value = page;
} else {
pageNum.value = '';
}
title.value = !val ? '新增员工' : '编辑员工';
if (page == 2) {
title.value = '修改密码';
}
if (FormDataRef.value) {
FormDataRef.value.clearValidate();
}
dialogVisible.value = true;
if (val) {
userId.value = val.id;
form.data = { ...val };
// form.data.roleId = val.roleList.map((i) => i.id);
}
getOption();
};
const onClickSave = () => {
FormDataRef.value.validate((valid) => {
if (valid) {
submitting.value = true;
// console.log('用户ID');
// return false;
// if (pageNum.value == 2) {
// 修改密码时
// updatePassword({
// id: userId.value,
// password: form.data.password,
// })
// .then(() => {
// ElMessage.success('操作成功');
// handleClose();
// emits('success');
// submitting.value = false;
// })
// .catch(() => {
// submitting.value = false;
// });
// } else {
// 新增、编辑员工时
const params = {
name: form.data.name,
mobile: form.data.mobile,
password: form.data.password,
roleId: form.data.roleId,
type: 1,
};
if (pageNum.value == 1) {
params.id = userId.value;
}
sysUserSave(params)
.then(() => {
ElMessage.success('操作成功');
handleClose();
emits('success');
submitting.value = false;
})
.catch(() => {
submitting.value = false;
});
// }
}
});
};
const handleClose = () => {
form.data = {};
dialogVisible.value = false;
if (FormDataRef.value) {
FormDataRef.value.clearValidate();
}
};
defineExpose({
onShowDialog,
});
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,358 @@
<template>
<el-dialog v-model="data.dialogVisible" :title="data.title" style="width: 650px; padding-bottom: 20px" :before-close="handleClose">
<el-tabs v-model="data.activeName" class="demo-tabs" @tab-click="handleClick">
<el-tab-pane label="未分配" name="first">
<el-button type="primary" @click="batchAssign" :loading="data.assignLoading">批量分配</el-button>
<el-table
:data="data.rows"
border
v-loading="data.dataListLoading"
element-loading-text="数据加载中..."
style="width: 100%"
:row-key="getRowKey"
@selection-change="unassignedSelected"
ref="multipleTableUnRef"
>
<el-table-column type="selection" reserve-selection width="55" />
<el-table-column label="设备类型" prop="deviceTypeName"> </el-table-column>
<el-table-column label="设备编号" prop="deviceId"> </el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button link type="primary" @click="assignClick(scope.row.deviceId)">分配</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="已分配" name="second">
<el-button type="primary" @click="batchDel" :loading="data.delLoading">批量删除</el-button>
<el-table
:data="data.rows"
border
v-loading="data.dataListLoading"
element-loading-text="数据加载中..."
style="width: 100%"
:row-key="getRowKey"
@selection-change="assignedSelected"
ref="multipleTableRef"
>
<el-table-column type="selection" reserve-selection width="55" />
<el-table-column label="设备类型" prop="deviceTypeName"> </el-table-column>
<el-table-column label="设备编号" prop="deviceId"> </el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button link type="primary" @click="delClick(scope.row.deviceId)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
<pagination
v-model:page="form.pageNum"
v-model:limit="form.pageSize"
:total="data.total || 0"
@pagination="getDataList"
/>
</el-dialog>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { deviceList, deviceAssign, deviceDel, tenantList } from '@/api/sys.js';
import { queryEarTagList, queryHostList } from '@/api/device.js';
import { collarList } from '@/api/abroad.js';
const emits = defineEmits(['success']);
const multipleTableUnRef = ref(null);
const multipleTableRef = ref(null);
const data = reactive({
dialogVisible: false,
title: '设备分配',
activeName: 'first',
unassignedSelect: [],
assignedSelect: [],
rows: [],
dataListLoading: false,
total: 0,
deviceType: '', // 设备类型1耳标2项圈3主机
allotType: '', // 是否已分配标识0未分配1已分配
tenantId: '', // 租户id
assignLoading: false,
delLoading: false,
});
const form = reactive({
pageNum: 1,
pageSize: 10,
});
const handleClick = (tab, event) => {
console.log('=== 标签页切换 ===', tab.props.name);
data.activeName = tab.props.name;
data.allotType = data.activeName === 'first' ? '0' : '1';
form.pageNum = 1;
form.pageSize = 10;
// 重新获取数据并过滤
getDataList();
};
// 列表
const getDataList = () => {
console.log('=== getDataList 开始执行 ===');
data.dataListLoading = true;
const params = {
...form,
deviceType: data.deviceType,
allotType: data.allotType,
tenantId: data.tenantId,
};
console.log('=== 请求参数 ===', params);
// 根据设备类型调用不同的API
let apiCall;
switch (data.deviceType) {
case '1': // 耳标
console.log('=== 调用耳标API ===');
apiCall = queryEarTagList(params);
break;
case '2': // 项圈
console.log('=== 调用项圈API ===');
apiCall = collarList(params);
break;
case '3': // 主机
console.log('=== 调用主机API ===');
apiCall = queryHostList(params);
break;
default:
console.log('=== 调用默认API ===');
apiCall = deviceList(params);
break;
}
apiCall
.then((res) => {
console.log('=== API 调用成功 ===', res);
data.dataListLoading = false;
if (res.code == 200) {
let rawData = [];
let total = 0;
// 处理不同的数据结构
if (res.data.list) {
// device.js 中的API返回 { list, total }
rawData = res.data.list || [];
total = res.data.total || 0;
console.log('=== 使用 list 格式数据 ===', { rawData, total });
} else {
// sys.js 中的API返回 { rows, total }
rawData = res.data?.rows || [];
total = res.data?.total || 0;
console.log('=== 使用 rows 格式数据 ===', { rawData, total });
}
// 处理数据:添加设备类型和分配状态
data.rows = rawData.map(item => {
const processedItem = { ...item };
// 根据设备类型自动添加设备类型名称
switch (data.deviceType) {
case '1': // 耳标
processedItem.deviceTypeName = '智能耳标';
break;
case '2': // 项圈
processedItem.deviceTypeName = '智能项圈';
break;
case '3': // 主机
processedItem.deviceTypeName = '智能主机';
break;
default:
processedItem.deviceTypeName = '未知设备';
break;
}
// 根据 deliveryNumber/delivery_number 判断分配状态(兼容两种格式)
const deliveryNumber = item.deliveryNumber || item.delivery_number;
processedItem.isAssigned = !!(deliveryNumber && deliveryNumber.trim() !== '');
console.log(`=== 处理设备 ${item.deviceId || item.sn} ===`, {
deviceType: data.deviceType,
deviceTypeName: processedItem.deviceTypeName,
deliveryNumber: deliveryNumber,
isAssigned: processedItem.isAssigned
});
return processedItem;
});
// 根据当前标签页过滤数据
if (data.activeName === 'first') {
// 未分配标签页:显示未分配的设备
data.rows = data.rows.filter(item => !item.isAssigned);
} else if (data.activeName === 'second') {
// 已分配标签页:显示已分配的设备
data.rows = data.rows.filter(item => item.isAssigned);
}
data.total = data.rows.length;
console.log('=== 处理后的数据 ===', { rows: data.rows, total: data.total });
} else {
console.error('=== API 返回错误 ===', res);
ElMessage.error(res.msg || '获取数据失败');
}
})
.catch((error) => {
console.error('=== API 调用失败 ===', error);
data.dataListLoading = false;
console.error('获取设备列表失败:', error);
data.rows = [];
data.total = 0;
});
};
// 未分配选中
const unassignedSelected = (val) => {
data.unassignedSelect = val.map((item) => item.deviceId);
};
// 批量绑定
const batchAssign = () => {
if (data.unassignedSelect.length === 0) {
ElMessage.error('请选择设备编号');
return;
}
data.assignLoading = true;
const params = {
deviceIds: data.unassignedSelect,
deviceType: data.deviceType,
tenantId: data.tenantId,
};
deviceAssign(params)
.then((res) => {
data.assignLoading = false;
if (res.code === 200) {
ElMessage({
message: res.msg,
type: 'success',
});
getDataList();
} else {
ElMessage.error(res.msg);
}
})
.catch((err) => {
data.assignLoading = false;
});
};
//
// 已分配选中
const assignedSelected = (val) => {
data.assignedSelect = val.map((item) => item.deviceId);
};
// 单个分配
const assignClick = (deviceId) => {
const params = {
deviceIds: [deviceId],
deviceType: data.deviceType,
tenantId: data.tenantId,
};
deviceAssign(params)
.then((res) => {
if (res.code === 200) {
ElMessage({
message: res.msg,
type: 'success',
});
getDataList();
} else {
ElMessage.error(res.msg);
}
})
.catch(() => {});
};
// 批量删除
const batchDel = () => {
if (data.assignedSelect.length === 0) {
ElMessage.error('请选择设备编号');
return;
}
ElMessageBox.confirm('请确认是否批量删除', '提示', {
cancelButtonText: '取消',
confirmButtonText: '确定',
type: 'warning',
}).then(() => {
data.delLoading = true;
const params = {
deviceIds: data.assignedSelect,
deviceType: data.deviceType,
tenantId: data.tenantId,
};
deviceDel(params).then((res) => {
data.delLoading = false;
if (res.code === 200) {
ElMessage({
message: res.msg,
type: 'success',
});
getDataList();
} else {
ElMessage.error(res.msg);
}
});
});
};
// 单个删除
const delClick = (id) => {
ElMessageBox.confirm('请确认是否删除', '提示', {
cancelButtonText: '取消',
confirmButtonText: '确定',
type: 'warning',
}).then(() => {
const params = {
deviceIds: [id],
deviceType: data.deviceType,
tenantId: data.tenantId,
};
deviceDel(params).then((res) => {
if (res.code === 200) {
ElMessage({
message: res.msg,
type: 'success',
});
getDataList();
} else {
ElMessage.error(res.msg);
}
});
});
};
const getRowKey = (row) => {
return row.id;
};
const onShowDialog = (tenantId, deviceType) => {
console.log('=== onShowDialog 被调用 ===', { tenantId, deviceType });
data.dialogVisible = true;
data.activeName = 'first';
data.deviceType = deviceType;
data.allotType = '0';
data.tenantId = tenantId;
console.log('=== 设置后的数据 ===', {
deviceType: data.deviceType,
allotType: data.allotType,
tenantId: data.tenantId
});
getDataList();
if (multipleTableUnRef.value) {
multipleTableUnRef.value.clearSelection();
}
if (multipleTableRef.value) {
multipleTableRef.value.clearSelection();
}
};
const handleClose = () => {
data.dialogVisible = false;
emits('success');
};
defineExpose({
onShowDialog,
});
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,213 @@
<template>
<el-dialog v-model="dialogVisible" :before-close="handleClose" style="width: 650px; padding-bottom: 20px" :title="title" :destroy-on-close="true">
<el-form ref="FormDataRef" :model="form" label-width="100" :rules="formRules">
<el-form-item label="岗位名称" prop="name" v-if="isTrue">
<el-input v-model="form.name" placeholder="请输入岗位名称" />
</el-form-item>
<el-row v-if="isTrue">
<el-col :span="24">
<el-form-item label="菜单权限">
<el-checkbox v-model="menuExpand" @change="handleCheckedTreeExpand">展开/折叠 </el-checkbox>
<el-checkbox v-model="menuNodeAll" @change="handleCheckedTreeNodeAll"> 全选/全不选 </el-checkbox>
<div class="border w-full py-1 max-h-[300px] overflow-y-auto">
<el-tree
ref="menuRef"
:data="treeData"
:default-expand-all="false"
:props="{ label: 'name' }"
node-key="id"
show-checkbox
style="width: 100%"
/>
</div>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="岗位描述" prop="description">
<el-input maxlength="100" v-model="form.description" placeholder="请输入岗位描述" :rows="2" type="textarea" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button :loading="submitting" type="primary" @click="onClickSave">保存</el-button>
<el-button @click="handleClose">取消</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { getCurrentInstance, reactive, ref, nextTick } from 'vue';
import { ElMessage } from 'element-plus';
import { savePositionMenu, menuListByRoleId } from '~/api/sys.js';
import { useUserStore } from '../../store/user';
const userStore = useUserStore();
const { proxy } = getCurrentInstance();
const emits = defineEmits(['success']);
const menuRef = ref(null);
const dialogVisible = ref(false);
const submitting = ref(false);
const FormDataRef = ref(null);
const roleId = ref('');
const treeData = ref([]);
const menuExpand = ref(false);
const menuNodeAll = ref(false);
const title = ref('');
const isTrue = ref(true);
const form = reactive({
id: '',
name: '',
description: '',
type: '',
menuIds: [],
});
const formRules = reactive({
name: [{ required: true, message: '岗位名称不能为空', trigger: 'blur' }],
description: [{ required: true, message: '岗位描述不能为空', trigger: 'blur' }],
});
// 展开/折叠
function handleCheckedTreeExpand(value) {
if (!menuRef.value || !treeData.value || !Array.isArray(treeData.value)) {
return;
}
const treeList = treeData.value;
for (let i = 0; i < treeList.length; i++) {
if (menuRef.value.store && menuRef.value.store.nodesMap && menuRef.value.store.nodesMap[treeList[i].id]) {
menuRef.value.store.nodesMap[treeList[i].id].expanded = value;
}
}
}
/** 树权限(全选/全不选) */
function handleCheckedTreeNodeAll(value) {
if (!menuRef.value) {
return;
}
// 确保treeData是数组
const menuData = Array.isArray(treeData.value) ? treeData.value : [];
menuRef.value.setCheckedNodes(value ? menuData : []);
}
const handleClose = () => {
if (FormDataRef.value) {
FormDataRef.value.clearValidate();
}
menuExpand.value = false;
menuNodeAll.value = false;
submitting.value = false;
// 强制关闭对话框
dialogVisible.value = false;
// 重置表单数据
form.id = '';
form.name = '';
form.description = '';
form.type = '';
form.menuIds = [];
// 清空树数据
treeData.value = [];
};
function getMenuAllCheckedKeys() {
// 确保menuRef存在
if (!menuRef.value) {
return [];
}
// 目前被选中的菜单节点
const checkedKeys = menuRef.value.getCheckedKeys();
// 半选中的菜单节点
const halfCheckedKeys = menuRef.value.getHalfCheckedKeys();
checkedKeys.unshift.apply(checkedKeys, halfCheckedKeys);
return checkedKeys;
}
const onClickSave = () => {
if (FormDataRef.value) {
FormDataRef.value.validate((valid) => {
if (valid) {
submitting.value = true;
const params = { ...form };
params.menuIds = getMenuAllCheckedKeys();
savePositionMenu(params)
.then((res) => {
submitting.value = false;
if (res.code === 200) {
ElMessage.success('保存成功');
// 使用 nextTick 确保消息显示后再关闭对话框
nextTick(() => {
handleClose();
emits('success');
});
} else {
ElMessage.warning(res.msg || '保存失败');
}
})
.catch((error) => {
submitting.value = false;
ElMessage.error('保存失败,请重试');
});
}
});
}
};
const open = async (row) => {
dialogVisible.value = true;
// 等待DOM更新完成
await nextTick();
if (row) {
roleId.value = row.id;
title.value = `${row.name}分配菜单`;
isTrue.value = false;
form.id = row.id;
form.name = row.name;
const { data } = await menuListByRoleId(row.id);
treeData.value = data.menuList || [];
// 确保menuRef存在后再调用
if (menuRef.value) {
menuRef.value.setCheckedKeys([]);
// 触发一次树形结构的更新
setTimeout(() => {
if (menuRef.value) {
menuRef.value.setCheckedKeys([]);
}
}, 10);
}
} else {
// 新增岗位的情况
roleId.value = '';
title.value = '新增岗位';
isTrue.value = true;
form.id = '';
form.name = '';
form.description = '';
const { data } = await menuListByRoleId(null);
treeData.value = data.menuList || [];
// 确保menuRef存在后再调用
if (menuRef.value) {
menuRef.value.setCheckedKeys([]);
}
}
};
defineExpose({
open,
});
</script>
<style lang="less" scoped>
.border {
border: 1px solid #dcdfe6;
border-radius: 4px;
}
</style>

View File

@@ -0,0 +1,120 @@
<template>
<div>
<base-search :formItemList="formItemList" @search="searchFrom" ref="baseSearchRef" @change="searchChange"> </base-search>
<div style="display: flex; padding: 10px; background: #fff; margin-bottom: 10px">
<el-button type="primary" @click="showMenuDialog(null)">新增岗位</el-button>
</div>
<div class="main-container">
<el-table v-loading="dataListLoading" :data="data.rows" border element-loading-text="数据加载中...">
<el-table-column label="岗位名称" prop="name"></el-table-column>
<el-table-column label="岗位描述" prop="description" show-overflow-tooltip> </el-table-column>
<el-table-column label="创建时间" prop="createTime"></el-table-column>
<el-table-column label="操作" width="130">
<template #default="scope">
<div class="table_column_operation">
<a @click="showMenuDialog(scope.row, 1)" v-if="scope.row.isAdmin == 1">编辑</a>
<a @click="showMenuDialog(scope.row, 2)" v-if="scope.row.isAdmin !== 1">编辑</a>
<a @click="del(scope.row.id)" class="delStyle" v-if="scope.row.isAdmin !== 1">删除</a>
</div>
</template>
</el-table-column>
<template #empty>
<div class="dataListOnEmpty">暂无数据</div>
</template>
</el-table>
<pagination v-model:limit="form.pageSize" v-model:page="form.pageNum" :total="data.total" @pagination="getDataList" />
</div>
<Add ref="addRef" @success="getDataList" />
<menuDialog ref="menuRef" @success="getDataList" />
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import baseSearch from '@/components/common/searchCustom/index.vue';
import { sysPositionList, sysPositionDel } from '~/api/sys.js';
import Add from './addOrEditPost.vue';
import menuDialog from './menu.vue';
const baseSearchRef = ref();
const addRef = ref();
const menuRef = ref();
const dataListLoading = ref(false);
const form = reactive({
pageNum: 1,
pageSize: 10,
});
const data = reactive({
rows: [],
total: 0,
});
const formItemList = reactive([
{
label: '岗位名称',
type: 'input',
param: 'name',
labelWidth: 65,
span: 6,
},
]);
const searchFrom = () => {
form.pageNum = 1;
getDataList();
};
const searchChange = (val) => {
console.log('Search change:', val);
};
const getDataList = () => {
dataListLoading.value = true;
const params = {
...form,
...baseSearchRef.value.penetrateParams(),
};
sysPositionList(params)
.then((ret) => {
data.rows = ret.data.rows;
data.total = ret.data.total;
dataListLoading.value = false;
})
.catch(() => {
dataListLoading.value = false;
});
};
// 新增 、编辑
const showAddDialog = (val) => {
if (addRef.value) {
addRef.value.onShowDialog(val);
}
};
// 设置权限
const showMenuDialog = (val) => {
if (menuRef.value) {
menuRef.value.open(val);
}
};
// 删除
const del = (id) => {
ElMessageBox.confirm('请确认是否删除', '提示', {
cancelButtonText: '取消',
confirmButtonText: '确定',
type: 'warning',
}).then(() => {
sysPositionDel(id).then(() => {
ElMessage.success('操作成功');
getDataList();
});
});
};
onMounted(() => {
getDataList();
});
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,128 @@
<template>
<div>
<base-search :formItemList="formItemList" @search="searchFrom" ref="baseSearchRef" @change="searchChange"> </base-search>
<div style="display: flex; padding: 10px; background: #fff; margin-bottom: 10px">
<el-button type="primary" @click="showAddOrEditDialog(null, null)">新增员工</el-button>
</div>
<div class="main-container">
<el-table v-loading="dataListLoading" :data="data.rows" border element-loading-text="数据加载中...">
<el-table-column label="员工姓名" prop="name"></el-table-column>
<el-table-column label="岗位" prop="roleName"> </el-table-column>
<el-table-column label="手机号" prop="mobile"></el-table-column>
<el-table-column label="账号状态">
<template #default="scope">
<el-switch
v-model="scope.row.status"
:active-value="1"
:inactive-value="0"
active-color="#13ce66"
inactive-color="#ff4949"
@change="changeStatus(scope.row)"
/>
</template>
</el-table-column>
<el-table-column label="创建时间" prop="createTime"></el-table-column>
<el-table-column label="操作" width="120">
<template #default="scope">
<div class="table_column_operation">
<a @click="showAddOrEditDialog(scope.row, 1)">编辑</a>
<!-- <a @click="showAddOrEditDialog(scope.row, 2)">修改密码</a> -->
<!-- <a @click="del(scope.row.id)" class="delStyle">删除</a> -->
</div>
</template>
</el-table-column>
<template #empty>
<div class="dataListOnEmpty">暂无数据</div>
</template>
</el-table>
<pagination v-model:limit="form.pageSize" v-model:page="form.pageNum" :total="data.total" @pagination="getDataList" />
</div>
<Add ref="addRef" @success="getDataList" />
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import baseSearch from '@/components/common/searchCustom/index.vue';
import { sysUserList, sysUserSave, sysUserDel } from '@/api/sys.js';
import Add from './addOrEditStaff.vue';
const baseSearchRef = ref();
const addRef = ref();
const dataListLoading = ref(false);
const form = reactive({
pageNum: 1,
pageSize: 20,
type: 1,
});
const data = reactive({
rows: [],
total: 20,
});
const formItemList = reactive([
{
label: '员工姓名',
type: 'input',
param: 'name',
},
]);
const searchFrom = () => {
form.pageNum = 1;
getDataList();
};
const searchChange = (val) => {
console.log(val);
};
const getDataList = () => {
dataListLoading.value = true;
const params = {
...form,
...baseSearchRef.value.penetrateParams(),
};
sysUserList(params)
.then((ret) => {
data.rows = ret.data.rows;
data.total = ret.data.total;
dataListLoading.value = false;
})
.catch(() => {
dataListLoading.value = false;
});
};
// 新增 、编辑、修改密码
const showAddOrEditDialog = (val, page) => {
if (addRef.value) {
addRef.value.onShowDialog(val, page);
}
};
const changeStatus = (row) => {
const params = {
id: row.id,
status: row.status == false ? 0 : 1,
};
sysUserSave(params).then((ret) => {
ElMessage.success('操作成功');
});
};
// 删除
const del = (id) => {
ElMessageBox.confirm('账号删除后无法登录,是否确定删除?', '提示', {
cancelButtonText: '取消',
confirmButtonText: '确定',
type: 'warning',
}).then(() => {
sysUserDel(id).then(() => {
ElMessage.success('操作成功');
getDataList();
});
});
};
onMounted(() => {
form.type = 1;
getDataList();
});
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,113 @@
<template>
<div>
<base-search :formItemList="formItemList" @search="searchFrom" ref="baseSearchRef"> </base-search>
<div style="display: flex; padding: 10px; background: #fff; margin-bottom: 10px">
<el-button type="primary" @click="showAddDialog(null)">新增租户</el-button>
</div>
<div class="main-container">
<el-table :data="data.rows" border v-loading="data.dataListLoading" element-loading-text="数据加载中..." style="width: 100%">
<el-table-column label="租户姓名" prop="name"> </el-table-column>
<el-table-column label="手机号" prop="mobile"> </el-table-column>
<el-table-column label="智能耳标" prop="jbqCount"> </el-table-column>
<el-table-column label="智能项圈" prop="xqCount"> </el-table-column>
<el-table-column label="智能主机" prop="serverCount"> </el-table-column>
<el-table-column label="创建时间" prop="createTime"> </el-table-column>
<el-table-column label="操作" width="340">
<template #default="scope">
<el-button link type="primary" @click="showAddDialog(scope.row)">编辑</el-button>
<el-button link type="primary" @click="del(scope.row.id)">删除</el-button>
<el-button link type="primary" @click="assign(scope.row.id, '1')">耳标分配</el-button>
<el-button link type="primary" @click="assign(scope.row.id, '2')">项圈分配</el-button>
<el-button link type="primary" @click="assign(scope.row.id, '3')">主机分配</el-button>
</template>
</el-table-column>
<template #empty>
<div class="dataListOnEmpty">暂无数据</div>
</template>
</el-table>
<pagination v-model:limit="form.pageSize" v-model:page="form.pageNum" :total="data.total" @pagination="getDataList" />
<TenantDialog ref="TenantDialogRef" @success="getDataList" />
<AssignDialog ref="AssignDialogRef" @success="getDataList" />
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import baseSearch from '@/components/common/searchCustom/index.vue';
import { tenantList, tenantDel } from '@/api/sys.js';
import TenantDialog from './tenantDialog.vue';
import AssignDialog from './assignDevice.vue';
const baseSearchRef = ref();
const TenantDialogRef = ref();
const AssignDialogRef = ref();
const formItemList = reactive([
{
label: '租户姓名',
type: 'input',
param: 'name',
placeholder: '请输入租户姓名',
},
]);
const data = reactive({
rows: [],
total: 0,
dataListLoading: false,
});
const form = reactive({
pageNum: 1,
pageSize: 10,
});
const searchFrom = () => {
form.pageNum = 1;
getDataList();
};
// 列表
const getDataList = () => {
data.dataListLoading = true;
const params = {
...form,
...baseSearchRef.value.penetrateParams(),
};
tenantList(params)
.then((res) => {
data.dataListLoading = false;
data.rows = res.data.rows;
data.total = res.data.total;
})
.catch(() => {
data.dataListLoading = false;
});
};
// 删除
const del = (id) => {
ElMessageBox.confirm('请确认是否删除', '提示', {
cancelButtonText: '取消',
confirmButtonText: '确定',
type: 'warning',
}).then(() => {
tenantDel(id).then(() => {
ElMessage.success('操作成功');
getDataList();
});
});
};
// 分配弹层
const assign = (id, type) => {
if (AssignDialogRef.value) {
AssignDialogRef.value.onShowDialog(id, type);
}
};
const showAddDialog = (row) => {
if (TenantDialogRef.value) {
TenantDialogRef.value.onShowDialog(row);
}
};
onMounted(() => {
getDataList();
});
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,106 @@
<template>
<el-dialog v-model="data.dialogVisible" :title="data.title" :before-close="handleClose" style="width: 450px; padding-bottom: 20px">
<el-form ref="formDataRef" :model="ruleForm" :rules="rules" label-width="auto">
<el-form-item label="租户姓名" prop="name">
<el-input v-model="ruleForm.name" placeholder="请输入租户姓名" clearable></el-input>
</el-form-item>
<el-form-item label="手机号" prop="mobile">
<el-input v-model="ruleForm.mobile" placeholder="请输入手机号" clearable></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button :loading="data.saveLoading" type="primary" @click="onClickSave">保存</el-button>
<el-button @click="handleClose">取消</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { checkMobile } from '~/utils/validateFuns.js';
import { tenantSave } from '@/api/sys.js';
const formDataRef = ref();
const emits = defineEmits(['success']);
const ruleForm = reactive({
name: '',
mobile: '',
});
const rules = reactive({
name: [{ required: true, message: '请输入租户姓名', trigger: 'blur' }],
mobile: [
{
required: true,
validator(rule, value, callback) {
if (!value) {
callback(new Error('请输入手机号'));
}
if (!checkMobile(value)) {
callback(new Error('请输入正确的手机号'));
}
callback();
},
trigger: 'blur',
},
],
});
const data = reactive({
dialogVisible: false,
title: '',
saveLoading: false,
});
const handleClose = () => {
data.dialogVisible = false;
if (formDataRef.value) {
formDataRef.value.resetFields();
}
};
const onClickSave = () => {
if (formDataRef.value) {
formDataRef.value.validate((valid) => {
if (valid) {
data.dataListLoading = true;
tenantSave(ruleForm)
.then((res) => {
ElMessage({
type: 'success',
message: res.msg,
});
emits('success');
if (formDataRef.value) {
formDataRef.value.resetFields();
}
data.dialogVisible = false;
})
.catch((err) => {});
} else {
console.log('error submit!');
}
});
}
};
const onShowDialog = (row) => {
data.title = row ? '编辑租户' : '新增租户';
data.dialogVisible = true;
if (formDataRef.value) {
formDataRef.value.resetFields();
}
if (row) {
nextTick(() => {
ruleForm.id = row.id;
ruleForm.name = row.name;
ruleForm.mobile = row.mobile;
});
} else {
ruleForm.id = '';
}
};
defineExpose({
onShowDialog,
});
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,87 @@
<template>
<div>
<base-search :formItemList="formItemList" @search="searchFrom" ref="baseSearchRef"> </base-search>
<div style="display: flex; padding: 10px; background: #fff; margin-bottom: 10px">
<el-button type="primary" @click="showAddDialog(null)">新增用户</el-button>
</div>
<div class="main-container">
<el-table :data="data.rows" border v-loading="data.dataListLoading" element-loading-text="数据加载中..." style="width: 100%">
<el-table-column label="用户姓名" prop="id"></el-table-column>
<el-table-column label="用户手机号" prop="roleName"> </el-table-column>
<el-table-column label="用户类型" prop="mobile"></el-table-column>
<el-table-column label="账号状态" prop="name"></el-table-column>
<el-table-column label="备注" prop="roleName"> </el-table-column>
<el-table-column label="创建人" prop="mobile"></el-table-column>
<el-table-column label="创建时间" prop="name"></el-table-column>
<el-table-column label="操作" width="120">
<template #default="scope">
<el-button link type="primary" @click="showAddDialog(scope.row)">编辑111</el-button>
<el-button link type="primary" @click="delClick(scope.row)">删除111</el-button>
</template>
</el-table-column>
<template #empty>
<div class="dataListOnEmpty">暂无数据</div>
</template>
</el-table>
<pagination v-model:limit="form.pageSize" v-model:page="form.pageNum" :total="data.total" @pagination="getDataList" />
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import baseSearch from '@/components/common/searchCustom/index.vue';
const baseSearchRef = ref();
const formItemList = reactive([
{
label: '用户姓名',
prop: 'username',
type: 'input',
placeholder: '请输入用户名',
span: 7,
labelWidth: 100,
},
{
label: '用户手机号',
prop: 'phone',
type: 'input',
placeholder: '请输入用户手机号',
span: 7,
labelWidth: 100,
},
{
label: '用户类型',
type: 'select',
selectOptions: [
{ value: 1, text: '供应商' },
{ value: 2, text: '资金方' },
{ value: 3, text: '采购商' },
],
param: 'type',
span: 7,
labelWidth: 100,
},
]);
const searchFrom = () => {
form.pageNum = 1;
getDataList();
};
const data = reactive({
rows: [
{
id: '1',
},
],
total: 0,
dataListLoading: false,
});
const form = reactive({
pageNum: 1,
pageSize: 10,
});
// 列表
const getDataList = () => {};
</script>
<style lang="less" scoped></style>

View File

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

View File

@@ -0,0 +1,198 @@
<template>
<el-dialog v-model="data.dialogVisible" title="详情" style="width: 800px; padding-bottom: 20px" :key="data.dialogKey">
<div v-loading="data.loading" element-loading-text="加载中...">
<el-row :gutter="40" class="el-row">
<el-col :span="12"><span class="label">司机姓名</span>{{ data.info.username || '' }}</el-col>
<el-col :span="12"><span class="label">司机手机号</span>{{ data.info.mobile || '' }}</el-col>
</el-row>
<el-row :gutter="40">
<el-col :span="12"><span class="label">账号状态</span>{{ data.info.status == 1 ? '启用' : '禁用' }}</el-col>
<el-col :span="12"><span class="label">车牌号</span>{{ data.info.car_number || '' }}</el-col>
</el-row>
<el-row :gutter="40">
<el-col :span="12" style="display: flex">
<div><span class="label">驾驶证</span></div>
<template v-if="data.info.driver_license">
<el-image
v-for="(item, index) in getImageList(data.info.driver_license)"
:key="index"
:src="item"
style="width: 80px; height: 80px; margin-right: 10px"
fit="cover"
:preview-src-list="getImageList(data.info.driver_license)"
preview-teleported
>
<template #error>
<div style="width: 50px; height: 50px; display: flex; justify-content: center" class="image-slot">
<el-icon :size="20"><Picture /></el-icon>
</div>
</template>
</el-image>
</template>
<span v-else style="color: #999">暂无图片</span>
</el-col>
<el-col :span="12" style="display: flex">
<div><span class="label">行驶证</span></div>
<template v-if="data.info.driving_license">
<el-image
v-for="(item, index) in getImageList(data.info.driving_license)"
:key="index"
:src="item"
style="width: 80px; height: 80px; margin-right: 10px"
fit="cover"
:preview-src-list="getImageList(data.info.driving_license)"
preview-teleported
>
<template #error>
<div style="width: 50px; height: 50px; display: flex; justify-content: center" class="image-slot">
<el-icon :size="20"><Picture /></el-icon>
</div>
</template>
</el-image>
</template>
<span v-else style="color: #999">暂无图片</span>
</el-col>
</el-row>
<el-row :gutter="40">
<el-col :span="12" style="display: flex"
><span class="label">牧运通备案码</span>
<template v-if="data.info.record_code">
<el-image
v-for="(item, index) in getImageList(data.info.record_code)"
:key="index"
:src="item"
style="width: 80px; height: 80px; margin-right: 10px"
fit="cover"
:preview-src-list="getImageList(data.info.record_code)"
preview-teleported
>
<template #error>
<div style="width: 50px; height: 50px; display: flex; justify-content: center" class="image-slot">
<el-icon :size="20"><Picture /></el-icon>
</div>
</template>
</el-image>
</template>
<span v-else style="color: #999">暂无图片</span>
</el-col>
<el-col :span="12" style="display: flex">
<div><span class="label">车头&车身照片</span></div>
<template v-if="data.info.car_img">
<el-image
v-for="(item, index) in getImageList(data.info.car_img)"
:key="index"
:src="item"
style="width: 80px; height: 80px; margin-right: 10px"
fit="cover"
:preview-src-list="getImageList(data.info.car_img)"
preview-teleported
>
<template #error>
<div style="width: 50px; height: 50px; display: flex; justify-content: center" class="image-slot">
<el-icon :size="20"><Picture /></el-icon>
</div>
</template>
</el-image>
</template>
<span v-else style="color: #999">暂无图片</span>
</el-col>
</el-row>
<el-row :gutter="40">
<el-col :span="12" style="display: flex">
<div><span class="label">身份证前后面</span></div>
<template v-if="data.info.id_card">
<el-image
v-for="(item, index) in getImageList(data.info.id_card)"
:key="index"
:src="item"
style="width: 80px; height: 80px; margin-right: 10px"
fit="cover"
:preview-src-list="getImageList(data.info.id_card)"
preview-teleported
>
<template #error>
<div style="width: 50px; height: 50px; display: flex; justify-content: center" class="image-slot">
<el-icon :size="20"><Picture /></el-icon>
</div>
</template>
</el-image>
</template>
<span v-else style="color: #999">暂无图片</span>
</el-col>
</el-row>
<el-row :gutter="40"
><el-col :span="12"><span class="label">备注</span>{{ data.info.remark || '无' }}</el-col>
</el-row>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive } from 'vue';
import { driverDetail } from '@/api/userManage.js';
const data = reactive({
dialogVisible: false,
info: {},
loading: false,
dialogKey: 0,
});
const handleClose = () => {
data.dialogVisible = false;
};
const onShowDetailDialog = async (row) => {
data.dialogVisible = true;
data.loading = true;
data.dialogKey++; // 强制重新渲染对话框
if (row && row.id) {
try {
// 根据ID重新获取最新的司机数据
const res = await driverDetail(row.id);
if (res.code === 200) {
data.info = res.data;
} else {
// 如果API调用失败使用传入的row数据作为备选
data.info = row;
}
} catch (error) {
console.error('获取司机详情失败:', error);
// 如果API调用失败使用传入的row数据作为备选
data.info = row;
}
} else {
data.info = row || {};
}
data.loading = false;
};
// 处理逗号分隔的图片URL
const getImageList = (imageUrl) => {
if (!imageUrl || imageUrl.trim() === '') {
return [];
}
// 按逗号分割并过滤空字符串
return imageUrl.split(',').map(url => url.trim()).filter(url => url !== '');
};
defineExpose({
onShowDetailDialog,
});
</script>
<style lang="less" scoped>
.el-row {
margin-bottom: 20px;
}
.label {
width: 120px;
display: inline-block;
text-align: right;
}
</style>

View File

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

View File

@@ -0,0 +1,187 @@
<template>
<div>
<base-search :formItemList="formItemList" @search="searchFrom" ref="baseSearchRef"> </base-search>
<div style="display: flex; padding: 10px; background: #fff; margin-bottom: 10px">
<el-button type="primary" @click="showAddDialog(null)">新增用户</el-button>
</div>
<div class="main-container">
<el-table :data="data.rows" border v-loading="data.dataListLoading" element-loading-text="数据加载中..." style="width: 100%">
<el-table-column label="用户姓名" prop="name"></el-table-column>
<el-table-column label="用户手机号" prop="mobile"> </el-table-column>
<el-table-column label="用户类型" prop="type">
<template #default="scope"> {{ getRoleName(scope.row.type) }} </template>
</el-table-column>
<el-table-column label="CBK账户" prop="cbkAccount">
<template #default="scope">
<template v-if="scope.row.cbkAccount">
<el-tooltip :content="scope.row.cbkAccount" placement="top">
<span style="cursor: help">****</span>
</el-tooltip>
</template>
<template v-else>
--
</template>
</template>
</el-table-column>
<el-table-column label="账号状态" prop="status">
<template #default="scope"> {{ scope.row.status == 0 ? '启用' : '禁用' }} </template>
</el-table-column>
<el-table-column label="备注" prop="remark">
<template #default="scope"> {{ scope.row.remark || '--' }} </template>
</el-table-column>
<el-table-column label="创建人" prop="createName">
<template #default="scope"> {{ scope.row.createName || '--' }} </template>
</el-table-column>
<el-table-column label="创建时间" prop="createTime"></el-table-column>
<el-table-column label="操作" width="140">
<template #default="scope">
<el-button link type="primary" @click="showAddDialog(scope.row)">编辑</el-button>
<!-- <el-button link type="primary" @click="delClick(scope.row)">删除</el-button> -->
</template>
</el-table-column>
<template #empty>
<div class="dataListOnEmpty">暂无数据</div>
</template>
</el-table>
<pagination v-model:limit="form.pageSize" v-model:page="form.pageNum" :total="data.total" @pagination="getDataList" />
<UserDialog ref="UserDialogRef" @success="getDataList" />
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import baseSearch from '@/components/common/searchCustom/index.vue';
import { userList, userStatus } from '@/api/userManage.js';
import UserDialog from './userDialog.vue';
const baseSearchRef = ref();
const UserDialogRef = ref();
const formItemList = reactive([
{
label: '用户姓名',
param: 'username',
type: 'input',
placeholder: '请输入用户名',
span: 7,
labelWidth: 100,
},
{
label: '精确姓名',
param: 'usernameExact',
type: 'select',
selectOptions: [
{ value: true, text: '是' },
{ value: false, text: '否' },
],
span: 4,
labelWidth: 80,
},
{
label: '用户手机号',
param: 'mobile',
type: 'input',
placeholder: '请输入用户手机号',
span: 7,
labelWidth: 100,
},
{
label: '精确手机号',
param: 'mobileExact',
type: 'select',
selectOptions: [
{ value: true, text: '是' },
{ value: false, text: '否' },
],
span: 4,
labelWidth: 90,
},
{
label: '用户类型',
type: 'select',
selectOptions: [
{ value: 1, text: '司机' },
{ value: 2, text: '供应商' },
{ value: 3, text: '资金方' },
{ value: 4, text: '采购商' },
],
param: 'type',
span: 7,
labelWidth: 100,
},
]);
const searchFrom = () => {
form.pageNum = 1;
getDataList();
};
const data = reactive({
rows: [],
total: 0,
dataListLoading: false,
});
const form = reactive({
pageNum: 1,
pageSize: 10,
});
const typeOptions = {
1: '司机',
2: '供应商',
3: '资金方',
4: '采购商',
};
// 根据type获取用户类型名称
const getRoleName = (type) => {
if (type) {
return typeOptions[type] || `未知类型${type}`;
}
return '--';
};
// 列表
const getDataList = () => {
data.dataListLoading = true;
const params = {
...form,
...baseSearchRef.value.penetrateParams(),
};
userList(params)
.then((res) => {
data.dataListLoading = false;
data.rows = res.data.rows;
data.total = res.data.total;
})
.catch(() => {
data.dataListLoading = false;
});
};
// 删除
const delClick = (row) => {
ElMessageBox.confirm('请确认是否删除该条数据?', '删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error',
})
.then(() => {
ElMessage({
type: 'success',
message: '删除成功',
});
})
.catch(() => {
ElMessage({
type: 'info',
message: '取消删除',
});
});
};
const showAddDialog = (row) => {
if (UserDialogRef.value) {
UserDialogRef.value.onShowDialog(row);
}
};
onMounted(() => {
getDataList();
});
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,210 @@
<template>
<el-dialog v-model="data.dialogVisible" :title="data.title" :before-close="handleClose" style="width: 550px; padding-bottom: 20px">
<el-form ref="formDataRef" :model="ruleForm" :rules="rules" label-width="auto">
<el-form-item label="用户姓名" prop="username">
<el-input v-model="ruleForm.username" placeholder="请输入用户姓名" clearable></el-input>
</el-form-item>
<el-form-item label="用户手机号" prop="mobile">
<el-input v-model="ruleForm.mobile" placeholder="请输入用户手机号" clearable></el-input>
</el-form-item>
<el-form-item label="用户类型" prop="type">
<el-select v-model="ruleForm.type" clearable placeholder="请选择用户类型" style="width: 100%">
<el-option v-for="item in data.typeOptions" :key="item.id" :label="item.label" :value="item.value"> </el-option>
</el-select>
</el-form-item>
<el-form-item label="CBK账号" prop="cbkAccount">
<el-input
v-model="ruleForm.cbkAccount"
:type="data.showCbkAccount ? 'text' : 'password'"
placeholder="请输入CBK账号"
clearable
@click="data.showCbkAccount = true"
@blur="data.showCbkAccount = false"
>
<template #suffix>
<el-icon @click="data.showCbkAccount = !data.showCbkAccount" style="cursor: pointer;">
<View v-if="!data.showCbkAccount" />
<Hide v-else />
</el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="账号状态" prop="status">
<el-radio-group v-model="ruleForm.status">
<el-radio :value="0">启用</el-radio>
<el-radio :value="1">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="ruleForm.remark" placeholder="请输入备注" clearable></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button :loading="data.saveLoading" type="primary" @click="onClickSave">保存</el-button>
<el-button @click="handleClose">取消</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, onMounted, nextTick } from 'vue';
import { userAdd, userEdit } from '@/api/userManage.js';
import { checkMobile } from '~/utils/validateFuns.js';
import { View, Hide } from '@element-plus/icons-vue';
const emits = defineEmits();
const formDataRef = ref();
const data = reactive({
dialogVisible: false,
title: '',
typeOptions: [
{
value: 1,
label: '司机',
},
{
value: 2,
label: '供应商',
},
{
value: 3,
label: '资金方',
},
{
value: 4,
label: '采购商',
},
],
saveLoading: false,
showCbkAccount: false, // 控制CBK账号显示/隐藏
});
const ruleForm = reactive({
username: '', // 用户姓名
mobile: '', // 用户手机号
type: '', // 用户类型 2供应商 3资金方 4采购商
cbkAccount: '', // CBK账号
status: '', // 账号状态 1是0否
remark: '', // 备注
});
const rules = reactive({
username: [{ required: true, message: '请输入用户姓名', trigger: 'blur' }],
mobile: [
{
required: true,
validator(rule, value, callback) {
if (!value) {
callback(new Error('请输入用户手机号'));
}
if (!checkMobile(value)) {
callback(new Error('请输入正确的手机号'));
}
callback();
},
trigger: 'blur',
},
],
type: [{ required: true, message: '请选择用户类型', trigger: 'change' }],
cbkAccount: [{ required: true, message: '请输入CBK账号', trigger: 'blur' }],
status: [{ required: true, message: '请选择账户状态', trigger: 'change' }],
});
const handleClose = () => {
if (formDataRef.value) {
formDataRef.value.resetFields();
}
data.dialogVisible = false;
};
// 保存按钮
const onClickSave = () => {
if (formDataRef.value) {
formDataRef.value.validate((valid) => {
if (valid) {
data.saveLoading = true;
if (data.title === '新增用户') {
const params = { ...ruleForm };
userAdd(params)
.then((res) => {
data.saveLoading = false;
if (res.code === 200) {
ElMessage({
message: res.msg,
type: 'success',
});
emits('success');
if (formDataRef.value) {
formDataRef.value.resetFields();
data.dialogVisible = false;
} else {
data.saveLoading = false;
ElMessage.error(res.msg);
}
}
})
.catch((err) => {
data.saveLoading = false;
});
} else {
const params = { ...ruleForm };
userEdit(params)
.then((res) => {
data.saveLoading = false;
if (res.code === 200) {
ElMessage({
message: res.msg,
type: 'success',
});
emits('success');
if (formDataRef.value) {
formDataRef.value.resetFields();
data.dialogVisible = false;
} else {
data.saveLoading = false;
ElMessage.error(res.msg);
}
}
})
.catch((err) => {
data.saveLoading = false;
console.error('编辑用户失败:', err);
ElMessage.error('编辑用户失败,请检查网络连接或权限设置');
});
}
} else {
console.log('error submit!');
}
});
}
};
const onShowDialog = (row) => {
data.title = row ? '编辑用户' : '新增用户';
data.dialogVisible = true;
data.showCbkAccount = false; // 重置CBK账号显示状态
if (formDataRef.value) {
formDataRef.value.resetFields();
}
if (row) {
nextTick(() => {
ruleForm.id = row.id;
// 字段绑定说明:
// 用户姓名 -> member_user.username
// 用户手机号 -> member.mobile (通过member_id关联)
// 用户类型 -> member.type (通过member_id关联)
// 账号状态 -> member.status (通过member_id关联0=启用1=禁用)
ruleForm.username = row.name || row.username; // 用户姓名 - 绑定到member_user.username
ruleForm.mobile = row.mobile; // 用户手机号 - 绑定到member.mobile
ruleForm.type = row.type || row.roleId; // 用户类型 - 绑定到member.type
ruleForm.cbkAccount = row.cbkAccount || ''; // CBK账号 - 绑定到member_user.cbk_account
ruleForm.status = row.status; // 账号状态 - 绑定到member.status (0=启用1=禁用)
ruleForm.remark = row.remark || ''; // 备注 - 绑定到member_user.remark
});
}
};
defineExpose({
onShowDialog,
});
</script>
<style lang="less" scoped></style>