完成中转仓管理
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"axios": "^0.27.2",
|
||||
"copy-webpack-plugin": "^4.6.0",
|
||||
"d3-geo": "^3.1.1",
|
||||
"echarts": "^5.5.0",
|
||||
"element-china-area-data": "^6.1.0",
|
||||
"element-plus": "^2.2.17",
|
||||
@@ -39,6 +40,7 @@
|
||||
"pinia": "^2.0.22",
|
||||
"pinia-plugin-persist": "^1.0.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"three": "^0.181.2",
|
||||
"vue": "^3.2.37",
|
||||
"vue-baidu-map-3x": "^1.0.38",
|
||||
"vue-chartjs": "^5.3.0",
|
||||
@@ -53,7 +55,9 @@
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.1.2",
|
||||
"@commitlint/config-conventional": "^17.1.0",
|
||||
"@types/d3-geo": "^3.1.0",
|
||||
"@types/node": "^18.19.20",
|
||||
"@types/three": "^0.181.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.38.1",
|
||||
"@typescript-eslint/parser": "^5.38.1",
|
||||
"@vitejs/plugin-vue": "^3.1.0",
|
||||
|
||||
35
pc-cattle-transportation/src/api/datav.js
Normal file
35
pc-cattle-transportation/src/api/datav.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import request from '@/utils/axios.ts';
|
||||
|
||||
/**
|
||||
* 获取大屏数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getDatavData() {
|
||||
// 模拟API响应
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
code: 200,
|
||||
msg: 'success',
|
||||
data: {
|
||||
// 运输路线数据:[起点经度, 起点纬度, 终点经度, 终点纬度, 路线名称]
|
||||
routes: [
|
||||
{ from: [116.4074, 39.9042], to: [121.4737, 31.2304], name: '北京 -> 上海' },
|
||||
{ from: [113.2644, 23.1291], to: [116.4074, 39.9042], name: '广州 -> 北京' },
|
||||
{ from: [87.6168, 43.8256], to: [121.4737, 31.2304], name: '乌鲁木齐 -> 上海' },
|
||||
{ from: [104.0665, 30.5728], to: [114.3055, 30.5928], name: '成都 -> 武汉' },
|
||||
{ from: [108.9402, 34.3416], to: [113.6253, 34.7466], name: '西安 -> 郑州' },
|
||||
{ from: [126.53, 45.80], to: [116.4074, 39.9042], name: '哈尔滨 -> 北京' }
|
||||
],
|
||||
// 统计数据
|
||||
stats: {
|
||||
totalTransport: 12580,
|
||||
activeVehicles: 342,
|
||||
totalDistance: 892300
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
53
pc-cattle-transportation/src/api/salesOverview.js
Normal file
53
pc-cattle-transportation/src/api/salesOverview.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import request from '@/utils/axios.ts';
|
||||
|
||||
// 销售概览 - 列表查询
|
||||
export function salesOverviewList(data) {
|
||||
return request({
|
||||
url: '/salesoverview/list',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 销售概览 - 新增
|
||||
export function salesOverviewAdd(data) {
|
||||
return request({
|
||||
url: '/salesoverview/add',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 销售概览 - 编辑
|
||||
export function salesOverviewEdit(data) {
|
||||
return request({
|
||||
url: '/salesoverview/edit',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 销售概览 - 删除
|
||||
export function salesOverviewDelete(id) {
|
||||
return request({
|
||||
url: `/salesoverview/delete?id=${id}`,
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
// 销售概览 - 详情查询
|
||||
export function salesOverviewDetail(id) {
|
||||
return request({
|
||||
url: `/salesoverview/detail?id=${id}`,
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
// 销售概览 - 计算统计数据
|
||||
export function calculateSalesOverview() {
|
||||
return request({
|
||||
url: '/salesoverview/calculate',
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
54
pc-cattle-transportation/src/api/warehouse.js
Normal file
54
pc-cattle-transportation/src/api/warehouse.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import request from '@/utils/axios.ts';
|
||||
|
||||
// --------- 中转仓管理 -----------
|
||||
// 中转仓 - 列表
|
||||
export function warehouseList(data) {
|
||||
return request({
|
||||
url: '/warehouse/list',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 中转仓 - 新增
|
||||
export function warehouseAdd(data) {
|
||||
return request({
|
||||
url: '/warehouse/add',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 中转仓 - 编辑
|
||||
export function warehouseEdit(data) {
|
||||
return request({
|
||||
url: '/warehouse/edit',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 中转仓 - 删除
|
||||
export function warehouseDel(id) {
|
||||
return request({
|
||||
url: `/warehouse/delete?id=${id}`,
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
// 中转仓 - 详情
|
||||
export function warehouseDetail(id) {
|
||||
return request({
|
||||
url: `/warehouse/detail?id=${id}`,
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
// 中转仓 - 获取所有启用的中转仓(下拉选择用)
|
||||
export function warehouseAll() {
|
||||
return request({
|
||||
url: '/warehouse/all',
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
46
pc-cattle-transportation/src/api/warehouseIn.js
Normal file
46
pc-cattle-transportation/src/api/warehouseIn.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import request from '@/utils/axios.ts';
|
||||
|
||||
// --------- 进仓管理 -----------
|
||||
// 进仓 - 列表
|
||||
export function warehouseInList(data) {
|
||||
return request({
|
||||
url: '/warehouseIn/list',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 进仓 - 新增
|
||||
export function warehouseInAdd(data) {
|
||||
return request({
|
||||
url: '/warehouseIn/add',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 进仓 - 编辑
|
||||
export function warehouseInEdit(data) {
|
||||
return request({
|
||||
url: '/warehouseIn/edit',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 进仓 - 删除
|
||||
export function warehouseInDel(id) {
|
||||
return request({
|
||||
url: `/warehouseIn/delete?id=${id}`,
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
// 进仓 - 详情
|
||||
export function warehouseInDetail(id) {
|
||||
return request({
|
||||
url: `/warehouseIn/detail?id=${id}`,
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
46
pc-cattle-transportation/src/api/warehouseOut.js
Normal file
46
pc-cattle-transportation/src/api/warehouseOut.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import request from '@/utils/axios.ts';
|
||||
|
||||
// --------- 出仓管理 -----------
|
||||
// 出仓 - 列表
|
||||
export function warehouseOutList(data) {
|
||||
return request({
|
||||
url: '/warehouseOut/list',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 出仓 - 新增
|
||||
export function warehouseOutAdd(data) {
|
||||
return request({
|
||||
url: '/warehouseOut/add',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 出仓 - 编辑
|
||||
export function warehouseOutEdit(data) {
|
||||
return request({
|
||||
url: '/warehouseOut/edit',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 出仓 - 删除
|
||||
export function warehouseOutDel(id) {
|
||||
return request({
|
||||
url: `/warehouseOut/delete?id=${id}`,
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
// 出仓 - 详情
|
||||
export function warehouseOutDetail(id) {
|
||||
return request({
|
||||
url: `/warehouseOut/detail?id=${id}`,
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,6 +7,14 @@
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<!-- 新的数据大屏入口 -->
|
||||
<div style="margin-right: 20px">
|
||||
<el-button type="primary" plain @click="router.push('/datav')">
|
||||
<el-icon style="margin-right: 5px"><DataAnalysis /></el-icon>
|
||||
数据监控大屏
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div style="margin-right: 20px" v-if="userType == 1">
|
||||
<el-button type="primary">
|
||||
<a style="color: #fff" href="https://b.datav.run/share/page/7d743ac7c27c0d332d0a19e758e0d7c4" target="_blank"> 可视化大屏 </a>
|
||||
@@ -23,7 +31,7 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { ArrowRight } from '@element-plus/icons-vue';
|
||||
import { ArrowRight, DataAnalysis } from '@element-plus/icons-vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { useUserStore } from '~/store/user';
|
||||
|
||||
@@ -98,7 +98,7 @@ app.use(JsonViewer);
|
||||
|
||||
app.use(BaiduMap, {
|
||||
// ak 是在百度地图开发者平台申请的密钥 详见 http://lbsyun.baidu.com/apiconsole/key */
|
||||
ak: 'xITbC7jegaAAuu4m9jC2Zx6eFbQJ29Rj', //
|
||||
ak: '3AN3VahoqaXUs32U8luXD2Dwn86KK5B7', //
|
||||
// v: '2.0', // 默认使用3.0
|
||||
// type: 'WebGL' // ||API 默认API (使用此模式 BMap=BMapGL)
|
||||
// type: 'WebGL', // ||API 默认API (使用此模式 BMap=BMapGL)
|
||||
|
||||
@@ -263,6 +263,80 @@ export const constantRoutes: Array<RouteRecordRaw> = [
|
||||
],
|
||||
|
||||
},
|
||||
// 销售概览路由
|
||||
{
|
||||
path: '/salesOverview',
|
||||
component: LayoutIndex,
|
||||
meta: {
|
||||
title: '销售概览',
|
||||
keepAlive: true,
|
||||
requireAuth: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'list', // ✅ 修复:使用相对路径
|
||||
name: 'SalesOverviewList',
|
||||
meta: {
|
||||
title: '销售概览',
|
||||
keepAlive: true,
|
||||
requireAuth: true,
|
||||
},
|
||||
component: () => import('~/views/salesOverview/list.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
// 中转仓管理路由
|
||||
{
|
||||
path: '/warehouse',
|
||||
component: LayoutIndex,
|
||||
meta: {
|
||||
title: '中转仓管理',
|
||||
keepAlive: true,
|
||||
requireAuth: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'warehouseList', // ✅ 修复:使用相对路径
|
||||
name: 'WarehouseList',
|
||||
meta: {
|
||||
title: '中转仓管理',
|
||||
keepAlive: true,
|
||||
requireAuth: true,
|
||||
},
|
||||
component: () => import('~/views/warehouse/warehouse.vue'),
|
||||
},
|
||||
{
|
||||
path: 'warehouseIn', // ✅ 修复:使用相对路径
|
||||
name: 'WarehouseIn',
|
||||
meta: {
|
||||
title: '进仓管理',
|
||||
keepAlive: true,
|
||||
requireAuth: true,
|
||||
},
|
||||
component: () => import('~/views/warehouse/warehouseIn.vue'),
|
||||
},
|
||||
{
|
||||
path: 'warehouseOut', // ✅ 修复:使用相对路径
|
||||
name: 'WarehouseOut',
|
||||
meta: {
|
||||
title: '出仓管理',
|
||||
keepAlive: true,
|
||||
requireAuth: true,
|
||||
},
|
||||
component: () => import('~/views/warehouse/warehouseOut.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/datav',
|
||||
name: 'DataV',
|
||||
meta: {
|
||||
title: '数据大屏',
|
||||
keepAlive: true,
|
||||
requireAuth: false,
|
||||
},
|
||||
component: () => import('~/views/datav/ChinaMapParticle.vue'),
|
||||
},
|
||||
];
|
||||
|
||||
export const dynamicRoutes = [
|
||||
|
||||
@@ -287,6 +287,7 @@ export const loadView = (view) => {
|
||||
// eslint-disable-next-line guard-for-in,no-restricted-syntax
|
||||
for (const path in modules) {
|
||||
const dir = path.split('views/')[1].split('.vue')[0];
|
||||
// 先尝试精确匹配
|
||||
if (dir === normalizedView) {
|
||||
// 使用函数包装导入过程,添加错误处理
|
||||
res = () =>
|
||||
@@ -299,6 +300,24 @@ export const loadView = (view) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 如果精确匹配失败,尝试大小写不敏感匹配
|
||||
if (!res) {
|
||||
// eslint-disable-next-line guard-for-in,no-restricted-syntax
|
||||
for (const path in modules) {
|
||||
const dir = path.split('views/')[1].split('.vue')[0];
|
||||
// 大小写不敏感匹配
|
||||
if (dir.toLowerCase() === normalizedView.toLowerCase()) {
|
||||
res = () =>
|
||||
modules[path]().catch((error) => {
|
||||
console.error('Failed to load module:', path, error);
|
||||
return import('~/views/entry/details.vue');
|
||||
});
|
||||
console.warn(`loadView: Case-insensitive match found for "${normalizedView}" -> "${dir}"`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到匹配的视图,返回默认视图
|
||||
if (!res) {
|
||||
console.error('loadView: View not found:', normalizedView);
|
||||
|
||||
803
pc-cattle-transportation/src/views/datav/ChinaMapParticle.vue
Normal file
803
pc-cattle-transportation/src/views/datav/ChinaMapParticle.vue
Normal file
@@ -0,0 +1,803 @@
|
||||
<template>
|
||||
<div class="datav-container">
|
||||
<!-- Three.js 画布容器 -->
|
||||
<div ref="canvasContainer" class="canvas-container"></div>
|
||||
|
||||
<!-- 顶部标题 -->
|
||||
<div class="header">
|
||||
<div class="title">牛只运输监控大屏</div>
|
||||
<div class="subtitle">CATTLE TRANSPORT MONITORING SYSTEM</div>
|
||||
</div>
|
||||
|
||||
<!-- 全屏控制按钮 -->
|
||||
<div class="controls">
|
||||
<button @click="toggleFullScreen">全屏沉浸体验</button>
|
||||
</div>
|
||||
|
||||
<!-- 加载提示 -->
|
||||
<div v-if="loading" class="loading-overlay">
|
||||
<div class="loading-text">正在构建地形地图数据...</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
|
||||
import * as d3 from 'd3-geo';
|
||||
import { getDatavData } from '@/api/datav.js';
|
||||
|
||||
// 状态
|
||||
const canvasContainer = ref<HTMLElement | null>(null);
|
||||
const loading = ref(true);
|
||||
let scene: THREE.Scene;
|
||||
let camera: THREE.PerspectiveCamera;
|
||||
let renderer: THREE.WebGLRenderer;
|
||||
let controls: OrbitControls;
|
||||
let animationFrameId: number;
|
||||
let clock: THREE.Clock;
|
||||
|
||||
// 地图相关变量
|
||||
let terrainMesh: THREE.Mesh | null = null;
|
||||
let terrainGroup: THREE.Group;
|
||||
let mapBorderGroup: THREE.Group;
|
||||
let flyLineGroup: THREE.Group;
|
||||
let bgParticlesGroup: THREE.Group;
|
||||
let uniforms: {
|
||||
uTime: { value: number };
|
||||
uHeightScale: { value: number };
|
||||
uNoiseScale: { value: number };
|
||||
uLightDir: { value: THREE.Vector3 };
|
||||
uBaseColor: { value: THREE.Color };
|
||||
uMountainColor: { value: THREE.Color };
|
||||
uVegetationColor: { value: THREE.Color };
|
||||
uVegetationDensity: { value: number };
|
||||
uBorderColor: { value: THREE.Color };
|
||||
uCameraPos: { value: THREE.Vector3 };
|
||||
};
|
||||
|
||||
// 飞线数据 (起始地 -> 目的地)
|
||||
let transportRoutes: Array<{ from: [number, number]; to: [number, number] }> = [];
|
||||
|
||||
// 地形顶点着色器
|
||||
const terrainVertexShader = `
|
||||
uniform float uTime;
|
||||
uniform float uHeightScale;
|
||||
uniform float uNoiseScale;
|
||||
|
||||
varying vec2 vUv;
|
||||
varying vec3 vNormal;
|
||||
varying vec3 vPosition;
|
||||
varying vec3 vWorldPosition;
|
||||
varying float vElevation;
|
||||
|
||||
// 噪声函数
|
||||
float random(vec2 st) {
|
||||
return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);
|
||||
}
|
||||
|
||||
float noise(vec2 st) {
|
||||
vec2 i = floor(st);
|
||||
vec2 f = fract(st);
|
||||
float a = random(i);
|
||||
float b = random(i + vec2(1.0, 0.0));
|
||||
float c = random(i + vec2(0.0, 1.0));
|
||||
float d = random(i + vec2(1.0, 1.0));
|
||||
vec2 u = f * f * (3.0 - 2.0 * f);
|
||||
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
|
||||
}
|
||||
|
||||
// 分形布朗运动 - 生成地形
|
||||
float fbm(vec2 st) {
|
||||
float value = 0.0;
|
||||
float amplitude = 0.5;
|
||||
float frequency = 1.0;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
value += amplitude * noise(st * frequency);
|
||||
frequency *= 2.0;
|
||||
amplitude *= 0.5;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
vPosition = position;
|
||||
|
||||
// 计算地形高度
|
||||
vec2 noiseCoord = uv * uNoiseScale;
|
||||
float elevation = fbm(noiseCoord);
|
||||
vElevation = elevation;
|
||||
|
||||
// 应用高度偏移
|
||||
vec3 pos = position;
|
||||
pos.z += elevation * uHeightScale;
|
||||
|
||||
// 计算法线(用于光照)
|
||||
float offset = 0.01;
|
||||
float hL = fbm((uv + vec2(offset, 0.0)) * uNoiseScale);
|
||||
float hR = fbm((uv + vec2(-offset, 0.0)) * uNoiseScale);
|
||||
float hD = fbm((uv + vec2(0.0, offset)) * uNoiseScale);
|
||||
float hU = fbm((uv + vec2(0.0, -offset)) * uNoiseScale);
|
||||
|
||||
vec3 normal = normalize(vec3(
|
||||
(hL - hR) / (2.0 * offset),
|
||||
(hD - hU) / (2.0 * offset),
|
||||
1.0
|
||||
));
|
||||
vNormal = normal;
|
||||
|
||||
vec4 worldPos = modelMatrix * vec4(pos, 1.0);
|
||||
vWorldPosition = worldPos.xyz;
|
||||
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
// 地形片元着色器(包含植被效果)
|
||||
const terrainFragmentShader = `
|
||||
uniform float uTime;
|
||||
uniform vec3 uLightDir;
|
||||
uniform vec3 uBaseColor;
|
||||
uniform vec3 uMountainColor;
|
||||
uniform vec3 uVegetationColor;
|
||||
uniform float uVegetationDensity;
|
||||
|
||||
varying vec2 vUv;
|
||||
varying vec3 vNormal;
|
||||
varying vec3 vPosition;
|
||||
varying vec3 vWorldPosition;
|
||||
varying float vElevation;
|
||||
|
||||
// 噪声函数(与顶点着色器相同)
|
||||
float random(vec2 st) {
|
||||
return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);
|
||||
}
|
||||
|
||||
float noise(vec2 st) {
|
||||
vec2 i = floor(st);
|
||||
vec2 f = fract(st);
|
||||
float a = random(i);
|
||||
float b = random(i + vec2(1.0, 0.0));
|
||||
float c = random(i + vec2(0.0, 1.0));
|
||||
float d = random(i + vec2(1.0, 1.0));
|
||||
vec2 u = f * f * (3.0 - 2.0 * f);
|
||||
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
|
||||
}
|
||||
|
||||
float fbm(vec2 st) {
|
||||
float value = 0.0;
|
||||
float amplitude = 0.5;
|
||||
float frequency = 1.0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
value += amplitude * noise(st * frequency);
|
||||
frequency *= 2.0;
|
||||
amplitude *= 0.5;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void main() {
|
||||
// 基础地形颜色(根据高度)
|
||||
vec3 baseTerrain = mix(uBaseColor, uMountainColor, vElevation);
|
||||
|
||||
// 植被层(在低海拔区域)
|
||||
float vegetationMask = 1.0 - smoothstep(0.3, 0.7, vElevation);
|
||||
float vegetationNoise = fbm(vUv * 20.0 + uTime * 0.1);
|
||||
vegetationMask *= step(0.3, vegetationNoise) * uVegetationDensity;
|
||||
|
||||
vec3 vegetation = uVegetationColor * vegetationMask;
|
||||
|
||||
// 光照计算
|
||||
vec3 normal = normalize(vNormal);
|
||||
vec3 lightDir = normalize(uLightDir);
|
||||
float diff = max(dot(normal, lightDir), 0.1);
|
||||
|
||||
// 最终颜色
|
||||
vec3 finalColor = (baseTerrain + vegetation) * diff;
|
||||
|
||||
// 添加一些细节纹理
|
||||
float detail = fbm(vUv * 50.0);
|
||||
finalColor += (detail - 0.5) * 0.1;
|
||||
|
||||
// 添加一些环境光
|
||||
finalColor += vec3(0.05, 0.05, 0.08);
|
||||
|
||||
gl_FragColor = vec4(finalColor, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
// 边框发光着色器
|
||||
const borderVertexShader = `
|
||||
varying vec3 vWorldPosition;
|
||||
varying vec3 vNormal;
|
||||
|
||||
void main() {
|
||||
vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
|
||||
vNormal = normalize(normalMatrix * normal);
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const borderFragmentShader = `
|
||||
uniform vec3 uBorderColor;
|
||||
uniform float uTime;
|
||||
uniform vec3 uCameraPos;
|
||||
|
||||
varying vec3 vWorldPosition;
|
||||
varying vec3 vNormal;
|
||||
|
||||
void main() {
|
||||
// 计算到相机的距离
|
||||
float dist = distance(vWorldPosition, uCameraPos);
|
||||
|
||||
// 菲涅尔效应 - 边缘发光
|
||||
vec3 viewDir = normalize(uCameraPos - vWorldPosition);
|
||||
vec3 normal = normalize(vNormal);
|
||||
float fresnel = pow(1.0 - max(dot(viewDir, normal), 0.0), 2.0);
|
||||
|
||||
// 发光强度
|
||||
float glow = fresnel * 2.0;
|
||||
|
||||
// 添加脉冲效果
|
||||
float pulse = sin(uTime * 2.0) * 0.5 + 0.5;
|
||||
glow += pulse * 0.3;
|
||||
|
||||
vec3 finalColor = uBorderColor * glow;
|
||||
float alpha = min(glow, 1.0);
|
||||
|
||||
gl_FragColor = vec4(finalColor, alpha);
|
||||
}
|
||||
`;
|
||||
|
||||
// 飞线顶点着色器
|
||||
const flylineVertexShader = `
|
||||
attribute float percent;
|
||||
varying float vPercent;
|
||||
uniform float uTime;
|
||||
|
||||
void main() {
|
||||
vPercent = percent;
|
||||
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
||||
gl_Position = projectionMatrix * mvPosition;
|
||||
}
|
||||
`;
|
||||
|
||||
// 飞线片元着色器
|
||||
const flylineFragmentShader = `
|
||||
varying float vPercent;
|
||||
uniform float uTime;
|
||||
uniform vec3 uColor;
|
||||
|
||||
void main() {
|
||||
// 飞线动画
|
||||
float length = 0.3; // 拖尾长度
|
||||
float speed = 0.8;
|
||||
float loop = fract(uTime * speed); // 0~1 循环
|
||||
|
||||
// 计算当前像素在飞线上的位置是否应该发光
|
||||
float dist = distance(vPercent, loop);
|
||||
|
||||
// 处理循环边界 (例如 0.9 和 0.1 应该也很远,但在循环中很近,这里简化处理)
|
||||
// 实际上对于飞线,我们希望它是一段一段飞过去
|
||||
|
||||
float alpha = 0.0;
|
||||
if (vPercent < loop && vPercent > loop - length) {
|
||||
alpha = (vPercent - (loop - length)) / length; // 渐变拖尾
|
||||
alpha = pow(alpha, 2.0); // 增强头部亮度
|
||||
}
|
||||
|
||||
// 基础底色透明度
|
||||
float baseAlpha = 0.1;
|
||||
|
||||
gl_FragColor = vec4(uColor, baseAlpha + alpha);
|
||||
}
|
||||
`;
|
||||
|
||||
// 初始化 Three.js 场景
|
||||
const initThreeScene = () => {
|
||||
if (!canvasContainer.value) return;
|
||||
|
||||
// 1. 场景
|
||||
scene = new THREE.Scene();
|
||||
scene.fog = new THREE.FogExp2(0x000000, 0.0008);
|
||||
|
||||
// 2. 相机
|
||||
const width = canvasContainer.value.clientWidth;
|
||||
const height = canvasContainer.value.clientHeight;
|
||||
camera = new THREE.PerspectiveCamera(60, width / height, 1, 10000);
|
||||
camera.position.set(0, -100, 200);
|
||||
camera.lookAt(0, 0, 0);
|
||||
|
||||
// 3. 渲染器
|
||||
renderer = new THREE.WebGLRenderer({
|
||||
antialias: true,
|
||||
alpha: true,
|
||||
powerPreference: "high-performance"
|
||||
});
|
||||
renderer.setSize(width, height);
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
renderer.setClearColor(0x000000, 0);
|
||||
canvasContainer.value.appendChild(renderer.domElement);
|
||||
|
||||
// 4. 控制器
|
||||
controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.05;
|
||||
controls.minDistance = 10;
|
||||
controls.maxDistance = 800;
|
||||
controls.maxPolarAngle = Math.PI / 2 + 0.2;
|
||||
|
||||
clock = new THREE.Clock();
|
||||
|
||||
// 创建层级组
|
||||
bgParticlesGroup = new THREE.Group(); // 背景旋转粒子
|
||||
terrainGroup = new THREE.Group(); // 地形层
|
||||
mapBorderGroup = new THREE.Group(); // 地图边框
|
||||
flyLineGroup = new THREE.Group(); // 飞线层
|
||||
|
||||
scene.add(bgParticlesGroup);
|
||||
scene.add(terrainGroup);
|
||||
scene.add(mapBorderGroup);
|
||||
scene.add(flyLineGroup);
|
||||
|
||||
// 初始化 Uniforms
|
||||
uniforms = {
|
||||
uTime: { value: 0 },
|
||||
uHeightScale: { value: 15.0 },
|
||||
uNoiseScale: { value: 2.0 },
|
||||
uLightDir: { value: new THREE.Vector3(0.5, 0.8, 0.3).normalize() },
|
||||
uBaseColor: { value: new THREE.Color('#1a1a2e') },
|
||||
uMountainColor: { value: new THREE.Color('#16213e') },
|
||||
uVegetationColor: { value: new THREE.Color('#0f3460') },
|
||||
uVegetationDensity: { value: 0.3 },
|
||||
uBorderColor: { value: new THREE.Color('#ffffff') },
|
||||
uCameraPos: { value: new THREE.Vector3() }
|
||||
};
|
||||
|
||||
// 窗口大小调整
|
||||
window.addEventListener('resize', onWindowResize);
|
||||
};
|
||||
|
||||
// 生成背景旋转粒子 (开普勒轨道风格)
|
||||
const createBackgroundParticles = () => {
|
||||
const count = 3000;
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
const positions = new Float32Array(count * 3);
|
||||
const sizes = new Float32Array(count);
|
||||
const colors = new Float32Array(count * 3);
|
||||
|
||||
const color1 = new THREE.Color('#1e3799');
|
||||
const color2 = new THREE.Color('#0c2461');
|
||||
|
||||
for(let i = 0; i < count; i++) {
|
||||
// 环形分布
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
// 半径范围 200 - 500
|
||||
const radius = 200 + Math.random() * 300;
|
||||
// 稍微有些高度差,形成圆盘状
|
||||
const height = (Math.random() - 0.5) * 50;
|
||||
|
||||
positions[i*3] = Math.cos(angle) * radius;
|
||||
positions[i*3+1] = Math.sin(angle) * radius; // 竖起来的圆盘
|
||||
positions[i*3+2] = height;
|
||||
|
||||
sizes[i] = Math.random() * 2;
|
||||
|
||||
const c = Math.random() > 0.5 ? color1 : color2;
|
||||
colors[i*3] = c.r;
|
||||
colors[i*3+1] = c.g;
|
||||
colors[i*3+2] = c.b;
|
||||
}
|
||||
|
||||
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||
geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
|
||||
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
||||
|
||||
const material = new THREE.PointsMaterial({
|
||||
size: 2,
|
||||
vertexColors: true,
|
||||
map: createCircleTexture(),
|
||||
transparent: true,
|
||||
opacity: 0.6,
|
||||
depthWrite: false,
|
||||
blending: THREE.AdditiveBlending
|
||||
});
|
||||
|
||||
const points = new THREE.Points(geometry, material);
|
||||
bgParticlesGroup.add(points);
|
||||
};
|
||||
|
||||
// 创建圆形纹理
|
||||
const createCircleTexture = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 32;
|
||||
canvas.height = 32;
|
||||
const context = canvas.getContext('2d');
|
||||
if (context) {
|
||||
const gradient = context.createRadialGradient(16, 16, 0, 16, 16, 16);
|
||||
gradient.addColorStop(0, 'rgba(255,255,255,1)');
|
||||
gradient.addColorStop(1, 'rgba(255,255,255,0)');
|
||||
context.fillStyle = gradient;
|
||||
context.fillRect(0, 0, 32, 32);
|
||||
}
|
||||
const texture = new THREE.Texture(canvas);
|
||||
texture.needsUpdate = true;
|
||||
return texture;
|
||||
};
|
||||
|
||||
// 创建地形网格(使用 LOD 优化)
|
||||
const createTerrain = () => {
|
||||
const width = 400;
|
||||
const height = 400;
|
||||
const segments = 128; // 细分程度,影响地形细节
|
||||
|
||||
const geometry = new THREE.PlaneGeometry(width, height, segments, segments);
|
||||
|
||||
const material = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
uTime: uniforms.uTime,
|
||||
uHeightScale: uniforms.uHeightScale,
|
||||
uNoiseScale: uniforms.uNoiseScale,
|
||||
uLightDir: uniforms.uLightDir,
|
||||
uBaseColor: uniforms.uBaseColor,
|
||||
uMountainColor: uniforms.uMountainColor,
|
||||
uVegetationColor: uniforms.uVegetationColor,
|
||||
uVegetationDensity: uniforms.uVegetationDensity
|
||||
},
|
||||
vertexShader: terrainVertexShader,
|
||||
fragmentShader: terrainFragmentShader,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
|
||||
const terrain = new THREE.Mesh(geometry, material);
|
||||
terrain.rotation.x = -Math.PI / 2; // 平铺在地面
|
||||
terrain.position.y = -50; // 放在地图下方
|
||||
terrain.receiveShadow = false;
|
||||
terrain.castShadow = false;
|
||||
|
||||
terrainGroup.add(terrain);
|
||||
terrainMesh = terrain;
|
||||
|
||||
return terrain;
|
||||
};
|
||||
|
||||
// 创建地图边框(发光效果)
|
||||
const createMapBorder = async () => {
|
||||
try {
|
||||
// 投影转换
|
||||
const projection = d3.geoMercator().center([104, 35]).scale(400).translate([0, 0]);
|
||||
|
||||
// 获取 GeoJSON
|
||||
const response = await fetch('https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json');
|
||||
const chinaJson = await response.json();
|
||||
|
||||
// 合并所有边界点
|
||||
const borderPoints: THREE.Vector3[] = [];
|
||||
|
||||
chinaJson.features.forEach((feature: any) => {
|
||||
if (feature.properties.name === "") return;
|
||||
|
||||
const coordinates = feature.geometry.coordinates;
|
||||
const type = feature.geometry.type;
|
||||
const polygons = type === 'MultiPolygon' ? coordinates : [coordinates];
|
||||
|
||||
polygons.forEach((polygon: any) => {
|
||||
const ring = type === 'MultiPolygon' ? polygon[0] : polygon[0];
|
||||
|
||||
for (let i = 0; i < ring.length; i++) {
|
||||
const [lng, lat] = ring[i];
|
||||
const [x, y] = projection([lng, lat]) || [0, 0];
|
||||
borderPoints.push(new THREE.Vector3(x, -y, 5)); // 稍微抬高一点
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 创建边框几何体(使用 Line 实现发光效果)
|
||||
if (borderPoints.length > 0) {
|
||||
// 创建边框线(使用 LineSegments 实现更粗的线条)
|
||||
const lineGeometry = new THREE.BufferGeometry().setFromPoints(borderPoints);
|
||||
|
||||
// 使用自定义着色器材质实现发光效果
|
||||
const borderMaterial = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
uBorderColor: uniforms.uBorderColor,
|
||||
uTime: uniforms.uTime,
|
||||
uCameraPos: uniforms.uCameraPos
|
||||
},
|
||||
vertexShader: borderVertexShader,
|
||||
fragmentShader: borderFragmentShader,
|
||||
transparent: true,
|
||||
blending: THREE.AdditiveBlending,
|
||||
linewidth: 2
|
||||
});
|
||||
|
||||
const border = new THREE.Line(lineGeometry, borderMaterial);
|
||||
mapBorderGroup.add(border);
|
||||
|
||||
// 添加额外的细线边框作为基础
|
||||
const baseLineMaterial = new THREE.LineBasicMaterial({
|
||||
color: 0xffffff,
|
||||
transparent: true,
|
||||
opacity: 0.4,
|
||||
linewidth: 1
|
||||
});
|
||||
const baseLine = new THREE.Line(lineGeometry, baseLineMaterial);
|
||||
mapBorderGroup.add(baseLine);
|
||||
}
|
||||
|
||||
return projection;
|
||||
} catch (error) {
|
||||
console.error('Failed to load map border data:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 生成飞线 (3D 贝塞尔曲线)
|
||||
const createFlyLines = (projection: any) => {
|
||||
if (!projection) return;
|
||||
transportRoutes.forEach(route => {
|
||||
const start = projection(route.from);
|
||||
const end = projection(route.to);
|
||||
if (!start || !end) return;
|
||||
|
||||
const v0 = new THREE.Vector3(start[0], -start[1], 0);
|
||||
const v3 = new THREE.Vector3(end[0], -end[1], 0);
|
||||
|
||||
// 计算控制点,使曲线拱起
|
||||
const dist = v0.distanceTo(v3);
|
||||
const height = dist * 0.5; // 拱起高度
|
||||
|
||||
const v1 = v0.clone().lerp(v3, 0.33).add(new THREE.Vector3(0, 0, height));
|
||||
const v2 = v0.clone().lerp(v3, 0.67).add(new THREE.Vector3(0, 0, height));
|
||||
|
||||
const curve = new THREE.CubicBezierCurve3(v0, v1, v2, v3);
|
||||
const points = curve.getPoints(50); // 50个分段
|
||||
|
||||
// 创建飞线几何体
|
||||
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
||||
const percents = new Float32Array(points.length);
|
||||
for(let i=0; i<points.length; i++) {
|
||||
percents[i] = i / (points.length - 1);
|
||||
}
|
||||
geometry.setAttribute('percent', new THREE.BufferAttribute(percents, 1));
|
||||
|
||||
const material = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
uTime: uniforms.uTime,
|
||||
uColor: { value: new THREE.Color('#ff9f43') } // 橙色飞线
|
||||
},
|
||||
vertexShader: flylineVertexShader,
|
||||
fragmentShader: flylineFragmentShader,
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
blending: THREE.AdditiveBlending
|
||||
});
|
||||
|
||||
const line = new THREE.Line(geometry, material);
|
||||
flyLineGroup.add(line);
|
||||
|
||||
// 添加终点箭头 (圆锥体)
|
||||
const coneGeo = new THREE.ConeGeometry(1, 3, 8);
|
||||
const coneMat = new THREE.MeshBasicMaterial({ color: 0xff9f43 });
|
||||
const cone = new THREE.Mesh(coneGeo, coneMat);
|
||||
cone.position.copy(v3);
|
||||
// 箭头朝向曲线切线方向
|
||||
const tangent = curve.getTangent(1);
|
||||
cone.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), tangent);
|
||||
flyLineGroup.add(cone);
|
||||
|
||||
// 起点光圈
|
||||
const ringGeo = new THREE.RingGeometry(1, 1.5, 16);
|
||||
const ringMat = new THREE.MeshBasicMaterial({ color: 0x00ffff, side: THREE.DoubleSide });
|
||||
const ring = new THREE.Mesh(ringGeo, ringMat);
|
||||
ring.position.copy(v0);
|
||||
flyLineGroup.add(ring);
|
||||
});
|
||||
};
|
||||
|
||||
// 窗口大小调整处理
|
||||
const onWindowResize = () => {
|
||||
if (!canvasContainer.value || !camera || !renderer) return;
|
||||
const width = canvasContainer.value.clientWidth;
|
||||
const height = canvasContainer.value.clientHeight;
|
||||
camera.aspect = width / height;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(width, height);
|
||||
};
|
||||
|
||||
// 全屏切换
|
||||
const toggleFullScreen = () => {
|
||||
if (!document.fullscreenElement) {
|
||||
document.documentElement.requestFullscreen();
|
||||
} else {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 动画循环
|
||||
const animate = () => {
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
const elapsedTime = clock.getElapsedTime();
|
||||
|
||||
// 更新 Uniforms
|
||||
if (uniforms) {
|
||||
uniforms.uTime.value = elapsedTime;
|
||||
uniforms.uCameraPos.value.copy(camera.position);
|
||||
|
||||
// LOD 优化:根据相机距离调整地形细节
|
||||
const cameraDistance = camera.position.length();
|
||||
if (terrainMesh) {
|
||||
const geometry = terrainMesh.geometry as THREE.PlaneGeometry;
|
||||
// 可以根据距离动态调整 segments(这里简化处理)
|
||||
// 实际可以使用 THREE.LOD 对象
|
||||
}
|
||||
}
|
||||
|
||||
// 旋转背景
|
||||
bgParticlesGroup.rotation.z = elapsedTime * 0.02; // 绕Z轴旋转 (垂直于地图)
|
||||
|
||||
// 更新控制器
|
||||
controls.update();
|
||||
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
initThreeScene();
|
||||
createBackgroundParticles();
|
||||
|
||||
// 创建地形
|
||||
createTerrain();
|
||||
|
||||
// 获取数据
|
||||
const res = await getDatavData();
|
||||
if(res.data && res.data.routes) {
|
||||
transportRoutes = res.data.routes;
|
||||
}
|
||||
|
||||
// 创建地图边框并获取投影
|
||||
const projection = await createMapBorder();
|
||||
|
||||
// 生成飞线
|
||||
if (projection) {
|
||||
createFlyLines(projection);
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
animate();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
}
|
||||
window.removeEventListener('resize', onWindowResize);
|
||||
|
||||
// 清理地形
|
||||
if (terrainMesh) {
|
||||
terrainMesh.geometry.dispose();
|
||||
if (terrainMesh.material instanceof THREE.Material) {
|
||||
terrainMesh.material.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// 清理边框
|
||||
mapBorderGroup.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh || child instanceof THREE.Line) {
|
||||
child.geometry.dispose();
|
||||
if (child.material instanceof THREE.Material) {
|
||||
child.material.dispose();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 清理飞线
|
||||
flyLineGroup.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh || child instanceof THREE.Line) {
|
||||
child.geometry.dispose();
|
||||
if (child.material instanceof THREE.Material) {
|
||||
child.material.dispose();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (renderer) {
|
||||
renderer.dispose();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.datav-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
background: radial-gradient(circle at center, #050505 0%, #0b0b10 100%);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.canvas-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 40px;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
color: #00ffff;
|
||||
letter-spacing: 5px;
|
||||
text-shadow: 0 0 10px rgba(0, 255, 255, 0.5);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1rem;
|
||||
color: #c5a059;
|
||||
letter-spacing: 3px;
|
||||
font-family: 'Arial', sans-serif;
|
||||
}
|
||||
|
||||
.controls {
|
||||
position: absolute;
|
||||
bottom: 40px;
|
||||
right: 40px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
button {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(197, 160, 89, 0.3);
|
||||
color: #c5a059;
|
||||
padding: 12px 30px;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.4s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
backdrop-filter: blur(5px);
|
||||
|
||||
&:hover {
|
||||
background: rgba(197, 160, 89, 0.1);
|
||||
border-color: #c5a059;
|
||||
box-shadow: 0 0 15px rgba(197, 160, 89, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #c5a059;
|
||||
font-size: 1.2rem;
|
||||
letter-spacing: 3px;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 0.5; }
|
||||
50% { opacity: 1; }
|
||||
100% { opacity: 0.5; }
|
||||
}
|
||||
</style>
|
||||
@@ -749,7 +749,7 @@ const loadDeviceLogs = async (deviceId, deviceType, deliveryId) => {
|
||||
const initMap = async () => {
|
||||
try {
|
||||
// 使用百度地图 API Key
|
||||
const BMapGL = await BMPGL('xITbC7jegaAAuu4m9jC2Zx6eFbQJ29Rj');
|
||||
const BMapGL = await BMPGL('3AN3VahoqaXUs32U8luXD2Dwn86KK5B7');
|
||||
const lat = parseFloat(warningData.latitude);
|
||||
const lon = parseFloat(warningData.longitude);
|
||||
|
||||
@@ -1054,7 +1054,7 @@ const initTrackMap = async () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const BMapGL = await BMPGL('xITbC7jegaAAuu4m9jC2Zx6eFbQJ29Rj');
|
||||
const BMapGL = await BMPGL('3AN3VahoqaXUs32U8luXD2Dwn86KK5B7');
|
||||
trackBMapGL.value = BMapGL; // 保存 BMapGL 实例
|
||||
|
||||
// 创建地图实例
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<!-- 参数缺失时的友好提示 -->
|
||||
<div v-if="!route.query.id" class="error-container">
|
||||
<div v-if="!route.query.id && !route.query.deliveryId" class="error-container">
|
||||
<el-result icon="warning" title="参数缺失" sub-title="缺少必要的参数,无法加载详情页面">
|
||||
<template #extra>
|
||||
<el-button type="primary" @click="goBack">返回上一页</el-button>
|
||||
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 正常内容 -->
|
||||
<div v-else class="details-container">
|
||||
<div v-else-if="route.query.id || route.query.deliveryId" class="details-container">
|
||||
<!-- 头部导航与操作 -->
|
||||
<div class="page-header">
|
||||
<div class="header-left">
|
||||
@@ -619,15 +619,21 @@ const collarLogForm = reactive({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
// 获取 deliveryId 的辅助函数(兼容 id 和 deliveryId 两种参数名)
|
||||
const getDeliveryId = () => {
|
||||
return route.query.id || route.query.deliveryId || data.id;
|
||||
};
|
||||
|
||||
// 查详情
|
||||
const getDetail = () => {
|
||||
|
||||
if (!route.query.id) {
|
||||
const deliveryId = getDeliveryId();
|
||||
|
||||
if (!deliveryId) {
|
||||
console.warn('=== 警告:deliveryId为空,跳过运单详情查询');
|
||||
return;
|
||||
}
|
||||
|
||||
waybillDetail(route.query.id)
|
||||
waybillDetail(deliveryId)
|
||||
.then((res) => {
|
||||
|
||||
if (res.code === 200) {
|
||||
@@ -658,7 +664,8 @@ const loadVehiclePhotos = () => {
|
||||
|
||||
// 智能主机列表查询
|
||||
const getHostList = () => {
|
||||
if (!route.query.id) {
|
||||
const deliveryId = getDeliveryId();
|
||||
if (!deliveryId) {
|
||||
console.warn('=== 警告:deliveryId为空,跳过主机列表查询');
|
||||
data.hostDataListLoading = false;
|
||||
return;
|
||||
@@ -668,7 +675,7 @@ const getHostList = () => {
|
||||
pageDeviceList({
|
||||
pageNum: 1,
|
||||
pageSize: 100, // 获取所有主机设备
|
||||
deliveryId: parseInt(route.query.id),
|
||||
deliveryId: parseInt(deliveryId),
|
||||
deviceType: 1, // 智能主机设备类型
|
||||
})
|
||||
.then((res) => {
|
||||
@@ -776,7 +783,8 @@ const getHostTrack = (item) => {
|
||||
// --------- 智能耳标 -----------
|
||||
// 耳标列表查询
|
||||
const getEarList = () => {
|
||||
if (!route.query.id) {
|
||||
const deliveryId = getDeliveryId();
|
||||
if (!deliveryId) {
|
||||
console.warn('=== 警告:deliveryId为空,跳过耳标列表查询');
|
||||
data.dataListLoading = false;
|
||||
return;
|
||||
@@ -786,7 +794,7 @@ const getEarList = () => {
|
||||
pageDeviceList({
|
||||
pageNum: form.pageNum,
|
||||
pageSize: form.pageSize,
|
||||
deliveryId: parseInt(route.query.id),
|
||||
deliveryId: parseInt(deliveryId),
|
||||
deviceType: 2, // 智能耳标设备类型
|
||||
})
|
||||
.then((res) => {
|
||||
@@ -814,9 +822,10 @@ const earLogClick = (row) => {
|
||||
data.earLogDialogVisible = true;
|
||||
|
||||
// 调用新的API获取60分钟间隔的日志数据
|
||||
const deliveryId = getDeliveryId();
|
||||
getEarTagLogs({
|
||||
deviceId: data.deviceId,
|
||||
deliveryId: parseInt(route.query.id)
|
||||
deliveryId: parseInt(deliveryId)
|
||||
}).then((res) => {
|
||||
|
||||
if (res.code === 200) {
|
||||
@@ -841,9 +850,10 @@ const earLogClick = (row) => {
|
||||
const earTrackClick = (row) => {
|
||||
|
||||
// 调用新的API获取60分钟间隔的轨迹数据
|
||||
const deliveryId = getDeliveryId();
|
||||
getEarTagTrajectory({
|
||||
deviceId: row.deviceId || row.sn || '',
|
||||
deliveryId: parseInt(route.query.id)
|
||||
deliveryId: parseInt(deliveryId)
|
||||
}).then((res) => {
|
||||
|
||||
if (res.code === 200 && res.data && res.data.length > 0) {
|
||||
@@ -853,7 +863,7 @@ const earTrackClick = (row) => {
|
||||
// 使用TrackDialog显示轨迹
|
||||
if (TrackDialogRef.value) {
|
||||
const info = {
|
||||
deliveryId: route.query.id,
|
||||
deliveryId: getDeliveryId(),
|
||||
deviceId: row.deviceId || row.sn || '',
|
||||
type: 'order',
|
||||
trajectoryPoints: trajectoryPoints // 传递轨迹点数据
|
||||
@@ -871,7 +881,8 @@ const earTrackClick = (row) => {
|
||||
|
||||
// 智能项圈列表查询
|
||||
const getCollarList = () => {
|
||||
if (!route.query.id) {
|
||||
const deliveryId = getDeliveryId();
|
||||
if (!deliveryId) {
|
||||
console.warn('=== 警告:deliveryId为空,跳过项圈列表查询');
|
||||
data.collarDataListLoading = false;
|
||||
return;
|
||||
@@ -881,7 +892,7 @@ const getCollarList = () => {
|
||||
pageDeviceList({
|
||||
pageNum: collarForm.pageNum,
|
||||
pageSize: collarForm.pageSize,
|
||||
deliveryId: parseInt(route.query.id),
|
||||
deliveryId: parseInt(deliveryId),
|
||||
deviceType: 4, // 智能项圈设备类型
|
||||
})
|
||||
.then((res) => {
|
||||
@@ -910,9 +921,10 @@ const collarLogClick = (row) => {
|
||||
data.collarDialogVisible = true;
|
||||
|
||||
// 调用新的API获取60分钟间隔的日志数据
|
||||
const deliveryId = getDeliveryId();
|
||||
getCollarLogs({
|
||||
deviceId: data.sn,
|
||||
deliveryId: parseInt(route.query.id)
|
||||
deliveryId: parseInt(deliveryId)
|
||||
}).then((res) => {
|
||||
|
||||
if (res.code === 200) {
|
||||
@@ -972,7 +984,7 @@ const getEarLogList = () => {
|
||||
earLogList({
|
||||
pageNum: earLogForm.pageNum,
|
||||
pageSize: earLogForm.pageSize,
|
||||
deliveryId: route.query.id,
|
||||
deliveryId: getDeliveryId(),
|
||||
deviceId: data.deviceId,
|
||||
})
|
||||
.then((res) => {
|
||||
@@ -995,7 +1007,7 @@ const getCollarLogList = () => {
|
||||
collarLogList({
|
||||
pageNum: collarLogForm.pageNum,
|
||||
pageSize: collarLogForm.pageSize,
|
||||
deliveryId: route.query.id,
|
||||
deliveryId: getDeliveryId(),
|
||||
deviceId: data.sn,
|
||||
})
|
||||
.then((res) => {
|
||||
@@ -1016,9 +1028,10 @@ const getCollarLogList = () => {
|
||||
const collarTrackClick = (row) => {
|
||||
|
||||
// 调用新的API获取60分钟间隔的轨迹数据
|
||||
const deliveryId = getDeliveryId();
|
||||
getCollarTrajectory({
|
||||
deviceId: row.sn || row.deviceId || '',
|
||||
deliveryId: parseInt(route.query.id)
|
||||
deliveryId: parseInt(deliveryId)
|
||||
}).then((res) => {
|
||||
|
||||
if (res.code === 200 && res.data && res.data.length > 0) {
|
||||
@@ -1028,7 +1041,7 @@ const collarTrackClick = (row) => {
|
||||
// 使用TrackDialog显示轨迹
|
||||
if (TrackDialogRef.value) {
|
||||
const info = {
|
||||
deliveryId: route.query.id,
|
||||
deliveryId: getDeliveryId(),
|
||||
deviceId: row.sn || row.deviceId || '',
|
||||
type: 'order',
|
||||
trajectoryPoints: trajectoryPoints // 传递轨迹点数据
|
||||
@@ -1053,7 +1066,7 @@ const hostLogClick = (row) => {
|
||||
// 调用新的API获取60分钟间隔的日志数据
|
||||
getHostLogs({
|
||||
deviceId: data.deviceId,
|
||||
deliveryId: parseInt(route.query.id)
|
||||
deliveryId: parseInt(getDeliveryId())
|
||||
}).then((res) => {
|
||||
|
||||
if (res.code === 200) {
|
||||
@@ -1079,7 +1092,7 @@ const hostTrackClick = (row) => {
|
||||
// 调用新的API获取60分钟间隔的轨迹数据
|
||||
getHostTrajectory({
|
||||
deviceId: row.deviceId || row.sn || '',
|
||||
deliveryId: parseInt(route.query.id)
|
||||
deliveryId: parseInt(getDeliveryId())
|
||||
}).then((res) => {
|
||||
|
||||
if (res.code === 200 && res.data && res.data.length > 0) {
|
||||
@@ -1089,7 +1102,7 @@ const hostTrackClick = (row) => {
|
||||
// 使用TrackDialog显示轨迹
|
||||
if (TrackDialogRef.value) {
|
||||
const info = {
|
||||
deliveryId: route.query.id,
|
||||
deliveryId: getDeliveryId(),
|
||||
deviceId: row.deviceId || row.sn || '',
|
||||
type: 'order',
|
||||
trajectoryPoints: trajectoryPoints // 传递轨迹点数据
|
||||
@@ -1207,20 +1220,21 @@ const getBuyerName = () => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
data.id = route.query.id;
|
||||
data.status = route.query.status;
|
||||
data.length = route.query.length;
|
||||
// 兼容 id 和 deliveryId 两种参数名
|
||||
const deliveryId = route.query.id || route.query.deliveryId;
|
||||
|
||||
|
||||
// 检查deliveryId是否存在
|
||||
if (!route.query.id) {
|
||||
if (!deliveryId) {
|
||||
console.warn('=== 警告:deliveryId为空,无法加载详情页面');
|
||||
ElMessage.error('缺少必要的参数,请从列表页面点击详情按钮进入');
|
||||
return;
|
||||
}
|
||||
|
||||
data.id = deliveryId;
|
||||
data.status = route.query.status;
|
||||
data.length = route.query.length;
|
||||
|
||||
// 检查deliveryId是否存在,存在时才测试设备关联情况
|
||||
testDeliveryDevices({ deliveryId: route.query.id })
|
||||
testDeliveryDevices({ deliveryId: deliveryId })
|
||||
.then(res => {
|
||||
|
||||
})
|
||||
|
||||
@@ -229,9 +229,167 @@ const generateRoutes = async () => {
|
||||
// 超级管理员优先跳转到系统管理页面
|
||||
targetPath = '/system/post';
|
||||
|
||||
} else if (firstMenu && firstMenu.pageUrl) {
|
||||
} else if (firstMenu) {
|
||||
// 普通用户跳转到第一个有权限的菜单页面
|
||||
targetPath = firstMenu.pageUrl;
|
||||
// 构建完整路由路径:需要根据菜单的父子关系构建
|
||||
const buildRoutePath = (menu, allMenus) => {
|
||||
if (!menu) return '/';
|
||||
|
||||
// 如果是子菜单,需要查找父菜单路径
|
||||
if (menu.parentId && menu.parentId !== 0 && menu.parentId !== '0') {
|
||||
const parent = allMenus.find(m => m.id === menu.parentId);
|
||||
if (parent) {
|
||||
// 递归查找父菜单路径
|
||||
let parentPath = '';
|
||||
|
||||
// 如果父菜单是目录(type=0),目录通常没有 routeUrl 和 pageUrl
|
||||
// 我们需要从子菜单的 pageUrl 中提取父路径
|
||||
if (parent.type === 0) {
|
||||
// 目录类型:从子菜单的 pageUrl 提取父路径(如 warehouse/warehouse -> warehouse)
|
||||
if (menu.pageUrl) {
|
||||
// 移除 pageUrl 开头的斜杠(如果有)
|
||||
const cleanPageUrl = menu.pageUrl.replace(/^\/+/, '');
|
||||
const pageUrlParts = cleanPageUrl.split('/').filter(p => p); // 过滤空字符串
|
||||
if (pageUrlParts.length >= 2) {
|
||||
parentPath = '/' + pageUrlParts[0];
|
||||
const childPath = menu.routeUrl || '';
|
||||
if (childPath) {
|
||||
return parentPath + '/' + childPath.replace(/^\/+/, '');
|
||||
}
|
||||
} else if (pageUrlParts.length === 1 && menu.routeUrl) {
|
||||
// 如果只有一个部分,可能是完整的路径(如 /shipping/shippinglist)
|
||||
// 检查是否包含父路径信息
|
||||
const fullPath = menu.pageUrl.startsWith('/') ? menu.pageUrl : '/' + menu.pageUrl;
|
||||
if (menu.routeUrl) {
|
||||
// 如果有 routeUrl,构建完整路径
|
||||
const parentFromPageUrl = fullPath.split('/').filter(p => p)[0];
|
||||
if (parentFromPageUrl) {
|
||||
return `/${parentFromPageUrl}/${menu.routeUrl}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果无法从 pageUrl 提取,尝试从父菜单的 routeUrl 获取
|
||||
if (!parentPath && parent.routeUrl) {
|
||||
parentPath = parent.routeUrl.startsWith('/')
|
||||
? parent.routeUrl
|
||||
: '/' + parent.routeUrl;
|
||||
}
|
||||
|
||||
// 如果仍然无法获取,尝试查找父菜单的父菜单
|
||||
if (!parentPath && parent.parentId && parent.parentId !== 0 && parent.parentId !== '0') {
|
||||
const grandParent = allMenus.find(m => m.id === parent.parentId);
|
||||
if (grandParent) {
|
||||
parentPath = buildRoutePath(grandParent, allMenus);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 非目录类型:使用 routeUrl 或 pageUrl
|
||||
parentPath = buildRoutePath(parent, allMenus);
|
||||
}
|
||||
|
||||
// 获取子菜单的路由路径
|
||||
const childPath = menu.routeUrl || '';
|
||||
if (childPath && parentPath) {
|
||||
// 移除父路径末尾的斜杠和子路径开头的斜杠
|
||||
const cleanParentPath = parentPath.replace(/\/+$/, '');
|
||||
const cleanChildPath = childPath.replace(/^\/+/, '');
|
||||
return cleanParentPath + '/' + cleanChildPath;
|
||||
}
|
||||
|
||||
// 如果仍然无法构建,尝试从 pageUrl 推断
|
||||
if (!parentPath && menu.pageUrl) {
|
||||
const cleanPageUrl = menu.pageUrl.replace(/^\/+/, '');
|
||||
const pageUrlParts = cleanPageUrl.split('/').filter(p => p);
|
||||
if (pageUrlParts.length >= 2 && childPath) {
|
||||
return `/${pageUrlParts[0]}/${childPath}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是顶级菜单或没有父菜单,使用 routeUrl 或 pageUrl
|
||||
let path = menu.routeUrl || '';
|
||||
if (!path && menu.pageUrl) {
|
||||
// 如果没有 routeUrl,尝试从 pageUrl 推断路径
|
||||
// 移除 pageUrl 开头的斜杠(如果有)
|
||||
const cleanPageUrl = menu.pageUrl.replace(/^\/+/, '');
|
||||
const pageUrlParts = cleanPageUrl.split('/').filter(p => p); // 过滤空字符串
|
||||
if (pageUrlParts.length >= 2) {
|
||||
// 如果有父路径和文件名,构建路径
|
||||
const parentDir = pageUrlParts[0];
|
||||
const fileName = pageUrlParts[1];
|
||||
// 对于 shipping/shippingList,路由是 shippinglist(小写)
|
||||
if (parentDir === 'shipping' && fileName.toLowerCase().includes('shippinglist')) {
|
||||
path = '/shipping/shippinglist';
|
||||
} else {
|
||||
// 默认使用父路径 + list
|
||||
path = `/${parentDir}/list`;
|
||||
}
|
||||
} else if (pageUrlParts.length === 1) {
|
||||
// 如果只有一个部分,直接使用(但确保以 / 开头)
|
||||
path = menu.pageUrl.startsWith('/') ? menu.pageUrl : '/' + menu.pageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
if (path) {
|
||||
// 确保路径以 / 开头
|
||||
if (!path.startsWith('/')) {
|
||||
path = '/' + path;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
return '/';
|
||||
};
|
||||
|
||||
targetPath = buildRoutePath(firstMenu, ret.data);
|
||||
|
||||
// 确保 targetPath 是字符串类型
|
||||
if (typeof targetPath !== 'string') {
|
||||
targetPath = String(targetPath || '/');
|
||||
}
|
||||
|
||||
// 如果构建失败,使用 pageUrl 作为后备
|
||||
if (!targetPath || targetPath === '/') {
|
||||
if (firstMenu.pageUrl) {
|
||||
// 从 pageUrl 推断路由路径
|
||||
const pageUrlParts = firstMenu.pageUrl.split('/');
|
||||
if (pageUrlParts.length >= 2) {
|
||||
// 如果有父路径和文件名,尝试构建路径
|
||||
const parentDir = pageUrlParts[0];
|
||||
const fileName = pageUrlParts[1];
|
||||
|
||||
// 检查是否有 routeUrl
|
||||
if (firstMenu.routeUrl) {
|
||||
targetPath = `/${parentDir}/${firstMenu.routeUrl}`;
|
||||
} else {
|
||||
// 如果没有 routeUrl,尝试从文件名推断路由路径
|
||||
// 例如:shipping/shippingList -> /shipping/shippinglist
|
||||
// 或者:warehouse/warehouse -> /warehouse/list
|
||||
// 这里需要根据实际路由配置来决定
|
||||
// 对于 shipping/shippingList,路由是 shippinglist(小写)
|
||||
if (parentDir === 'shipping' && fileName.toLowerCase().includes('shippinglist')) {
|
||||
targetPath = '/shipping/shippinglist';
|
||||
} else {
|
||||
// 默认使用 list
|
||||
targetPath = `/${parentDir}/list`;
|
||||
}
|
||||
}
|
||||
} else if (pageUrlParts.length === 1) {
|
||||
// 如果只有一个部分,直接使用(但这种情况应该很少)
|
||||
targetPath = firstMenu.pageUrl.startsWith('/')
|
||||
? firstMenu.pageUrl
|
||||
: `/${firstMenu.pageUrl}`;
|
||||
} else {
|
||||
// 如果 pageUrl 格式不正确,使用默认路径
|
||||
targetPath = '/shipping/loadingOrder';
|
||||
}
|
||||
} else {
|
||||
targetPath = '/shipping/loadingOrder';
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// 默认跳转到装车订单页面
|
||||
@@ -249,10 +407,17 @@ const generateRoutes = async () => {
|
||||
await permissionStore.generateRoutes();
|
||||
}
|
||||
|
||||
// 确保路径以斜杠开头
|
||||
// 确保 targetPath 是字符串类型,并规范化路径
|
||||
if (typeof targetPath !== 'string') {
|
||||
targetPath = String(targetPath || '/');
|
||||
}
|
||||
|
||||
// 确保路径以斜杠开头,并移除多余的斜杠
|
||||
if (!targetPath.startsWith('/')) {
|
||||
targetPath = '/' + targetPath;
|
||||
}
|
||||
// 移除多余的斜杠(但保留开头的单个斜杠)
|
||||
targetPath = targetPath.replace(/\/+/g, '/');
|
||||
|
||||
// 使用replace而不是push,避免路由警告
|
||||
try {
|
||||
|
||||
198
pc-cattle-transportation/src/views/salesOverview/dialog.vue
Normal file
198
pc-cattle-transportation/src/views/salesOverview/dialog.vue
Normal file
@@ -0,0 +1,198 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="data.dialogVisible"
|
||||
:title="data.editId ? '编辑销售概览' : '新增销售概览'"
|
||||
:before-close="handleClose"
|
||||
width="600px"
|
||||
>
|
||||
<el-form ref="formDataRef" :model="ruleForm" :rules="rules" label-width="140px">
|
||||
<el-form-item label="应收货款(元)" prop="accountsReceivable">
|
||||
<el-input-number
|
||||
v-model="ruleForm.accountsReceivable"
|
||||
:precision="2"
|
||||
:min="0"
|
||||
:max="99999999.99"
|
||||
placeholder="请输入应收货款"
|
||||
style="width: 100%"
|
||||
></el-input-number>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="未收货款(元)" prop="uncollectedPayment">
|
||||
<el-input-number
|
||||
v-model="ruleForm.uncollectedPayment"
|
||||
:precision="2"
|
||||
:min="0"
|
||||
:max="99999999.99"
|
||||
placeholder="请输入未收货款"
|
||||
style="width: 100%"
|
||||
></el-input-number>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="实收货款(元)" prop="actualPayment">
|
||||
<el-input-number
|
||||
v-model="ruleForm.actualPayment"
|
||||
:precision="2"
|
||||
:min="0"
|
||||
:max="99999999.99"
|
||||
placeholder="请输入实收货款"
|
||||
style="width: 100%"
|
||||
></el-input-number>
|
||||
</el-form-item>
|
||||
|
||||
<el-alert
|
||||
v-if="data.editId"
|
||||
title="提示"
|
||||
description="采购总额、销售总额、利润、采购数量、采购单数、销售单数等字段由系统自动计算,无法手动修改。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
style="margin-bottom: 20px"
|
||||
/>
|
||||
</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 } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { salesOverviewAdd, salesOverviewEdit } from '@/api/salesOverview.js';
|
||||
|
||||
const emits = defineEmits(['success']);
|
||||
const formDataRef = ref(null);
|
||||
|
||||
const data = reactive({
|
||||
dialogVisible: false,
|
||||
saveLoading: false,
|
||||
editId: null,
|
||||
});
|
||||
|
||||
const ruleForm = reactive({
|
||||
id: null,
|
||||
accountsReceivable: null,
|
||||
uncollectedPayment: null,
|
||||
actualPayment: null,
|
||||
});
|
||||
|
||||
const rules = reactive({
|
||||
accountsReceivable: [
|
||||
{ type: 'number', min: 0, message: '应收货款不能小于0', trigger: 'blur' },
|
||||
],
|
||||
uncollectedPayment: [
|
||||
{ type: 'number', min: 0, message: '未收货款不能小于0', trigger: 'blur' },
|
||||
],
|
||||
actualPayment: [
|
||||
{ type: 'number', min: 0, message: '实收货款不能小于0', trigger: 'blur' },
|
||||
],
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
if (formDataRef.value) {
|
||||
formDataRef.value.resetFields();
|
||||
}
|
||||
// 重置表单数据
|
||||
ruleForm.id = null;
|
||||
ruleForm.accountsReceivable = null;
|
||||
ruleForm.uncollectedPayment = null;
|
||||
ruleForm.actualPayment = null;
|
||||
data.editId = null;
|
||||
data.dialogVisible = false;
|
||||
};
|
||||
|
||||
const onClickSave = () => {
|
||||
if (!formDataRef.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
formDataRef.value.validate((valid) => {
|
||||
if (!valid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
data.saveLoading = true;
|
||||
|
||||
const params = {
|
||||
accountsReceivable: ruleForm.accountsReceivable || 0,
|
||||
uncollectedPayment: ruleForm.uncollectedPayment || 0,
|
||||
actualPayment: ruleForm.actualPayment || 0,
|
||||
};
|
||||
|
||||
if (data.editId) {
|
||||
// 编辑
|
||||
params.id = data.editId;
|
||||
salesOverviewEdit(params)
|
||||
.then((res) => {
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('编辑成功');
|
||||
handleClose();
|
||||
emits('success');
|
||||
} else {
|
||||
ElMessage.error(res.msg || '编辑失败');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
ElMessage.error('编辑失败:' + (error.message || '未知错误'));
|
||||
})
|
||||
.finally(() => {
|
||||
data.saveLoading = false;
|
||||
});
|
||||
} else {
|
||||
// 新增
|
||||
salesOverviewAdd(params)
|
||||
.then((res) => {
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('新增成功');
|
||||
handleClose();
|
||||
emits('success');
|
||||
} else {
|
||||
ElMessage.error(res.msg || '新增失败');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
ElMessage.error('新增失败:' + (error.message || '未知错误'));
|
||||
})
|
||||
.finally(() => {
|
||||
data.saveLoading = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onShowDialog = (row) => {
|
||||
if (row) {
|
||||
// 编辑
|
||||
data.editId = row.id;
|
||||
ruleForm.id = row.id;
|
||||
ruleForm.accountsReceivable = row.accountsReceivable || null;
|
||||
ruleForm.uncollectedPayment = row.uncollectedPayment || null;
|
||||
ruleForm.actualPayment = row.actualPayment || null;
|
||||
} else {
|
||||
// 新增
|
||||
data.editId = null;
|
||||
ruleForm.id = null;
|
||||
ruleForm.accountsReceivable = null;
|
||||
ruleForm.uncollectedPayment = null;
|
||||
ruleForm.actualPayment = null;
|
||||
}
|
||||
data.dialogVisible = true;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
onShowDialog,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
323
pc-cattle-transportation/src/views/salesOverview/list.vue
Normal file
323
pc-cattle-transportation/src/views/salesOverview/list.vue
Normal file
@@ -0,0 +1,323 @@
|
||||
<template>
|
||||
<div>
|
||||
<base-search :formItemList="formItemList" @search="searchFrom" ref="baseSearchRef"></base-search>
|
||||
|
||||
<!-- 横向滚动操作栏 -->
|
||||
<div class="operation-scroll-bar">
|
||||
<div class="operation-scroll-container">
|
||||
<el-button
|
||||
type="primary"
|
||||
v-hasPermi="['salesoverview:add']"
|
||||
@click="showAddDialog"
|
||||
style="margin-left: 10px"
|
||||
>
|
||||
新增销售概览
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
v-hasPermi="['salesoverview:calculate']"
|
||||
@click="handleCalculate"
|
||||
style="margin-left: 10px"
|
||||
:loading="calculating"
|
||||
>
|
||||
计算统计数据
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-container">
|
||||
<el-table
|
||||
:data="rows"
|
||||
border
|
||||
v-loading="data.dataListLoading"
|
||||
element-loading-text="数据加载中..."
|
||||
style="width: 100%"
|
||||
:show-overflow-tooltip="true"
|
||||
>
|
||||
<el-table-column label="ID" prop="id" min-width="80" width="100">
|
||||
<template #default="scope">
|
||||
{{ scope.row.id || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="采购总额(元)" prop="toalProcurementAmount" min-width="120" width="150">
|
||||
<template #default="scope">
|
||||
{{ scope.row.toalProcurementAmount !== null && scope.row.toalProcurementAmount !== undefined
|
||||
? parseFloat(scope.row.toalProcurementAmount).toFixed(2) : '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="销售总额(元)" prop="toalSalesAmount" min-width="120" width="150">
|
||||
<template #default="scope">
|
||||
{{ scope.row.toalSalesAmount !== null && scope.row.toalSalesAmount !== undefined
|
||||
? parseFloat(scope.row.toalSalesAmount).toFixed(2) : '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="利润(元)" prop="profits" min-width="120" width="150">
|
||||
<template #default="scope">
|
||||
<span :style="{ color: scope.row.profits >= 0 ? '#67C23A' : '#F56C6C' }">
|
||||
{{ scope.row.profits !== null && scope.row.profits !== undefined
|
||||
? parseFloat(scope.row.profits).toFixed(2) : '--' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="应收货款(元)" prop="accountsReceivable" min-width="120" width="150">
|
||||
<template #default="scope">
|
||||
{{ scope.row.accountsReceivable !== null && scope.row.accountsReceivable !== undefined
|
||||
? parseFloat(scope.row.accountsReceivable).toFixed(2) : '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="未收货款(元)" prop="uncollectedPayment" min-width="120" width="150">
|
||||
<template #default="scope">
|
||||
{{ scope.row.uncollectedPayment !== null && scope.row.uncollectedPayment !== undefined
|
||||
? parseFloat(scope.row.uncollectedPayment).toFixed(2) : '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="实收货款(元)" prop="actualPayment" min-width="120" width="150">
|
||||
<template #default="scope">
|
||||
{{ scope.row.actualPayment !== null && scope.row.actualPayment !== undefined
|
||||
? parseFloat(scope.row.actualPayment).toFixed(2) : '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="采购数量(头)" prop="totalPurchase" min-width="120" width="150">
|
||||
<template #default="scope">
|
||||
{{ scope.row.totalPurchase || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="采购单数(车)" prop="totalOrder" min-width="120" width="150">
|
||||
<template #default="scope">
|
||||
{{ scope.row.totalOrder || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="销售单数(单)" prop="totalSales" min-width="120" width="150">
|
||||
<template #default="scope">
|
||||
{{ scope.row.totalSales || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" prop="createTime" min-width="160" width="180">
|
||||
<template #default="scope">
|
||||
{{ scope.row.createTime || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="更新时间" prop="upTime" min-width="160" width="180">
|
||||
<template #default="scope">
|
||||
{{ scope.row.upTime || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="200" width="250" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" v-hasPermi="['salesoverview:edit']" @click="showEditDialog(scope.row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button link type="primary" v-hasPermi="['salesoverview:delete']" @click="del(scope.row.id)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<div class="dataListOnEmpty">暂无数据</div>
|
||||
</template>
|
||||
</el-table>
|
||||
<Pagination
|
||||
v-if="data.total > 0"
|
||||
v-model:limit="form.pageSize"
|
||||
v-model:page="form.pageNum"
|
||||
:total="data.total"
|
||||
@pagination="handlePagination"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<SalesOverviewDialog ref="dialogRef" @success="getDataList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import baseSearch from '@/components/common/searchCustom/index.vue';
|
||||
import Pagination from '@/components/Pagination/index.vue';
|
||||
import SalesOverviewDialog from './dialog.vue';
|
||||
import {
|
||||
salesOverviewList,
|
||||
salesOverviewDelete,
|
||||
calculateSalesOverview,
|
||||
} from '@/api/salesOverview.js';
|
||||
|
||||
const baseSearchRef = ref();
|
||||
const dialogRef = ref();
|
||||
const calculating = ref(false);
|
||||
const data = reactive({
|
||||
total: 0,
|
||||
dataListLoading: false,
|
||||
tableKey: 0,
|
||||
});
|
||||
const rows = ref([]);
|
||||
const form = reactive({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const formItemList = reactive([
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'createTime',
|
||||
type: 'daterange',
|
||||
startPlaceholder: '开始日期',
|
||||
endPlaceholder: '结束日期',
|
||||
},
|
||||
]);
|
||||
|
||||
const searchFrom = () => {
|
||||
form.pageNum = 1;
|
||||
getDataList();
|
||||
};
|
||||
|
||||
const handlePagination = (paginationData) => {
|
||||
if (paginationData) {
|
||||
form.pageNum = paginationData.page || form.pageNum;
|
||||
form.pageSize = paginationData.limit || form.pageSize;
|
||||
}
|
||||
getDataList();
|
||||
};
|
||||
|
||||
const getDataList = () => {
|
||||
data.dataListLoading = true;
|
||||
const searchParams = baseSearchRef.value.penetrateParams();
|
||||
|
||||
const params = {
|
||||
...form,
|
||||
...searchParams,
|
||||
};
|
||||
|
||||
// 处理日期范围参数
|
||||
if (searchParams.createTime && Array.isArray(searchParams.createTime) && searchParams.createTime.length === 2) {
|
||||
params.startTime = searchParams.createTime[0];
|
||||
params.endTime = searchParams.createTime[1];
|
||||
delete params.createTime;
|
||||
}
|
||||
|
||||
console.log('[SALES-OVERVIEW-LIST] 请求参数:', params);
|
||||
salesOverviewList(params)
|
||||
.then((res) => {
|
||||
console.log('[SALES-OVERVIEW-LIST] 响应数据:', res);
|
||||
|
||||
let responseData = res.data || res;
|
||||
|
||||
if (responseData && typeof responseData === 'object' && 'total' in responseData && 'rows' in responseData) {
|
||||
rows.value = responseData.rows || [];
|
||||
data.total = responseData.total || 0;
|
||||
} else if (responseData && responseData.data) {
|
||||
rows.value = responseData.data.rows || [];
|
||||
data.total = responseData.data.total || 0;
|
||||
} else {
|
||||
rows.value = [];
|
||||
data.total = 0;
|
||||
}
|
||||
|
||||
console.log('[SALES-OVERVIEW-LIST] 解析后的数据 - 总数:', data.total, '当前页数据:', rows.value.length);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('[SALES-OVERVIEW-LIST] 请求失败:', error);
|
||||
ElMessage.error('查询失败:' + (error.message || '未知错误'));
|
||||
rows.value = [];
|
||||
data.total = 0;
|
||||
})
|
||||
.finally(() => {
|
||||
data.dataListLoading = false;
|
||||
});
|
||||
};
|
||||
|
||||
const showAddDialog = () => {
|
||||
if (dialogRef.value) {
|
||||
dialogRef.value.onShowDialog(null);
|
||||
}
|
||||
};
|
||||
|
||||
const showEditDialog = (row) => {
|
||||
if (dialogRef.value) {
|
||||
dialogRef.value.onShowDialog(row);
|
||||
}
|
||||
};
|
||||
|
||||
const del = (id) => {
|
||||
ElMessageBox.confirm('确定要删除这条销售概览记录吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
salesOverviewDelete(id)
|
||||
.then((res) => {
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除成功');
|
||||
getDataList();
|
||||
} else {
|
||||
ElMessage.error(res.msg || '删除失败');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
ElMessage.error('删除失败:' + (error.message || '未知错误'));
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// 用户取消删除
|
||||
});
|
||||
};
|
||||
|
||||
const handleCalculate = () => {
|
||||
ElMessageBox.confirm('确定要重新计算销售概览统计数据吗?此操作可能需要较长时间。', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
calculating.value = true;
|
||||
calculateSalesOverview()
|
||||
.then((res) => {
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('计算成功');
|
||||
getDataList();
|
||||
} else {
|
||||
ElMessage.error(res.msg || '计算失败');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
ElMessage.error('计算失败:' + (error.message || '未知错误'));
|
||||
})
|
||||
.finally(() => {
|
||||
calculating.value = false;
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// 用户取消计算
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getDataList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.operation-scroll-bar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.operation-scroll-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.dataListOnEmpty {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
color: #909399;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -359,10 +359,12 @@
|
||||
<div class="photo-upload-wrapper">
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
drag
|
||||
action="/api/common/upload"
|
||||
:show-file-list="false"
|
||||
:on-success="handleQuarantinePhotoSuccess"
|
||||
:headers="uploadHeaders"
|
||||
accept="image/*"
|
||||
>
|
||||
<el-image
|
||||
v-if="formData.quarantineTickeyUrl"
|
||||
@@ -374,7 +376,10 @@
|
||||
preview-teleported
|
||||
preview-disabled
|
||||
/>
|
||||
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
|
||||
<template v-else>
|
||||
<el-icon class="avatar-uploader-icon"><Plus /></el-icon>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<el-button
|
||||
v-if="formData.quarantineTickeyUrl"
|
||||
@@ -402,10 +407,12 @@
|
||||
<div class="photo-upload-wrapper">
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
drag
|
||||
action="/api/common/upload"
|
||||
:show-file-list="false"
|
||||
:on-success="handlePoundListPhotoSuccess"
|
||||
:headers="uploadHeaders"
|
||||
accept="image/*"
|
||||
>
|
||||
<el-image
|
||||
v-if="formData.poundListImg"
|
||||
@@ -417,7 +424,10 @@
|
||||
preview-teleported
|
||||
preview-disabled
|
||||
/>
|
||||
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
|
||||
<template v-else>
|
||||
<el-icon class="avatar-uploader-icon"><Plus /></el-icon>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<el-button
|
||||
v-if="formData.poundListImg"
|
||||
@@ -447,10 +457,12 @@
|
||||
<div class="photo-upload-wrapper">
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
drag
|
||||
action="/api/common/upload"
|
||||
:show-file-list="false"
|
||||
:on-success="handleEmptyVehicleFrontPhotoSuccess"
|
||||
:headers="uploadHeaders"
|
||||
accept="image/*"
|
||||
>
|
||||
<el-image
|
||||
v-if="formData.emptyVehicleFrontPhoto"
|
||||
@@ -462,7 +474,10 @@
|
||||
preview-teleported
|
||||
preview-disabled
|
||||
/>
|
||||
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
|
||||
<template v-else>
|
||||
<el-icon class="avatar-uploader-icon"><Plus /></el-icon>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<el-button
|
||||
v-if="formData.emptyVehicleFrontPhoto"
|
||||
@@ -490,10 +505,12 @@
|
||||
<div class="photo-upload-wrapper">
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
drag
|
||||
action="/api/common/upload"
|
||||
:show-file-list="false"
|
||||
:on-success="handleLoadedVehicleFrontPhotoSuccess"
|
||||
:headers="uploadHeaders"
|
||||
accept="image/*"
|
||||
>
|
||||
<el-image
|
||||
v-if="formData.loadedVehicleFrontPhoto"
|
||||
@@ -505,7 +522,10 @@
|
||||
preview-teleported
|
||||
preview-disabled
|
||||
/>
|
||||
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
|
||||
<template v-else>
|
||||
<el-icon class="avatar-uploader-icon"><Plus /></el-icon>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<el-button
|
||||
v-if="formData.loadedVehicleFrontPhoto"
|
||||
@@ -535,10 +555,12 @@
|
||||
<div class="photo-upload-wrapper">
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
drag
|
||||
action="/api/common/upload"
|
||||
:show-file-list="false"
|
||||
:on-success="handleLoadedVehicleWeightPhotoSuccess"
|
||||
:headers="uploadHeaders"
|
||||
accept="image/*"
|
||||
>
|
||||
<el-image
|
||||
v-if="formData.loadedVehicleWeightPhoto"
|
||||
@@ -550,7 +572,10 @@
|
||||
preview-teleported
|
||||
preview-disabled
|
||||
/>
|
||||
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
|
||||
<template v-else>
|
||||
<el-icon class="avatar-uploader-icon"><Plus /></el-icon>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<el-button
|
||||
v-if="formData.loadedVehicleWeightPhoto"
|
||||
@@ -578,10 +603,12 @@
|
||||
<div class="photo-upload-wrapper">
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
drag
|
||||
action="/api/common/upload"
|
||||
:show-file-list="false"
|
||||
:on-success="handleDriverIdCardPhotoSuccess"
|
||||
:headers="uploadHeaders"
|
||||
accept="image/*"
|
||||
>
|
||||
<el-image
|
||||
v-if="formData.driverIdCardPhoto"
|
||||
@@ -593,7 +620,10 @@
|
||||
preview-teleported
|
||||
preview-disabled
|
||||
/>
|
||||
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
|
||||
<template v-else>
|
||||
<el-icon class="avatar-uploader-icon"><Plus /></el-icon>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<el-button
|
||||
v-if="formData.driverIdCardPhoto"
|
||||
@@ -623,10 +653,12 @@
|
||||
<div class="photo-upload-wrapper">
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
drag
|
||||
action="/api/common/upload"
|
||||
:show-file-list="false"
|
||||
:on-success="handleDestinationPoundListPhotoSuccess"
|
||||
:headers="uploadHeaders"
|
||||
accept="image/*"
|
||||
>
|
||||
<el-image
|
||||
v-if="formData.destinationPoundListImg"
|
||||
@@ -638,7 +670,10 @@
|
||||
preview-teleported
|
||||
preview-disabled
|
||||
/>
|
||||
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
|
||||
<template v-else>
|
||||
<el-icon class="avatar-uploader-icon"><Plus /></el-icon>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<el-button
|
||||
v-if="formData.destinationPoundListImg"
|
||||
@@ -666,10 +701,12 @@
|
||||
<div class="photo-upload-wrapper">
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
drag
|
||||
action="/api/common/upload"
|
||||
:show-file-list="false"
|
||||
:on-success="handleDestinationVehicleFrontPhotoSuccess"
|
||||
:headers="uploadHeaders"
|
||||
accept="image/*"
|
||||
>
|
||||
<el-image
|
||||
v-if="formData.destinationVehicleFrontPhoto"
|
||||
@@ -681,7 +718,10 @@
|
||||
preview-teleported
|
||||
preview-disabled
|
||||
/>
|
||||
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
|
||||
<template v-else>
|
||||
<el-icon class="avatar-uploader-icon"><Plus /></el-icon>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<el-button
|
||||
v-if="formData.destinationVehicleFrontPhoto"
|
||||
@@ -714,6 +754,7 @@
|
||||
<el-col :span="12">
|
||||
<el-form-item label="装车过磅视频" prop="entruckWeightVideo" label-width="180px">
|
||||
<el-upload
|
||||
drag
|
||||
accept=".mp4,.avi,.rmvb,.mkv,.MP4,.AVI,.RMVB,.MKV"
|
||||
class="upload-demo"
|
||||
action="/api/common/upload"
|
||||
@@ -723,7 +764,11 @@
|
||||
:on-success="handleEntruckWeightVideoSuccess"
|
||||
:on-remove="() => handleRemoveVideo('entruckWeightVideo')"
|
||||
>
|
||||
<el-button type="primary">上传</el-button>
|
||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">将视频文件拖到此处,或<em>点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">支持 mp4/avi/rmvb/mkv 格式</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<div v-if="formData.entruckWeightVideo" class="video-preview-wrapper">
|
||||
<video :src="formData.entruckWeightVideo" style="width: 100%; max-height: 300px; margin-top: 10px" controls></video>
|
||||
@@ -741,6 +786,7 @@
|
||||
<el-col :span="12">
|
||||
<el-form-item label="空车过磅视频" prop="emptyWeightVideo" label-width="180px">
|
||||
<el-upload
|
||||
drag
|
||||
accept=".mp4,.avi,.rmvb,.mkv,.MP4,.AVI,.RMVB,.MKV"
|
||||
class="upload-demo"
|
||||
action="/api/common/upload"
|
||||
@@ -750,7 +796,11 @@
|
||||
:on-success="handleEmptyWeightVideoSuccess"
|
||||
:on-remove="() => handleRemoveVideo('emptyWeightVideo')"
|
||||
>
|
||||
<el-button type="primary">上传</el-button>
|
||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">将视频文件拖到此处,或<em>点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">支持 mp4/avi/rmvb/mkv 格式</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<div v-if="formData.emptyWeightVideo" class="video-preview-wrapper">
|
||||
<video :src="formData.emptyWeightVideo" style="width: 100%; max-height: 300px; margin-top: 10px" controls></video>
|
||||
@@ -770,6 +820,7 @@
|
||||
<el-col :span="12">
|
||||
<el-form-item label="装牛视频" prop="cattleLoadingVideo" label-width="180px">
|
||||
<el-upload
|
||||
drag
|
||||
accept=".mp4,.avi,.rmvb,.mkv,.MP4,.AVI,.RMVB,.MKV"
|
||||
class="upload-demo"
|
||||
action="/api/common/upload"
|
||||
@@ -779,7 +830,11 @@
|
||||
:on-success="handleCattleLoadingVideoSuccess"
|
||||
:on-remove="() => handleRemoveVideo('cattleLoadingVideo')"
|
||||
>
|
||||
<el-button type="primary">上传</el-button>
|
||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">将视频文件拖到此处,或<em>点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">支持 mp4/avi/rmvb/mkv 格式</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<div v-if="formData.cattleLoadingVideo" class="video-preview-wrapper">
|
||||
<video :src="formData.cattleLoadingVideo" style="width: 100%; max-height: 300px; margin-top: 10px" controls></video>
|
||||
@@ -797,6 +852,7 @@
|
||||
<el-col :span="12">
|
||||
<el-form-item label="控槽视频" prop="controlSlotVideo" label-width="180px">
|
||||
<el-upload
|
||||
drag
|
||||
accept=".mp4,.avi,.rmvb,.mkv,.MP4,.AVI,.RMVB,.MKV"
|
||||
class="upload-demo"
|
||||
action="/api/common/upload"
|
||||
@@ -806,7 +862,11 @@
|
||||
:on-success="handleControlSlotVideoSuccess"
|
||||
:on-remove="() => handleRemoveVideo('controlSlotVideo')"
|
||||
>
|
||||
<el-button type="primary">上传</el-button>
|
||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">将视频文件拖到此处,或<em>点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">支持 mp4/avi/rmvb/mkv 格式</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<div v-if="formData.controlSlotVideo" class="video-preview-wrapper">
|
||||
<video :src="formData.controlSlotVideo" style="width: 100%; max-height: 300px; margin-top: 10px" controls></video>
|
||||
@@ -826,6 +886,7 @@
|
||||
<el-col :span="12">
|
||||
<el-form-item label="装完牛绕车一圈视频" prop="cattleLoadingCircleVideo" label-width="180px">
|
||||
<el-upload
|
||||
drag
|
||||
accept=".mp4,.avi,.rmvb,.mkv,.MP4,.AVI,.RMVB,.MKV"
|
||||
class="upload-demo"
|
||||
action="/api/common/upload"
|
||||
@@ -835,7 +896,11 @@
|
||||
:on-success="handleCattleLoadingCircleVideoSuccess"
|
||||
:on-remove="() => handleRemoveVideo('cattleLoadingCircleVideo')"
|
||||
>
|
||||
<el-button type="primary">上传</el-button>
|
||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">将视频文件拖到此处,或<em>点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">支持 mp4/avi/rmvb/mkv 格式</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<div v-if="formData.cattleLoadingCircleVideo" class="video-preview-wrapper">
|
||||
<video :src="formData.cattleLoadingCircleVideo" style="width: 100%; max-height: 300px; margin-top: 10px" controls></video>
|
||||
@@ -853,6 +918,7 @@
|
||||
<el-col :span="12">
|
||||
<el-form-item label="卸牛视频" prop="unloadCattleVideo" label-width="180px">
|
||||
<el-upload
|
||||
drag
|
||||
accept=".mp4,.avi,.rmvb,.mkv,.MP4,.AVI,.RMVB,.MKV"
|
||||
class="upload-demo"
|
||||
action="/api/common/upload"
|
||||
@@ -862,7 +928,11 @@
|
||||
:on-success="handleUnloadCattleVideoSuccess"
|
||||
:on-remove="() => handleRemoveVideo('unloadCattleVideo')"
|
||||
>
|
||||
<el-button type="primary">上传</el-button>
|
||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">将视频文件拖到此处,或<em>点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">支持 mp4/avi/rmvb/mkv 格式</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<div v-if="formData.unloadCattleVideo" class="video-preview-wrapper">
|
||||
<video :src="formData.unloadCattleVideo" style="width: 100%; max-height: 300px; margin-top: 10px" controls></video>
|
||||
@@ -882,6 +952,7 @@
|
||||
<el-col :span="12">
|
||||
<el-form-item label="落地过磅视频" prop="destinationWeightVideo" label-width="180px">
|
||||
<el-upload
|
||||
drag
|
||||
accept=".mp4,.avi,.rmvb,.mkv,.MP4,.AVI,.RMVB,.MKV"
|
||||
class="upload-demo"
|
||||
action="/api/common/upload"
|
||||
@@ -891,7 +962,11 @@
|
||||
:on-success="handleDestinationWeightVideoSuccess"
|
||||
:on-remove="() => handleRemoveVideo('destinationWeightVideo')"
|
||||
>
|
||||
<el-button type="primary">上传</el-button>
|
||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">将视频文件拖到此处,或<em>点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">支持 mp4/avi/rmvb/mkv 格式</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<div v-if="formData.destinationWeightVideo" class="video-preview-wrapper">
|
||||
<video :src="formData.destinationWeightVideo" style="width: 100%; max-height: 300px; margin-top: 10px" controls></video>
|
||||
@@ -2545,13 +2620,13 @@ let watchTimer = null;
|
||||
height: 178px;
|
||||
display: block;
|
||||
}
|
||||
:deep(.avatar-uploader .el-icon) {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
:deep(.avatar-uploader .el-upload-dragger) {
|
||||
width: 178px;
|
||||
height: 178px;
|
||||
line-height: 178px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
@@ -2559,9 +2634,24 @@ let watchTimer = null;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
:deep(.avatar-uploader .el-icon:hover) {
|
||||
:deep(.avatar-uploader .el-upload-dragger:hover) {
|
||||
border-color: #409eff;
|
||||
}
|
||||
:deep(.avatar-uploader .el-icon) {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
:deep(.avatar-uploader .el-upload__text) {
|
||||
color: #606266;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
}
|
||||
:deep(.avatar-uploader .el-upload__text em) {
|
||||
color: #409eff;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* 照片上传包装器 */
|
||||
.photo-upload-wrapper {
|
||||
@@ -2569,6 +2659,28 @@ let watchTimer = null;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* 拖拽上传样式优化 */
|
||||
:deep(.avatar-uploader.is-drag) {
|
||||
width: 178px;
|
||||
height: 178px;
|
||||
}
|
||||
|
||||
:deep(.avatar-uploader.is-drag .el-upload-dragger) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:deep(.avatar-uploader.is-drag .el-upload-dragger .el-icon) {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
:deep(.avatar-uploader.is-drag .el-upload__text) {
|
||||
font-size: 11px;
|
||||
padding: 0 8px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.photo-upload-wrapper .preview-btn {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
|
||||
@@ -447,7 +447,7 @@ const initTrackMap = async () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const BMapGL = await BMPGL('xITbC7jegaAAuu4m9jC2Zx6eFbQJ29Rj');
|
||||
const BMapGL = await BMPGL('3AN3VahoqaXUs32U8luXD2Dwn86KK5B7');
|
||||
|
||||
// 创建地图实例
|
||||
trackMapInstance.value = new BMapGL.Map('trackMap');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-dialog v-model="data.dialogVisible" title="创建订单" :before-close="handleClose" width="600px">
|
||||
<el-dialog v-model="data.dialogVisible" :title="ruleForm.id ? '编辑订单' : '创建订单'" :before-close="handleClose" width="600px">
|
||||
<el-form ref="formDataRef" :model="ruleForm" :rules="rules" label-width="120px">
|
||||
<el-form-item label="卖方" prop="sellerId">
|
||||
<el-select
|
||||
@@ -79,6 +79,17 @@
|
||||
style="width: 100%"
|
||||
></el-input-number>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="预付款(元)" prop="advancePayment">
|
||||
<el-input-number
|
||||
v-model="ruleForm.advancePayment"
|
||||
:precision="2"
|
||||
:min="0"
|
||||
:max="9999999.99"
|
||||
placeholder="请输入预付款"
|
||||
style="width: 100%"
|
||||
></el-input-number>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
@@ -93,7 +104,7 @@
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { orderAddNew, orderUpdate, updateDeliveryInfo } from '@/api/shipping.js';
|
||||
import { orderAddNew, orderUpdate, updateDeliveryInfo, getOrdersByDeliveryId } from '@/api/shipping.js';
|
||||
import { memberListByType } from '@/api/userManage.js';
|
||||
|
||||
const emits = defineEmits();
|
||||
@@ -120,6 +131,7 @@ const ruleForm = reactive({
|
||||
sellerId: null, // 卖方ID(单选)
|
||||
settlementType: 1, // 结算方式:1-上车重量,2-下车重量,3-按肉价结算
|
||||
firmPrice: null, // 约定价格(元/斤)
|
||||
advancePayment: null, // 预付款(元)
|
||||
deliveryId: null, // 运送清单ID(关联时使用)
|
||||
});
|
||||
|
||||
@@ -154,6 +166,10 @@ const rules = reactive({
|
||||
{ required: true, message: '请输入约定价格', trigger: 'blur' },
|
||||
{ type: 'number', min: 0, message: '约定价格不能小于0', trigger: 'blur' }
|
||||
],
|
||||
advancePayment: [
|
||||
{ required: true, message: '请输入预付款', trigger: 'blur' },
|
||||
{ type: 'number', min: 0, message: '预付款不能小于0', trigger: 'blur' }
|
||||
],
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
@@ -166,6 +182,7 @@ const handleClose = () => {
|
||||
ruleForm.sellerId = null;
|
||||
ruleForm.settlementType = 1;
|
||||
ruleForm.firmPrice = null;
|
||||
ruleForm.advancePayment = null;
|
||||
ruleForm.deliveryId = null;
|
||||
data.dialogVisible = false;
|
||||
};
|
||||
@@ -297,6 +314,7 @@ const onClickSave = () => {
|
||||
sellerId: ruleForm.sellerId ? String(ruleForm.sellerId) : '', // 转为字符串格式
|
||||
settlementType: ruleForm.settlementType,
|
||||
firmPrice: ruleForm.firmPrice, // 约定价格(元/斤)
|
||||
advancePayment: ruleForm.advancePayment, // 预付款(元)
|
||||
};
|
||||
|
||||
// 如果是新增订单且有deliveryId,传递deliveryId参数
|
||||
@@ -374,7 +392,255 @@ const onClickSave = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const onShowDialog = (orderData, deliveryId) => {
|
||||
// 根据名称查找用户ID(卖方)
|
||||
const findSellerIdByName = async (sellerName) => {
|
||||
if (!sellerName || sellerName.trim() === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 设置搜索名称并查询
|
||||
data.sellerName = sellerName.trim();
|
||||
data.sellerPageNum = 1;
|
||||
|
||||
// 同时查询供应商(type=2)和采购商(type=4)
|
||||
const [supplierRes, purchaserRes] = await Promise.all([
|
||||
memberListByType({
|
||||
pageNum: 1,
|
||||
pageSize: 100, // 增加查询数量以提高匹配概率
|
||||
type: 2,
|
||||
username: sellerName.trim(),
|
||||
}),
|
||||
memberListByType({
|
||||
pageNum: 1,
|
||||
pageSize: 100,
|
||||
type: 4,
|
||||
username: sellerName.trim(),
|
||||
})
|
||||
]);
|
||||
|
||||
// 合并结果并去重
|
||||
const supplierList = supplierRes.data.rows || [];
|
||||
const purchaserList = purchaserRes.data.rows || [];
|
||||
const mergedList = [...supplierList, ...purchaserList];
|
||||
|
||||
// 去重:使用 Map 根据 id 去重
|
||||
const uniqueMap = new Map();
|
||||
mergedList.forEach(item => {
|
||||
if (!uniqueMap.has(item.id)) {
|
||||
uniqueMap.set(item.id, item);
|
||||
}
|
||||
});
|
||||
const allSellers = Array.from(uniqueMap.values());
|
||||
|
||||
// 精确匹配用户名
|
||||
const matchedSeller = allSellers.find(item =>
|
||||
item.username === sellerName.trim() ||
|
||||
item.username?.includes(sellerName.trim()) ||
|
||||
item.mobile === sellerName.trim()
|
||||
);
|
||||
|
||||
if (matchedSeller) {
|
||||
// 将匹配的用户添加到选项列表中
|
||||
if (!data.sellerOptions.find(item => item.id === matchedSeller.id)) {
|
||||
data.sellerOptions.unshift(matchedSeller);
|
||||
}
|
||||
return matchedSeller.id;
|
||||
}
|
||||
|
||||
// 如果没有精确匹配,返回第一个结果(如果存在)
|
||||
if (allSellers.length > 0) {
|
||||
if (!data.sellerOptions.find(item => item.id === allSellers[0].id)) {
|
||||
data.sellerOptions.unshift(allSellers[0]);
|
||||
}
|
||||
return allSellers[0].id;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('查找卖方失败:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 根据名称查找用户ID(买方)
|
||||
const findBuyerIdByName = async (buyerName) => {
|
||||
if (!buyerName || buyerName.trim() === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 设置搜索名称并查询
|
||||
data.buyerName = buyerName.trim();
|
||||
data.buyerPageNum = 1;
|
||||
|
||||
// 同时查询供应商(type=2)和采购商(type=4)
|
||||
const [supplierRes, purchaserRes] = await Promise.all([
|
||||
memberListByType({
|
||||
pageNum: 1,
|
||||
pageSize: 100, // 增加查询数量以提高匹配概率
|
||||
type: 2,
|
||||
username: buyerName.trim(),
|
||||
}),
|
||||
memberListByType({
|
||||
pageNum: 1,
|
||||
pageSize: 100,
|
||||
type: 4,
|
||||
username: buyerName.trim(),
|
||||
})
|
||||
]);
|
||||
|
||||
// 合并结果并去重
|
||||
const supplierList = supplierRes.data.rows || [];
|
||||
const purchaserList = purchaserRes.data.rows || [];
|
||||
const mergedList = [...supplierList, ...purchaserList];
|
||||
|
||||
// 去重:使用 Map 根据 id 去重
|
||||
const uniqueMap = new Map();
|
||||
mergedList.forEach(item => {
|
||||
if (!uniqueMap.has(item.id)) {
|
||||
uniqueMap.set(item.id, item);
|
||||
}
|
||||
});
|
||||
const allBuyers = Array.from(uniqueMap.values());
|
||||
|
||||
// 精确匹配用户名
|
||||
const matchedBuyer = allBuyers.find(item =>
|
||||
item.username === buyerName.trim() ||
|
||||
item.username?.includes(buyerName.trim()) ||
|
||||
item.mobile === buyerName.trim()
|
||||
);
|
||||
|
||||
if (matchedBuyer) {
|
||||
// 将匹配的用户添加到选项列表中
|
||||
if (!data.buyerOptions.find(item => item.id === matchedBuyer.id)) {
|
||||
data.buyerOptions.unshift(matchedBuyer);
|
||||
}
|
||||
return matchedBuyer.id;
|
||||
}
|
||||
|
||||
// 如果没有精确匹配,返回第一个结果(如果存在)
|
||||
if (allBuyers.length > 0) {
|
||||
if (!data.buyerOptions.find(item => item.id === allBuyers[0].id)) {
|
||||
data.buyerOptions.unshift(allBuyers[0]);
|
||||
}
|
||||
return allBuyers[0].id;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('查找买方失败:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 根据ID查找用户信息并添加到选项列表(卖方)
|
||||
const loadSellerById = async (sellerId) => {
|
||||
if (!sellerId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const id = typeof sellerId === 'string' ? parseInt(sellerId.split(',')[0]) : sellerId;
|
||||
|
||||
// 先检查是否已经在选项列表中
|
||||
const existingSeller = data.sellerOptions.find(item => item.id === id);
|
||||
if (existingSeller) {
|
||||
return id;
|
||||
}
|
||||
|
||||
// 如果没有,尝试通过搜索查找(使用空字符串搜索所有用户)
|
||||
data.sellerName = '';
|
||||
data.sellerPageNum = 1;
|
||||
|
||||
const [supplierRes, purchaserRes] = await Promise.all([
|
||||
memberListByType({
|
||||
pageNum: 1,
|
||||
pageSize: 100,
|
||||
type: 2,
|
||||
username: '',
|
||||
}),
|
||||
memberListByType({
|
||||
pageNum: 1,
|
||||
pageSize: 100,
|
||||
type: 4,
|
||||
username: '',
|
||||
})
|
||||
]);
|
||||
|
||||
const supplierList = supplierRes.data.rows || [];
|
||||
const purchaserList = purchaserRes.data.rows || [];
|
||||
const mergedList = [...supplierList, ...purchaserList];
|
||||
|
||||
const foundSeller = mergedList.find(item => item.id === id);
|
||||
if (foundSeller) {
|
||||
if (!data.sellerOptions.find(item => item.id === foundSeller.id)) {
|
||||
data.sellerOptions.unshift(foundSeller);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
return id; // 即使找不到用户信息,也返回ID
|
||||
} catch (error) {
|
||||
console.error('根据ID加载卖方失败:', error);
|
||||
return sellerId;
|
||||
}
|
||||
};
|
||||
|
||||
// 根据ID查找用户信息并添加到选项列表(买方)
|
||||
const loadBuyerById = async (buyerId) => {
|
||||
if (!buyerId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const id = typeof buyerId === 'string' ? parseInt(buyerId.split(',')[0]) : buyerId;
|
||||
|
||||
// 先检查是否已经在选项列表中
|
||||
const existingBuyer = data.buyerOptions.find(item => item.id === id);
|
||||
if (existingBuyer) {
|
||||
return id;
|
||||
}
|
||||
|
||||
// 如果没有,尝试通过搜索查找(使用空字符串搜索所有用户)
|
||||
data.buyerName = '';
|
||||
data.buyerPageNum = 1;
|
||||
|
||||
const [supplierRes, purchaserRes] = await Promise.all([
|
||||
memberListByType({
|
||||
pageNum: 1,
|
||||
pageSize: 100,
|
||||
type: 2,
|
||||
username: '',
|
||||
}),
|
||||
memberListByType({
|
||||
pageNum: 1,
|
||||
pageSize: 100,
|
||||
type: 4,
|
||||
username: '',
|
||||
})
|
||||
]);
|
||||
|
||||
const supplierList = supplierRes.data.rows || [];
|
||||
const purchaserList = purchaserRes.data.rows || [];
|
||||
const mergedList = [...supplierList, ...purchaserList];
|
||||
|
||||
const foundBuyer = mergedList.find(item => item.id === id);
|
||||
if (foundBuyer) {
|
||||
if (!data.buyerOptions.find(item => item.id === foundBuyer.id)) {
|
||||
data.buyerOptions.unshift(foundBuyer);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
return id; // 即使找不到用户信息,也返回ID
|
||||
} catch (error) {
|
||||
console.error('根据ID加载买方失败:', error);
|
||||
return buyerId;
|
||||
}
|
||||
};
|
||||
|
||||
const onShowDialog = async (orderData, deliveryId) => {
|
||||
if (formDataRef.value) {
|
||||
formDataRef.value.resetFields();
|
||||
}
|
||||
@@ -382,15 +648,36 @@ const onShowDialog = (orderData, deliveryId) => {
|
||||
// 重置表单数据
|
||||
ruleForm.id = orderData?.id || null;
|
||||
|
||||
// 处理buyerId:支持字符串、数组和数字格式,取第一个值(单选模式)
|
||||
if (orderData?.buyerId) {
|
||||
if (typeof orderData.buyerId === 'number') {
|
||||
ruleForm.buyerId = orderData.buyerId;
|
||||
} else if (Array.isArray(orderData.buyerId) && orderData.buyerId.length > 0) {
|
||||
ruleForm.buyerId = parseInt(orderData.buyerId[0]);
|
||||
} else if (typeof orderData.buyerId === 'string' && orderData.buyerId.trim() !== '') {
|
||||
const ids = orderData.buyerId.split(',').map(id => id.trim()).filter(id => id !== '');
|
||||
ruleForm.buyerId = ids.length > 0 ? parseInt(ids[0]) : null;
|
||||
// 处理buyerId:优先使用buyerName查找,如果没有名称则使用buyerId
|
||||
if (orderData?.buyerName) {
|
||||
// 优先根据名称查找
|
||||
const foundBuyerId = await findBuyerIdByName(orderData.buyerName);
|
||||
ruleForm.buyerId = foundBuyerId;
|
||||
|
||||
// 如果名称查找失败,但有buyerId,则使用ID并加载用户信息
|
||||
if (!foundBuyerId && orderData?.buyerId) {
|
||||
const buyerId = typeof orderData.buyerId === 'number'
|
||||
? orderData.buyerId
|
||||
: (Array.isArray(orderData.buyerId) && orderData.buyerId.length > 0
|
||||
? parseInt(orderData.buyerId[0])
|
||||
: (typeof orderData.buyerId === 'string' && orderData.buyerId.trim() !== ''
|
||||
? parseInt(orderData.buyerId.split(',')[0])
|
||||
: null));
|
||||
if (buyerId) {
|
||||
ruleForm.buyerId = await loadBuyerById(buyerId);
|
||||
}
|
||||
}
|
||||
} else if (orderData?.buyerId) {
|
||||
// 如果没有名称但有ID,使用ID并加载用户信息
|
||||
const buyerId = typeof orderData.buyerId === 'number'
|
||||
? orderData.buyerId
|
||||
: (Array.isArray(orderData.buyerId) && orderData.buyerId.length > 0
|
||||
? parseInt(orderData.buyerId[0])
|
||||
: (typeof orderData.buyerId === 'string' && orderData.buyerId.trim() !== ''
|
||||
? parseInt(orderData.buyerId.split(',')[0])
|
||||
: null));
|
||||
if (buyerId) {
|
||||
ruleForm.buyerId = await loadBuyerById(buyerId);
|
||||
} else {
|
||||
ruleForm.buyerId = null;
|
||||
}
|
||||
@@ -398,15 +685,36 @@ const onShowDialog = (orderData, deliveryId) => {
|
||||
ruleForm.buyerId = null;
|
||||
}
|
||||
|
||||
// 处理sellerId:支持字符串、数组和数字格式,取第一个值(单选模式)
|
||||
if (orderData?.sellerId) {
|
||||
if (typeof orderData.sellerId === 'number') {
|
||||
ruleForm.sellerId = orderData.sellerId;
|
||||
} else if (Array.isArray(orderData.sellerId) && orderData.sellerId.length > 0) {
|
||||
ruleForm.sellerId = parseInt(orderData.sellerId[0]);
|
||||
} else if (typeof orderData.sellerId === 'string' && orderData.sellerId.trim() !== '') {
|
||||
const ids = orderData.sellerId.split(',').map(id => id.trim()).filter(id => id !== '');
|
||||
ruleForm.sellerId = ids.length > 0 ? parseInt(ids[0]) : null;
|
||||
// 处理sellerId:优先使用sellerName查找,如果没有名称则使用sellerId
|
||||
if (orderData?.sellerName) {
|
||||
// 优先根据名称查找
|
||||
const foundSellerId = await findSellerIdByName(orderData.sellerName);
|
||||
ruleForm.sellerId = foundSellerId;
|
||||
|
||||
// 如果名称查找失败,但有sellerId,则使用ID并加载用户信息
|
||||
if (!foundSellerId && orderData?.sellerId) {
|
||||
const sellerId = typeof orderData.sellerId === 'number'
|
||||
? orderData.sellerId
|
||||
: (Array.isArray(orderData.sellerId) && orderData.sellerId.length > 0
|
||||
? parseInt(orderData.sellerId[0])
|
||||
: (typeof orderData.sellerId === 'string' && orderData.sellerId.trim() !== ''
|
||||
? parseInt(orderData.sellerId.split(',')[0])
|
||||
: null));
|
||||
if (sellerId) {
|
||||
ruleForm.sellerId = await loadSellerById(sellerId);
|
||||
}
|
||||
}
|
||||
} else if (orderData?.sellerId) {
|
||||
// 如果没有名称但有ID,使用ID并加载用户信息
|
||||
const sellerId = typeof orderData.sellerId === 'number'
|
||||
? orderData.sellerId
|
||||
: (Array.isArray(orderData.sellerId) && orderData.sellerId.length > 0
|
||||
? parseInt(orderData.sellerId[0])
|
||||
: (typeof orderData.sellerId === 'string' && orderData.sellerId.trim() !== ''
|
||||
? parseInt(orderData.sellerId.split(',')[0])
|
||||
: null));
|
||||
if (sellerId) {
|
||||
ruleForm.sellerId = await loadSellerById(sellerId);
|
||||
} else {
|
||||
ruleForm.sellerId = null;
|
||||
}
|
||||
@@ -416,13 +724,51 @@ const onShowDialog = (orderData, deliveryId) => {
|
||||
|
||||
ruleForm.settlementType = orderData?.settlementType || 1;
|
||||
ruleForm.firmPrice = orderData?.firmPrice != null ? orderData.firmPrice : null;
|
||||
|
||||
// 设置deliveryId(如果提供)
|
||||
ruleForm.deliveryId = deliveryId || orderData?.deliveryId || null;
|
||||
|
||||
// 处理预付款自动填充
|
||||
// 1. 如果编辑订单,直接使用订单数据中的预付款
|
||||
if (orderData?.advancePayment != null) {
|
||||
ruleForm.advancePayment = orderData.advancePayment;
|
||||
}
|
||||
// 2. 如果是创建订单且有deliveryId,尝试从已关联的订单中获取预付款作为默认值
|
||||
else if (!ruleForm.id && ruleForm.deliveryId) {
|
||||
try {
|
||||
const res = await getOrdersByDeliveryId(ruleForm.deliveryId);
|
||||
if (res.code === 200) {
|
||||
const responseData = res.data || res;
|
||||
let orders = [];
|
||||
if (responseData && typeof responseData === 'object' && 'rows' in responseData) {
|
||||
orders = responseData.rows || [];
|
||||
} else if (responseData && responseData.data && 'rows' in responseData.data) {
|
||||
orders = responseData.data.rows || [];
|
||||
}
|
||||
|
||||
// 如果有关联的订单,使用第一个订单的预付款作为默认值
|
||||
if (orders.length > 0 && orders[0].advancePayment != null) {
|
||||
ruleForm.advancePayment = orders[0].advancePayment;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取关联订单预付款失败:', error);
|
||||
// 失败不影响继续操作,预付款保持为null
|
||||
}
|
||||
}
|
||||
// 3. 其他情况,预付款为null
|
||||
else {
|
||||
ruleForm.advancePayment = null;
|
||||
}
|
||||
|
||||
data.dialogVisible = true;
|
||||
// 初始化时加载列表
|
||||
getSellerList();
|
||||
getBuyerList();
|
||||
// 初始化时加载列表(如果选项列表为空,则加载默认列表)
|
||||
if (data.sellerOptions.length === 0) {
|
||||
getSellerList();
|
||||
}
|
||||
if (data.buyerOptions.length === 0) {
|
||||
getBuyerList();
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
|
||||
280
pc-cattle-transportation/src/views/warehouse/warehouse.vue
Normal file
280
pc-cattle-transportation/src/views/warehouse/warehouse.vue
Normal file
@@ -0,0 +1,280 @@
|
||||
<template>
|
||||
<div>
|
||||
<base-search :formItemList="formItemList" @search="searchFrom" ref="baseSearchRef"></base-search>
|
||||
|
||||
<!-- 横向滚动操作栏 -->
|
||||
<div class="operation-scroll-bar">
|
||||
<div class="operation-scroll-container">
|
||||
<el-button
|
||||
type="primary"
|
||||
v-hasPermi="['warehouse:add']"
|
||||
@click="showAddDialog"
|
||||
style="margin-left: 10px"
|
||||
>
|
||||
新增中转仓
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-container">
|
||||
<el-table
|
||||
:data="rows"
|
||||
border
|
||||
v-loading="data.dataListLoading"
|
||||
element-loading-text="数据加载中..."
|
||||
style="width: 100%"
|
||||
:show-overflow-tooltip="true"
|
||||
>
|
||||
<el-table-column label="ID" prop="id" min-width="80" width="100">
|
||||
<template #default="scope">
|
||||
{{ scope.row.id || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="中转仓名称" prop="warehouseName" min-width="120" width="150">
|
||||
<template #default="scope">
|
||||
{{ scope.row.warehouseName || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="中转仓编码" prop="warehouseCode" min-width="120" width="150">
|
||||
<template #default="scope">
|
||||
{{ scope.row.warehouseCode || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="地址" prop="address" min-width="200" width="250">
|
||||
<template #default="scope">
|
||||
{{ scope.row.address || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="容量(头)" prop="capacity" min-width="100" width="120">
|
||||
<template #default="scope">
|
||||
{{ scope.row.capacity || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="负责人" prop="managerName" min-width="100" width="120">
|
||||
<template #default="scope">
|
||||
{{ scope.row.managerName || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="联系电话" prop="managerMobile" min-width="120" width="150">
|
||||
<template #default="scope">
|
||||
{{ scope.row.managerMobile || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" prop="status" min-width="80" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
|
||||
{{ scope.row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" prop="createTime" min-width="160" width="180">
|
||||
<template #default="scope">
|
||||
{{ scope.row.createTime || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="200" width="250" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" v-hasPermi="['warehouse:edit']" @click="showEditDialog(scope.row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button link type="primary" v-hasPermi="['warehouse:query']" @click="showDetailDialog(scope.row)">
|
||||
详情
|
||||
</el-button>
|
||||
<el-button link type="danger" v-hasPermi="['warehouse:delete']" @click="del(scope.row.id)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<div class="dataListOnEmpty">暂无数据</div>
|
||||
</template>
|
||||
</el-table>
|
||||
<Pagination
|
||||
v-if="data.total > 0"
|
||||
v-model:limit="form.pageSize"
|
||||
v-model:page="form.pageNum"
|
||||
:total="data.total"
|
||||
@pagination="handlePagination"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<warehouseDialog ref="dialogRef" @success="getDataList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import baseSearch from '@/components/common/searchCustom/index.vue';
|
||||
import Pagination from '@/components/Pagination/index.vue';
|
||||
import warehouseDialog from './warehouseDialog.vue';
|
||||
import { warehouseList, warehouseDel } from '@/api/warehouse.js';
|
||||
|
||||
const baseSearchRef = ref();
|
||||
const dialogRef = ref();
|
||||
const data = reactive({
|
||||
total: 0,
|
||||
dataListLoading: false,
|
||||
tableKey: 0,
|
||||
});
|
||||
const rows = ref([]);
|
||||
const form = reactive({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const formItemList = reactive([
|
||||
{
|
||||
label: '中转仓名称',
|
||||
prop: 'warehouseName',
|
||||
type: 'input',
|
||||
placeholder: '请输入中转仓名称',
|
||||
span: 7,
|
||||
labelWidth: 100,
|
||||
},
|
||||
{
|
||||
label: '中转仓编码',
|
||||
prop: 'warehouseCode',
|
||||
type: 'input',
|
||||
placeholder: '请输入中转仓编码',
|
||||
span: 7,
|
||||
labelWidth: 100,
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
selectOptions: [
|
||||
{ value: 1, text: '启用' },
|
||||
{ value: 0, text: '禁用' },
|
||||
],
|
||||
span: 7,
|
||||
labelWidth: 80,
|
||||
},
|
||||
]);
|
||||
|
||||
const searchFrom = () => {
|
||||
form.pageNum = 1;
|
||||
getDataList();
|
||||
};
|
||||
|
||||
const handlePagination = (paginationData) => {
|
||||
if (paginationData) {
|
||||
form.pageNum = paginationData.page || form.pageNum;
|
||||
form.pageSize = paginationData.limit || form.pageSize;
|
||||
}
|
||||
getDataList();
|
||||
};
|
||||
|
||||
const getDataList = () => {
|
||||
data.dataListLoading = true;
|
||||
const searchParams = baseSearchRef.value.penetrateParams();
|
||||
|
||||
const params = {
|
||||
...form,
|
||||
...searchParams,
|
||||
};
|
||||
|
||||
warehouseList(params)
|
||||
.then((res) => {
|
||||
let responseData = res.data || res;
|
||||
|
||||
if (responseData && typeof responseData === 'object' && 'total' in responseData && 'rows' in responseData) {
|
||||
rows.value = responseData.rows || [];
|
||||
data.total = responseData.total || 0;
|
||||
} else if (responseData && responseData.data) {
|
||||
rows.value = responseData.data.rows || [];
|
||||
data.total = responseData.data.total || 0;
|
||||
} else {
|
||||
rows.value = [];
|
||||
data.total = 0;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('查询失败:', error);
|
||||
ElMessage.error('查询失败:' + (error.message || '未知错误'));
|
||||
rows.value = [];
|
||||
data.total = 0;
|
||||
})
|
||||
.finally(() => {
|
||||
data.dataListLoading = false;
|
||||
});
|
||||
};
|
||||
|
||||
const showAddDialog = () => {
|
||||
if (dialogRef.value) {
|
||||
dialogRef.value.onShowDialog(null);
|
||||
}
|
||||
};
|
||||
|
||||
const showEditDialog = (row) => {
|
||||
if (dialogRef.value) {
|
||||
dialogRef.value.onShowDialog(row);
|
||||
}
|
||||
};
|
||||
|
||||
const showDetailDialog = (row) => {
|
||||
if (dialogRef.value) {
|
||||
dialogRef.value.onShowDialog(row, true);
|
||||
}
|
||||
};
|
||||
|
||||
const del = (id) => {
|
||||
ElMessageBox.confirm('确定要删除这条中转仓记录吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
warehouseDel(id)
|
||||
.then((res) => {
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除成功');
|
||||
getDataList();
|
||||
} else {
|
||||
ElMessage.error(res.msg || '删除失败');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('删除失败:', error);
|
||||
ElMessage.error('删除失败:' + (error.message || '未知错误'));
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// 用户取消删除
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getDataList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.operation-scroll-bar {
|
||||
background: #fff;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.operation-scroll-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.dataListOnEmpty {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
|
||||
383
pc-cattle-transportation/src/views/warehouse/warehouseDialog.vue
Normal file
383
pc-cattle-transportation/src/views/warehouse/warehouseDialog.vue
Normal file
@@ -0,0 +1,383 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="data.dialogVisible"
|
||||
:title="data.isDetail ? '中转仓详情' : (data.editId ? '编辑中转仓' : '新增中转仓')"
|
||||
:before-close="handleClose"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form ref="formDataRef" :model="ruleForm" :rules="rules" label-width="120px" :disabled="data.isDetail">
|
||||
<el-form-item label="中转仓名称" prop="warehouseName">
|
||||
<el-input v-model="ruleForm.warehouseName" placeholder="请输入中转仓名称" maxlength="100" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="中转仓编码" prop="warehouseCode">
|
||||
<el-input
|
||||
v-model="ruleForm.warehouseCode"
|
||||
placeholder="请输入中转仓编码(如不填写将自动生成)"
|
||||
maxlength="50"
|
||||
:disabled="!!data.editId"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="地址" prop="address">
|
||||
<el-input
|
||||
v-model="ruleForm.address"
|
||||
placeholder="请输入地址"
|
||||
maxlength="255"
|
||||
style="width: calc(100% - 100px); margin-right: 10px;"
|
||||
/>
|
||||
<el-button type="primary" @click="openLocationMap">选择位置</el-button>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="经度">
|
||||
<el-input v-model="ruleForm.longitude" placeholder="经度" disabled />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="纬度">
|
||||
<el-input v-model="ruleForm.latitude" placeholder="纬度" disabled />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="容量(头)" prop="capacity">
|
||||
<el-input-number
|
||||
v-model="ruleForm.capacity"
|
||||
:min="1"
|
||||
:max="999999"
|
||||
placeholder="请输入容量"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="负责人姓名">
|
||||
<el-input v-model="ruleForm.managerName" placeholder="请输入负责人姓名" maxlength="50" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="联系电话">
|
||||
<el-input v-model="ruleForm.managerMobile" placeholder="请输入联系电话" maxlength="20" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="ruleForm.status">
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
<el-radio :label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="ruleForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button v-if="!data.isDetail" :loading="data.saveLoading" type="primary" @click="onClickSave">
|
||||
保存
|
||||
</el-button>
|
||||
<el-button @click="handleClose">{{ data.isDetail ? '关闭' : '取消' }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 地址选择地图 -->
|
||||
<el-dialog v-model="showLocationMap" title="选择地址" width="900px">
|
||||
<baidu-map
|
||||
class="map"
|
||||
:center="ruleForm.longitude && ruleForm.latitude ? {lng: parseFloat(ruleForm.longitude), lat: parseFloat(ruleForm.latitude)} : {lng: 116.404, lat: 39.915}"
|
||||
:zoom="15"
|
||||
:scroll-wheel-zoom="true"
|
||||
@click="handleLocationClick"
|
||||
style="height: 500px"
|
||||
>
|
||||
<bm-marker
|
||||
v-if="ruleForm.longitude && ruleForm.latitude"
|
||||
:position="{lng: parseFloat(ruleForm.longitude), lat: parseFloat(ruleForm.latitude)}"
|
||||
:dragging="true"
|
||||
@dragging="handleMarkerDrag"
|
||||
/>
|
||||
<bm-map-type :map-types="['BMAP_NORMAL_MAP', 'BMAP_HYBRID_MAP']"></bm-map-type>
|
||||
</baidu-map>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showLocationMap = false">取消</el-button>
|
||||
<el-button type="primary" @click="showLocationMap = false">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { BaiduMap, BmMapType, BmMarker } from 'vue-baidu-map-3x';
|
||||
import { warehouseAdd, warehouseEdit, warehouseDetail } from '@/api/warehouse.js';
|
||||
|
||||
const emits = defineEmits(['success']);
|
||||
const formDataRef = ref(null);
|
||||
const showLocationMap = ref(false);
|
||||
|
||||
const data = reactive({
|
||||
dialogVisible: false,
|
||||
saveLoading: false,
|
||||
editId: null,
|
||||
isDetail: false,
|
||||
});
|
||||
|
||||
const ruleForm = reactive({
|
||||
id: null,
|
||||
warehouseName: '',
|
||||
warehouseCode: '',
|
||||
address: '',
|
||||
longitude: '',
|
||||
latitude: '',
|
||||
capacity: null,
|
||||
managerName: '',
|
||||
managerMobile: '',
|
||||
status: 1,
|
||||
remark: '',
|
||||
});
|
||||
|
||||
const rules = reactive({
|
||||
warehouseName: [
|
||||
{ required: true, message: '请输入中转仓名称', trigger: 'blur' },
|
||||
],
|
||||
address: [
|
||||
{ required: true, message: '请输入地址', trigger: 'blur' },
|
||||
],
|
||||
capacity: [
|
||||
{ required: true, message: '请输入容量', trigger: 'blur' },
|
||||
{ type: 'number', min: 1, message: '容量必须大于0', trigger: 'blur' },
|
||||
],
|
||||
status: [
|
||||
{ required: true, message: '请选择状态', trigger: 'change' },
|
||||
],
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
if (formDataRef.value) {
|
||||
formDataRef.value.resetFields();
|
||||
}
|
||||
// 重置表单数据
|
||||
Object.assign(ruleForm, {
|
||||
id: null,
|
||||
warehouseName: '',
|
||||
warehouseCode: '',
|
||||
address: '',
|
||||
longitude: '',
|
||||
latitude: '',
|
||||
capacity: null,
|
||||
managerName: '',
|
||||
managerMobile: '',
|
||||
status: 1,
|
||||
remark: '',
|
||||
});
|
||||
data.editId = null;
|
||||
data.isDetail = false;
|
||||
data.dialogVisible = false;
|
||||
};
|
||||
|
||||
const onClickSave = () => {
|
||||
if (!formDataRef.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
formDataRef.value.validate((valid) => {
|
||||
if (!valid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
data.saveLoading = true;
|
||||
|
||||
const params = {
|
||||
warehouseName: ruleForm.warehouseName,
|
||||
warehouseCode: ruleForm.warehouseCode,
|
||||
address: ruleForm.address,
|
||||
longitude: ruleForm.longitude,
|
||||
latitude: ruleForm.latitude,
|
||||
capacity: ruleForm.capacity,
|
||||
managerName: ruleForm.managerName,
|
||||
managerMobile: ruleForm.managerMobile,
|
||||
status: ruleForm.status,
|
||||
remark: ruleForm.remark,
|
||||
};
|
||||
|
||||
if (data.editId) {
|
||||
// 编辑
|
||||
params.id = data.editId;
|
||||
warehouseEdit(params)
|
||||
.then((res) => {
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('编辑成功');
|
||||
handleClose();
|
||||
emits('success');
|
||||
} else {
|
||||
ElMessage.error(res.msg || '编辑失败');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
ElMessage.error('编辑失败:' + (error.message || '未知错误'));
|
||||
})
|
||||
.finally(() => {
|
||||
data.saveLoading = false;
|
||||
});
|
||||
} else {
|
||||
// 新增
|
||||
warehouseAdd(params)
|
||||
.then((res) => {
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('新增成功');
|
||||
handleClose();
|
||||
emits('success');
|
||||
} else {
|
||||
ElMessage.error(res.msg || '新增失败');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
ElMessage.error('新增失败:' + (error.message || '未知错误'));
|
||||
})
|
||||
.finally(() => {
|
||||
data.saveLoading = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onShowDialog = (row, isDetail = false) => {
|
||||
data.isDetail = isDetail || false;
|
||||
data.editId = null;
|
||||
|
||||
if (row) {
|
||||
data.editId = row.id;
|
||||
// 如果是详情模式,先获取详情数据
|
||||
if (isDetail) {
|
||||
warehouseDetail(row.id)
|
||||
.then((res) => {
|
||||
if (res.code === 200 && res.data) {
|
||||
const detailData = res.data;
|
||||
Object.assign(ruleForm, {
|
||||
id: detailData.id,
|
||||
warehouseName: detailData.warehouseName || '',
|
||||
warehouseCode: detailData.warehouseCode || '',
|
||||
address: detailData.address || '',
|
||||
longitude: detailData.longitude || '',
|
||||
latitude: detailData.latitude || '',
|
||||
capacity: detailData.capacity,
|
||||
managerName: detailData.managerName || '',
|
||||
managerMobile: detailData.managerMobile || '',
|
||||
status: detailData.status !== undefined ? detailData.status : 1,
|
||||
remark: detailData.remark || '',
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
ElMessage.error('获取详情失败:' + (error.message || '未知错误'));
|
||||
});
|
||||
} else {
|
||||
// 编辑模式,直接使用传入的数据
|
||||
Object.assign(ruleForm, {
|
||||
id: row.id,
|
||||
warehouseName: row.warehouseName || '',
|
||||
warehouseCode: row.warehouseCode || '',
|
||||
address: row.address || '',
|
||||
longitude: row.longitude || '',
|
||||
latitude: row.latitude || '',
|
||||
capacity: row.capacity,
|
||||
managerName: row.managerName || '',
|
||||
managerMobile: row.managerMobile || '',
|
||||
status: row.status !== undefined ? row.status : 1,
|
||||
remark: row.remark || '',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 新增模式,重置表单
|
||||
Object.assign(ruleForm, {
|
||||
id: null,
|
||||
warehouseName: '',
|
||||
warehouseCode: '',
|
||||
address: '',
|
||||
longitude: '',
|
||||
latitude: '',
|
||||
capacity: null,
|
||||
managerName: '',
|
||||
managerMobile: '',
|
||||
status: 1,
|
||||
remark: '',
|
||||
});
|
||||
}
|
||||
|
||||
data.dialogVisible = true;
|
||||
};
|
||||
|
||||
// 打开地图选择地址
|
||||
const openLocationMap = () => {
|
||||
// 如果输入框有地址,先进行地理编码
|
||||
if (ruleForm.address && ruleForm.address.trim()) {
|
||||
showLocationMap.value = true;
|
||||
setTimeout(() => {
|
||||
if (window.BMap && window.BMap.Geocoder) {
|
||||
const geocoder = new window.BMap.Geocoder();
|
||||
geocoder.getPoint(ruleForm.address, (point) => {
|
||||
if (point) {
|
||||
ruleForm.longitude = point.lng;
|
||||
ruleForm.latitude = point.lat;
|
||||
ElMessage.success('已定位到该地址');
|
||||
} else {
|
||||
ElMessage.warning('未找到该地址,请在地图上手动选择');
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 500);
|
||||
} else {
|
||||
showLocationMap.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
// 地图点击事件
|
||||
const handleLocationClick = (e) => {
|
||||
ruleForm.longitude = e.point.lng;
|
||||
ruleForm.latitude = e.point.lat;
|
||||
// 反向地理编码获取地址
|
||||
if (window.BMap && window.BMap.Geocoder) {
|
||||
const geocoder = new window.BMap.Geocoder();
|
||||
geocoder.getLocation(e.point, (res) => {
|
||||
if (res) {
|
||||
ruleForm.address = res.address;
|
||||
ElMessage.success('已设置地址');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 标记拖拽事件
|
||||
const handleMarkerDrag = (e) => {
|
||||
ruleForm.longitude = e.point.lng;
|
||||
ruleForm.latitude = e.point.lat;
|
||||
if (window.BMap && window.BMap.Geocoder) {
|
||||
const geocoder = new window.BMap.Geocoder();
|
||||
geocoder.getLocation(e.point, (res) => {
|
||||
if (res) {
|
||||
ruleForm.address = res.address;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 暴露方法给父组件调用
|
||||
defineExpose({
|
||||
onShowDialog,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.map {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
}
|
||||
</style>
|
||||
|
||||
320
pc-cattle-transportation/src/views/warehouse/warehouseIn.vue
Normal file
320
pc-cattle-transportation/src/views/warehouse/warehouseIn.vue
Normal file
@@ -0,0 +1,320 @@
|
||||
<template>
|
||||
<div>
|
||||
<base-search :formItemList="formItemList" @search="searchFrom" ref="baseSearchRef"></base-search>
|
||||
|
||||
<!-- 横向滚动操作栏 -->
|
||||
<div class="operation-scroll-bar">
|
||||
<div class="operation-scroll-container">
|
||||
<el-button
|
||||
type="primary"
|
||||
v-hasPermi="['warehousein:add']"
|
||||
@click="showAddDialog"
|
||||
style="margin-left: 10px"
|
||||
>
|
||||
新增进仓记录
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-container">
|
||||
<el-table
|
||||
:data="rows"
|
||||
border
|
||||
v-loading="data.dataListLoading"
|
||||
element-loading-text="数据加载中..."
|
||||
style="width: 100%"
|
||||
:show-overflow-tooltip="true"
|
||||
>
|
||||
<el-table-column label="进仓单号" prop="inNumber" min-width="150" width="180">
|
||||
<template #default="scope">
|
||||
{{ scope.row.inNumber || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="中转仓" prop="warehouseName" min-width="120" width="150">
|
||||
<template #default="scope">
|
||||
{{ scope.row.warehouseName || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="运单号" prop="deliveryNumber" min-width="120" width="150">
|
||||
<template #default="scope">
|
||||
{{ scope.row.deliveryNumber || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="车牌号" prop="licensePlate" min-width="100" width="120">
|
||||
<template #default="scope">
|
||||
{{ scope.row.licensePlate || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="司机姓名" prop="driverName" min-width="100" width="120">
|
||||
<template #default="scope">
|
||||
{{ scope.row.driverName || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="司机手机号" prop="driverMobile" min-width="120" width="150">
|
||||
<template #default="scope">
|
||||
{{ scope.row.driverMobile || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="来源地" prop="sourceLocation" min-width="150" width="200">
|
||||
<template #default="scope">
|
||||
{{ scope.row.sourceLocation || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="牛只数量(头)" prop="cattleCount" min-width="100" width="120">
|
||||
<template #default="scope">
|
||||
{{ scope.row.cattleCount || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="重量(公斤)" prop="weight" min-width="100" width="120">
|
||||
<template #default="scope">
|
||||
{{ scope.row.weight || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="进仓时间" prop="inTime" min-width="160" width="180">
|
||||
<template #default="scope">
|
||||
{{ scope.row.inTime || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" prop="status" min-width="80" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusType(scope.row.status)">
|
||||
{{ scope.row.statusDesc || '--' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" prop="createTime" min-width="160" width="180">
|
||||
<template #default="scope">
|
||||
{{ scope.row.createTime || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="200" width="250" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" v-hasPermi="['warehousein:edit']" @click="showEditDialog(scope.row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button link type="primary" v-hasPermi="['warehousein:query']" @click="showDetailDialog(scope.row)">
|
||||
详情
|
||||
</el-button>
|
||||
<el-button link type="danger" v-hasPermi="['warehousein:delete']" @click="del(scope.row.id)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<div class="dataListOnEmpty">暂无数据</div>
|
||||
</template>
|
||||
</el-table>
|
||||
<Pagination
|
||||
v-if="data.total > 0"
|
||||
v-model:limit="form.pageSize"
|
||||
v-model:page="form.pageNum"
|
||||
:total="data.total"
|
||||
@pagination="handlePagination"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<warehouseInDialog ref="dialogRef" @success="getDataList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import baseSearch from '@/components/common/searchCustom/index.vue';
|
||||
import Pagination from '@/components/Pagination/index.vue';
|
||||
import warehouseInDialog from './warehouseInDialog.vue';
|
||||
import { warehouseInList, warehouseInDel } from '@/api/warehouseIn.js';
|
||||
|
||||
const baseSearchRef = ref();
|
||||
const dialogRef = ref();
|
||||
const data = reactive({
|
||||
total: 0,
|
||||
dataListLoading: false,
|
||||
});
|
||||
const rows = ref([]);
|
||||
const form = reactive({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const formItemList = reactive([
|
||||
{
|
||||
label: '进仓单号',
|
||||
prop: 'inNumber',
|
||||
type: 'input',
|
||||
placeholder: '请输入进仓单号',
|
||||
span: 7,
|
||||
labelWidth: 100,
|
||||
},
|
||||
{
|
||||
label: '中转仓',
|
||||
prop: 'warehouseId',
|
||||
type: 'select',
|
||||
selectOptions: [],
|
||||
span: 7,
|
||||
labelWidth: 80,
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
selectOptions: [
|
||||
{ value: 1, text: '待进仓' },
|
||||
{ value: 2, text: '已进仓' },
|
||||
{ value: 3, text: '已出仓' },
|
||||
],
|
||||
span: 7,
|
||||
labelWidth: 80,
|
||||
},
|
||||
{
|
||||
label: '进仓时间',
|
||||
prop: 'inTime',
|
||||
type: 'daterange',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间',
|
||||
span: 7,
|
||||
labelWidth: 100,
|
||||
},
|
||||
]);
|
||||
|
||||
const searchFrom = () => {
|
||||
form.pageNum = 1;
|
||||
getDataList();
|
||||
};
|
||||
|
||||
const handlePagination = (paginationData) => {
|
||||
if (paginationData) {
|
||||
form.pageNum = paginationData.page || form.pageNum;
|
||||
form.pageSize = paginationData.limit || form.pageSize;
|
||||
}
|
||||
getDataList();
|
||||
};
|
||||
|
||||
const getDataList = () => {
|
||||
data.dataListLoading = true;
|
||||
const searchParams = baseSearchRef.value.penetrateParams();
|
||||
|
||||
const params = {
|
||||
...form,
|
||||
...searchParams,
|
||||
};
|
||||
|
||||
// 处理日期范围参数
|
||||
if (searchParams.inTime && Array.isArray(searchParams.inTime) && searchParams.inTime.length === 2) {
|
||||
params.startTime = searchParams.inTime[0];
|
||||
params.endTime = searchParams.inTime[1];
|
||||
delete params.inTime;
|
||||
}
|
||||
|
||||
warehouseInList(params)
|
||||
.then((res) => {
|
||||
let responseData = res.data || res;
|
||||
|
||||
if (responseData && typeof responseData === 'object' && 'total' in responseData && 'rows' in responseData) {
|
||||
rows.value = responseData.rows || [];
|
||||
data.total = responseData.total || 0;
|
||||
} else if (responseData && responseData.data) {
|
||||
rows.value = responseData.data.rows || [];
|
||||
data.total = responseData.data.total || 0;
|
||||
} else {
|
||||
rows.value = [];
|
||||
data.total = 0;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('查询失败:', error);
|
||||
ElMessage.error('查询失败:' + (error.message || '未知错误'));
|
||||
rows.value = [];
|
||||
data.total = 0;
|
||||
})
|
||||
.finally(() => {
|
||||
data.dataListLoading = false;
|
||||
});
|
||||
};
|
||||
|
||||
const showAddDialog = () => {
|
||||
if (dialogRef.value) {
|
||||
dialogRef.value.onShowDialog(null);
|
||||
}
|
||||
};
|
||||
|
||||
const showEditDialog = (row) => {
|
||||
if (dialogRef.value) {
|
||||
dialogRef.value.onShowDialog(row);
|
||||
}
|
||||
};
|
||||
|
||||
const showDetailDialog = (row) => {
|
||||
if (dialogRef.value) {
|
||||
dialogRef.value.onShowDialog(row, true);
|
||||
}
|
||||
};
|
||||
|
||||
const del = (id) => {
|
||||
ElMessageBox.confirm('确定要删除这条进仓记录吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
warehouseInDel(id)
|
||||
.then((res) => {
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除成功');
|
||||
getDataList();
|
||||
} else {
|
||||
ElMessage.error(res.msg || '删除失败');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('删除失败:', error);
|
||||
ElMessage.error('删除失败:' + (error.message || '未知错误'));
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// 用户取消删除
|
||||
});
|
||||
};
|
||||
|
||||
const getStatusType = (status) => {
|
||||
const typeMap = {
|
||||
1: 'info', // 待进仓 - 灰色
|
||||
2: 'success', // 已进仓 - 绿色
|
||||
3: 'warning', // 已出仓 - 橙色
|
||||
};
|
||||
return typeMap[status] || 'info';
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getDataList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.operation-scroll-bar {
|
||||
background: #fff;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.operation-scroll-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.dataListOnEmpty {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,975 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="data.dialogVisible"
|
||||
:title="data.isDetail ? '进仓详情' : (data.editId ? '编辑进仓记录' : '新增进仓记录')"
|
||||
:before-close="handleClose"
|
||||
width="900px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form ref="formDataRef" :model="ruleForm" :rules="rules" label-width="120px" :disabled="data.isDetail">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="中转仓" prop="warehouseId">
|
||||
<el-select
|
||||
v-model="ruleForm.warehouseId"
|
||||
placeholder="请选择中转仓"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 100%"
|
||||
@change="handleWarehouseChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in warehouseList"
|
||||
:key="item.id"
|
||||
:label="item.warehouseName"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="运送清单" prop="deliveryId">
|
||||
<el-select
|
||||
v-model="ruleForm.deliveryId"
|
||||
placeholder="请选择运送清单"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 100%"
|
||||
@change="handleDeliveryChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in deliveryList"
|
||||
:key="item.id"
|
||||
:label="`${item.deliveryNumber || '--'} - ${item.licensePlate || '--'}`"
|
||||
: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="orderId">
|
||||
<el-select
|
||||
v-model="ruleForm.orderId"
|
||||
placeholder="请选择订单(可多选)"
|
||||
multiple
|
||||
clearable
|
||||
filterable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in orderList"
|
||||
:key="item.id"
|
||||
:label="`订单${item.id} - 单价: ${item.firmPrice || '--'}元/斤`"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="进仓时间" prop="inTime">
|
||||
<el-date-picker
|
||||
v-model="ruleForm.inTime"
|
||||
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="24">
|
||||
<el-form-item label="来源地" prop="sourceLocation">
|
||||
<el-input
|
||||
v-model="ruleForm.sourceLocation"
|
||||
placeholder="请输入来源地"
|
||||
maxlength="255"
|
||||
style="width: calc(100% - 100px); margin-right: 10px;"
|
||||
/>
|
||||
<el-button type="primary" @click="openSourceLocationMap">选择位置</el-button>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="来源地经度">
|
||||
<el-input v-model="ruleForm.sourceLon" placeholder="经度" disabled />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="来源地纬度">
|
||||
<el-input v-model="ruleForm.sourceLat" placeholder="纬度" disabled />
|
||||
</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="ruleForm.cattleCount"
|
||||
:min="1"
|
||||
:max="9999"
|
||||
placeholder="请输入牛只数量"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="重量(公斤)" prop="weight">
|
||||
<el-input-number
|
||||
v-model="ruleForm.weight"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
placeholder="请输入重量"
|
||||
style="width: 100%"
|
||||
>
|
||||
<template #append>kg</template>
|
||||
</el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="ruleForm.status" placeholder="请选择状态" style="width: 100%">
|
||||
<el-option label="待进仓" :value="1" />
|
||||
<el-option label="已进仓" :value="2" />
|
||||
<el-option label="已出仓" :value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="照片">
|
||||
<div style="display: flex; flex-direction: column; gap: 10px;">
|
||||
<!-- 拖拽上传区域 -->
|
||||
<el-upload
|
||||
drag
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
:before-upload="beforePhotoUpload"
|
||||
:limit="9"
|
||||
accept="image/*"
|
||||
:on-change="handlePhotoChange"
|
||||
:show-file-list="false"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">将图片文件拖到此处,或<em>点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">支持 jpg/png/gif 格式,单个文件不超过 10MB,最多上传 9 张</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<!-- 图片预览列表 -->
|
||||
<div v-if="photoFileList.length > 0" class="photo-preview-list">
|
||||
<div
|
||||
v-for="(file, index) in photoFileList"
|
||||
:key="index"
|
||||
class="photo-preview-item"
|
||||
>
|
||||
<el-image
|
||||
:src="file.url || file.response?.data?.url || ''"
|
||||
fit="cover"
|
||||
class="photo-preview-image"
|
||||
:preview-src-list="photoFileList.map(f => f.url || f.response?.data?.url || '').filter(Boolean)"
|
||||
:initial-index="index"
|
||||
/>
|
||||
<el-button
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
circle
|
||||
size="small"
|
||||
class="photo-delete-btn"
|
||||
@click="handlePhotoRemove(file)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="视频">
|
||||
<el-upload
|
||||
drag
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
:on-change="handleVideoChange"
|
||||
:on-remove="handleVideoRemove"
|
||||
:before-upload="beforeVideoUpload"
|
||||
:limit="3"
|
||||
accept="video/*"
|
||||
:show-file-list="false"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">将视频文件拖到此处,或<em>点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">支持 mp4/avi/rmvb/mkv 格式,单个文件不超过 100MB,最多上传 3 个</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<!-- 视频预览列表 -->
|
||||
<div v-if="videoFileList.length > 0" class="video-preview-list">
|
||||
<div
|
||||
v-for="(file, index) in videoFileList"
|
||||
:key="index"
|
||||
class="video-preview-item"
|
||||
>
|
||||
<video
|
||||
:src="file.url || file.response?.data?.url || ''"
|
||||
controls
|
||||
class="video-preview-player"
|
||||
></video>
|
||||
<div class="video-name">{{ file.name || '视频' }}</div>
|
||||
<el-button
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
circle
|
||||
size="small"
|
||||
class="video-delete-btn"
|
||||
@click="handleVideoRemove(file)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="ruleForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button v-if="!data.isDetail" :loading="data.saveLoading" type="primary" @click="onClickSave">
|
||||
保存
|
||||
</el-button>
|
||||
<el-button @click="handleClose">{{ data.isDetail ? '关闭' : '取消' }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 来源地地址选择地图 -->
|
||||
<el-dialog v-model="showSourceLocationMap" title="选择来源地地址" width="900px">
|
||||
<baidu-map
|
||||
class="map"
|
||||
:center="ruleForm.sourceLon && ruleForm.sourceLat ? {lng: parseFloat(ruleForm.sourceLon), lat: parseFloat(ruleForm.sourceLat)} : {lng: 116.404, lat: 39.915}"
|
||||
:zoom="15"
|
||||
:scroll-wheel-zoom="true"
|
||||
@click="handleSourceLocationClick"
|
||||
style="height: 500px"
|
||||
>
|
||||
<bm-marker
|
||||
v-if="ruleForm.sourceLon && ruleForm.sourceLat"
|
||||
:position="{lng: parseFloat(ruleForm.sourceLon), lat: parseFloat(ruleForm.sourceLat)}"
|
||||
:dragging="true"
|
||||
@dragging="handleSourceMarkerDrag"
|
||||
/>
|
||||
<bm-map-type :map-types="['BMAP_NORMAL_MAP', 'BMAP_HYBRID_MAP']"></bm-map-type>
|
||||
</baidu-map>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showSourceLocationMap = false">取消</el-button>
|
||||
<el-button type="primary" @click="showSourceLocationMap = false">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 图片预览 -->
|
||||
<el-dialog v-model="showImageViewer" title="图片预览" width="800px">
|
||||
<el-image :src="imageViewerUrl" style="width: 100%" fit="contain" />
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Plus, UploadFilled, Delete } from '@element-plus/icons-vue';
|
||||
import { BaiduMap, BmMapType, BmMarker } from 'vue-baidu-map-3x';
|
||||
import { warehouseInAdd, warehouseInEdit, warehouseInDetail } from '@/api/warehouseIn.js';
|
||||
import { warehouseAll } from '@/api/warehouse.js';
|
||||
import { orderPageQuery, shippingList } from '@/api/shipping.js';
|
||||
|
||||
const emits = defineEmits(['success']);
|
||||
const formDataRef = ref(null);
|
||||
const showSourceLocationMap = ref(false);
|
||||
const showImageViewer = ref(false);
|
||||
const imageViewerUrl = ref('');
|
||||
const photoFileList = ref([]);
|
||||
const videoFileList = ref([]);
|
||||
|
||||
const warehouseList = ref([]);
|
||||
const orderList = ref([]);
|
||||
const deliveryList = ref([]);
|
||||
|
||||
const data = reactive({
|
||||
dialogVisible: false,
|
||||
saveLoading: false,
|
||||
editId: null,
|
||||
isDetail: false,
|
||||
});
|
||||
|
||||
const ruleForm = reactive({
|
||||
id: null,
|
||||
warehouseId: null,
|
||||
orderId: [],
|
||||
deliveryId: null,
|
||||
sourceLocation: '',
|
||||
sourceLon: '',
|
||||
sourceLat: '',
|
||||
cattleCount: null,
|
||||
weight: null,
|
||||
inTime: '',
|
||||
photos: '',
|
||||
videos: '',
|
||||
remark: '',
|
||||
status: 1,
|
||||
});
|
||||
|
||||
const rules = reactive({
|
||||
warehouseId: [
|
||||
{ required: true, message: '请选择中转仓', trigger: 'change' },
|
||||
],
|
||||
deliveryId: [
|
||||
{ required: true, message: '请选择运送清单', trigger: 'change' },
|
||||
],
|
||||
cattleCount: [
|
||||
{ required: true, message: '请输入牛只数量', trigger: 'blur' },
|
||||
{ type: 'number', min: 1, message: '牛只数量必须大于0', trigger: 'blur' },
|
||||
],
|
||||
inTime: [
|
||||
{ required: true, message: '请选择进仓时间', trigger: 'change' },
|
||||
],
|
||||
});
|
||||
|
||||
// 加载中转仓列表
|
||||
const loadWarehouseList = async () => {
|
||||
try {
|
||||
const res = await warehouseAll();
|
||||
if (res.code === 200) {
|
||||
warehouseList.value = res.data || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载中转仓列表失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载订单列表
|
||||
const loadOrderList = async () => {
|
||||
try {
|
||||
const res = await orderPageQuery({ pageNum: 1, pageSize: 1000 });
|
||||
if (res.code === 200) {
|
||||
const responseData = res.data || res;
|
||||
if (responseData && typeof responseData === 'object' && 'rows' in responseData) {
|
||||
orderList.value = responseData.rows || [];
|
||||
} else if (responseData && responseData.data && 'rows' in responseData.data) {
|
||||
orderList.value = responseData.data.rows || [];
|
||||
} else {
|
||||
orderList.value = [];
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载订单列表失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载运送清单列表
|
||||
const loadDeliveryList = async () => {
|
||||
try {
|
||||
const res = await shippingList({ pageNum: 1, pageSize: 1000 });
|
||||
if (res.code === 200) {
|
||||
const responseData = res.data || res;
|
||||
if (responseData && typeof responseData === 'object' && 'rows' in responseData) {
|
||||
deliveryList.value = responseData.rows || [];
|
||||
} else if (responseData && responseData.data && 'rows' in responseData.data) {
|
||||
deliveryList.value = responseData.data.rows || [];
|
||||
} else {
|
||||
deliveryList.value = [];
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载运送清单列表失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (formDataRef.value) {
|
||||
formDataRef.value.resetFields();
|
||||
}
|
||||
Object.assign(ruleForm, {
|
||||
id: null,
|
||||
warehouseId: null,
|
||||
orderId: [],
|
||||
deliveryId: null,
|
||||
sourceLocation: '',
|
||||
sourceLon: '',
|
||||
sourceLat: '',
|
||||
cattleCount: null,
|
||||
weight: null,
|
||||
inTime: '',
|
||||
photos: '',
|
||||
videos: '',
|
||||
remark: '',
|
||||
status: 1,
|
||||
});
|
||||
photoFileList.value = [];
|
||||
videoFileList.value = [];
|
||||
data.editId = null;
|
||||
data.isDetail = false;
|
||||
data.dialogVisible = false;
|
||||
};
|
||||
|
||||
const onClickSave = () => {
|
||||
if (!formDataRef.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
formDataRef.value.validate((valid) => {
|
||||
if (!valid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
data.saveLoading = true;
|
||||
|
||||
// 处理订单ID(多个订单用逗号分隔)
|
||||
const orderIdStr = ruleForm.orderId && ruleForm.orderId.length > 0
|
||||
? ruleForm.orderId.join(',')
|
||||
: null;
|
||||
|
||||
// 处理照片和视频URL(多个用逗号分隔)
|
||||
const photosStr = photoFileList.value.map(file => file.url || file.response?.data?.url || '').filter(url => url).join(',');
|
||||
const videosStr = videoFileList.value.map(file => file.url || file.response?.data?.url || '').filter(url => url).join(',');
|
||||
|
||||
const params = {
|
||||
warehouseId: ruleForm.warehouseId,
|
||||
orderId: orderIdStr,
|
||||
deliveryId: ruleForm.deliveryId,
|
||||
sourceLocation: ruleForm.sourceLocation,
|
||||
sourceLon: ruleForm.sourceLon,
|
||||
sourceLat: ruleForm.sourceLat,
|
||||
cattleCount: ruleForm.cattleCount,
|
||||
weight: ruleForm.weight,
|
||||
inTime: ruleForm.inTime,
|
||||
photos: photosStr,
|
||||
videos: videosStr,
|
||||
remark: ruleForm.remark,
|
||||
};
|
||||
|
||||
if (data.editId) {
|
||||
// 编辑
|
||||
params.id = data.editId;
|
||||
params.status = ruleForm.status;
|
||||
warehouseInEdit(params)
|
||||
.then((res) => {
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('编辑成功');
|
||||
handleClose();
|
||||
emits('success');
|
||||
} else {
|
||||
ElMessage.error(res.msg || '编辑失败');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
ElMessage.error('编辑失败:' + (error.message || '未知错误'));
|
||||
})
|
||||
.finally(() => {
|
||||
data.saveLoading = false;
|
||||
});
|
||||
} else {
|
||||
// 新增
|
||||
warehouseInAdd(params)
|
||||
.then((res) => {
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('新增成功');
|
||||
handleClose();
|
||||
emits('success');
|
||||
} else {
|
||||
ElMessage.error(res.msg || '新增失败');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
ElMessage.error('新增失败:' + (error.message || '未知错误'));
|
||||
})
|
||||
.finally(() => {
|
||||
data.saveLoading = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onShowDialog = (row, isDetail = false) => {
|
||||
data.isDetail = isDetail || false;
|
||||
data.editId = null;
|
||||
|
||||
if (row) {
|
||||
data.editId = row.id;
|
||||
// 如果是详情模式,先获取详情数据
|
||||
if (isDetail) {
|
||||
warehouseInDetail(row.id)
|
||||
.then((res) => {
|
||||
if (res.code === 200 && res.data) {
|
||||
const detailData = res.data;
|
||||
Object.assign(ruleForm, {
|
||||
id: detailData.id,
|
||||
warehouseId: detailData.warehouseId,
|
||||
orderId: detailData.orderId ? detailData.orderId.split(',').map(id => parseInt(id)) : [],
|
||||
deliveryId: detailData.deliveryId,
|
||||
sourceLocation: detailData.sourceLocation || '',
|
||||
sourceLon: detailData.sourceLon || '',
|
||||
sourceLat: detailData.sourceLat || '',
|
||||
cattleCount: detailData.cattleCount,
|
||||
weight: detailData.weight,
|
||||
inTime: detailData.inTime || '',
|
||||
photos: detailData.photos || '',
|
||||
videos: detailData.videos || '',
|
||||
remark: detailData.remark || '',
|
||||
status: detailData.status !== undefined ? detailData.status : 1,
|
||||
});
|
||||
// 处理照片和视频文件列表
|
||||
if (detailData.photos) {
|
||||
photoFileList.value = detailData.photos.split(',').map(url => ({ url, name: 'photo' }));
|
||||
}
|
||||
if (detailData.videos) {
|
||||
videoFileList.value = detailData.videos.split(',').map(url => ({ url, name: 'video' }));
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
ElMessage.error('获取详情失败:' + (error.message || '未知错误'));
|
||||
});
|
||||
} else {
|
||||
// 编辑模式,直接使用传入的数据
|
||||
Object.assign(ruleForm, {
|
||||
id: row.id,
|
||||
warehouseId: row.warehouseId,
|
||||
orderId: row.orderId ? row.orderId.split(',').map(id => parseInt(id)) : [],
|
||||
deliveryId: row.deliveryId,
|
||||
sourceLocation: row.sourceLocation || '',
|
||||
sourceLon: row.sourceLon || '',
|
||||
sourceLat: row.sourceLat || '',
|
||||
cattleCount: row.cattleCount,
|
||||
weight: row.weight,
|
||||
inTime: row.inTime || '',
|
||||
photos: row.photos || '',
|
||||
videos: row.videos || '',
|
||||
remark: row.remark || '',
|
||||
status: row.status !== undefined ? row.status : 1,
|
||||
});
|
||||
// 处理照片和视频文件列表
|
||||
if (row.photos) {
|
||||
photoFileList.value = row.photos.split(',').map(url => ({ url, name: 'photo' }));
|
||||
}
|
||||
if (row.videos) {
|
||||
videoFileList.value = row.videos.split(',').map(url => ({ url, name: 'video' }));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 新增模式,重置表单
|
||||
Object.assign(ruleForm, {
|
||||
id: null,
|
||||
warehouseId: null,
|
||||
orderId: [],
|
||||
deliveryId: null,
|
||||
sourceLocation: '',
|
||||
sourceLon: '',
|
||||
sourceLat: '',
|
||||
cattleCount: null,
|
||||
weight: null,
|
||||
inTime: '',
|
||||
photos: '',
|
||||
videos: '',
|
||||
remark: '',
|
||||
status: 1,
|
||||
});
|
||||
photoFileList.value = [];
|
||||
videoFileList.value = [];
|
||||
}
|
||||
|
||||
data.dialogVisible = true;
|
||||
};
|
||||
|
||||
// 中转仓选择变化
|
||||
const handleWarehouseChange = (warehouseId) => {
|
||||
// 可以在这里添加逻辑
|
||||
};
|
||||
|
||||
// 运送清单选择变化
|
||||
const handleDeliveryChange = (deliveryId) => {
|
||||
if (!deliveryId) {
|
||||
// 清空相关字段
|
||||
ruleForm.sourceLocation = '';
|
||||
ruleForm.sourceLon = '';
|
||||
ruleForm.sourceLat = '';
|
||||
ruleForm.cattleCount = null;
|
||||
ruleForm.weight = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 从 deliveryList 中找到对应的运送清单
|
||||
const selectedDelivery = deliveryList.value.find(item => item.id === deliveryId);
|
||||
if (selectedDelivery) {
|
||||
// 自动填充来源地信息
|
||||
if (selectedDelivery.startLocation) {
|
||||
ruleForm.sourceLocation = selectedDelivery.startLocation;
|
||||
}
|
||||
if (selectedDelivery.startLon) {
|
||||
ruleForm.sourceLon = selectedDelivery.startLon;
|
||||
}
|
||||
if (selectedDelivery.startLat) {
|
||||
ruleForm.sourceLat = selectedDelivery.startLat;
|
||||
}
|
||||
|
||||
// 自动填充牛只数量
|
||||
if (selectedDelivery.ratedQuantity) {
|
||||
ruleForm.cattleCount = selectedDelivery.ratedQuantity;
|
||||
}
|
||||
|
||||
// 自动计算重量:entruckWeight - emptyWeight
|
||||
if (selectedDelivery.entruckWeight && selectedDelivery.emptyWeight) {
|
||||
const entruckWeight = parseFloat(selectedDelivery.entruckWeight) || 0;
|
||||
const emptyWeight = parseFloat(selectedDelivery.emptyWeight) || 0;
|
||||
const calculatedWeight = entruckWeight - emptyWeight;
|
||||
if (calculatedWeight > 0) {
|
||||
ruleForm.weight = parseFloat(calculatedWeight.toFixed(2));
|
||||
}
|
||||
} else if (selectedDelivery.entruckWeight) {
|
||||
// 如果只有装车重量,也可以填充
|
||||
ruleForm.weight = parseFloat(selectedDelivery.entruckWeight) || null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 打开来源地地图选择地址
|
||||
const openSourceLocationMap = () => {
|
||||
if (ruleForm.sourceLocation && ruleForm.sourceLocation.trim()) {
|
||||
showSourceLocationMap.value = true;
|
||||
setTimeout(() => {
|
||||
if (window.BMap && window.BMap.Geocoder) {
|
||||
const geocoder = new window.BMap.Geocoder();
|
||||
geocoder.getPoint(ruleForm.sourceLocation, (point) => {
|
||||
if (point) {
|
||||
ruleForm.sourceLon = point.lng;
|
||||
ruleForm.sourceLat = point.lat;
|
||||
ElMessage.success('已定位到该地址');
|
||||
} else {
|
||||
ElMessage.warning('未找到该地址,请在地图上手动选择');
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 500);
|
||||
} else {
|
||||
showSourceLocationMap.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
// 地图点击事件
|
||||
const handleSourceLocationClick = (e) => {
|
||||
ruleForm.sourceLon = e.point.lng;
|
||||
ruleForm.sourceLat = e.point.lat;
|
||||
if (window.BMap && window.BMap.Geocoder) {
|
||||
const geocoder = new window.BMap.Geocoder();
|
||||
geocoder.getLocation(e.point, (res) => {
|
||||
if (res) {
|
||||
ruleForm.sourceLocation = res.address;
|
||||
ElMessage.success('已设置来源地地址');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 标记拖拽事件
|
||||
const handleSourceMarkerDrag = (e) => {
|
||||
ruleForm.sourceLon = e.point.lng;
|
||||
ruleForm.sourceLat = e.point.lat;
|
||||
if (window.BMap && window.BMap.Geocoder) {
|
||||
const geocoder = new window.BMap.Geocoder();
|
||||
geocoder.getLocation(e.point, (res) => {
|
||||
if (res) {
|
||||
ruleForm.sourceLocation = res.address;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 图片预览(保留用于兼容)
|
||||
const handlePictureCardPreview = (file) => {
|
||||
imageViewerUrl.value = file.url || file.response?.data?.url || '';
|
||||
showImageViewer.value = true;
|
||||
};
|
||||
|
||||
// 照片文件变化处理(拖拽上传时触发)
|
||||
const handlePhotoChange = (file, fileList) => {
|
||||
// 验证文件
|
||||
if (!beforePhotoUpload(file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 手动上传文件
|
||||
uploadPhotoFile(file);
|
||||
};
|
||||
|
||||
// 手动上传照片文件
|
||||
const uploadPhotoFile = async (file) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file.raw || file);
|
||||
|
||||
try {
|
||||
// 获取 token
|
||||
let token = '';
|
||||
const userStore = localStorage.getItem('userStore');
|
||||
if (userStore) {
|
||||
const us = JSON.parse(userStore);
|
||||
token = us.token || '';
|
||||
}
|
||||
|
||||
const response = await fetch('/api/common/upload', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': token,
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.code === 200) {
|
||||
const photoUrl = result.data?.url || result.data;
|
||||
photoFileList.value.push({
|
||||
url: photoUrl,
|
||||
name: file.name,
|
||||
uid: file.uid || Date.now(),
|
||||
});
|
||||
ElMessage.success('照片上传成功');
|
||||
} else {
|
||||
ElMessage.error(result.msg || '照片上传失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('照片上传失败:', error);
|
||||
ElMessage.error('照片上传失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 删除照片
|
||||
const handlePhotoRemove = (file) => {
|
||||
const index = photoFileList.value.findIndex(item => item.uid === file.uid || item.url === file.url);
|
||||
if (index > -1) {
|
||||
photoFileList.value.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
// 视频文件变化处理(拖拽上传时触发)
|
||||
const handleVideoChange = (file, fileList) => {
|
||||
// 验证文件
|
||||
if (!beforeVideoUpload(file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 手动上传文件
|
||||
uploadVideoFile(file);
|
||||
};
|
||||
|
||||
// 手动上传视频文件
|
||||
const uploadVideoFile = async (file) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file.raw || file);
|
||||
|
||||
try {
|
||||
// 获取 token
|
||||
let token = '';
|
||||
const userStore = localStorage.getItem('userStore');
|
||||
if (userStore) {
|
||||
const us = JSON.parse(userStore);
|
||||
token = us.token || '';
|
||||
}
|
||||
|
||||
const response = await fetch('/api/common/upload', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': token,
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.code === 200) {
|
||||
const videoUrl = result.data?.url || result.data;
|
||||
videoFileList.value.push({
|
||||
url: videoUrl,
|
||||
name: file.name,
|
||||
uid: file.uid || Date.now(),
|
||||
});
|
||||
ElMessage.success('视频上传成功');
|
||||
} else {
|
||||
ElMessage.error(result.msg || '视频上传失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('视频上传失败:', error);
|
||||
ElMessage.error('视频上传失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 删除视频
|
||||
const handleVideoRemove = (file) => {
|
||||
const index = videoFileList.value.findIndex(item => item.uid === file.uid || item.url === file.url);
|
||||
if (index > -1) {
|
||||
videoFileList.value.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
// 上传前验证照片
|
||||
const beforePhotoUpload = (file) => {
|
||||
const isImage = file.type.startsWith('image/');
|
||||
const isLt10M = file.size / 1024 / 1024 < 10;
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error('只能上传图片文件!');
|
||||
return false;
|
||||
}
|
||||
if (!isLt10M) {
|
||||
ElMessage.error('图片大小不能超过 10MB!');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查数量限制
|
||||
if (photoFileList.value.length >= 9) {
|
||||
ElMessage.error('最多只能上传 9 张照片!');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true; // 允许继续处理
|
||||
};
|
||||
|
||||
// 上传前验证视频
|
||||
const beforeVideoUpload = (file) => {
|
||||
const isVideo = file.type.startsWith('video/');
|
||||
const isLt100M = file.size / 1024 / 1024 < 100;
|
||||
|
||||
if (!isVideo) {
|
||||
ElMessage.error('只能上传视频文件!');
|
||||
return false;
|
||||
}
|
||||
if (!isLt100M) {
|
||||
ElMessage.error('视频大小不能超过 100MB!');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查数量限制
|
||||
if (videoFileList.value.length >= 3) {
|
||||
ElMessage.error('最多只能上传 3 个视频!');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true; // 允许继续处理
|
||||
};
|
||||
|
||||
// 监听对话框打开,加载数据
|
||||
watch(() => data.dialogVisible, (newVal) => {
|
||||
if (newVal) {
|
||||
loadWarehouseList();
|
||||
loadOrderList();
|
||||
loadDeliveryList();
|
||||
}
|
||||
});
|
||||
|
||||
// 暴露方法给父组件调用
|
||||
defineExpose({
|
||||
onShowDialog,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.map {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
/* 照片预览列表样式 */
|
||||
.photo-preview-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.photo-preview-item {
|
||||
position: relative;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.photo-preview-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.photo-delete-btn {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
z-index: 10;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* 视频预览列表样式 */
|
||||
.video-preview-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.video-preview-item {
|
||||
position: relative;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.video-preview-player {
|
||||
width: 100%;
|
||||
max-height: 300px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.video-name {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.video-delete-btn {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
z-index: 10;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
</style>
|
||||
|
||||
323
pc-cattle-transportation/src/views/warehouse/warehouseOut.vue
Normal file
323
pc-cattle-transportation/src/views/warehouse/warehouseOut.vue
Normal file
@@ -0,0 +1,323 @@
|
||||
<template>
|
||||
<div>
|
||||
<base-search :formItemList="formItemList" @search="searchFrom" ref="baseSearchRef"></base-search>
|
||||
|
||||
<!-- 横向滚动操作栏 -->
|
||||
<div class="operation-scroll-bar">
|
||||
<div class="operation-scroll-container">
|
||||
<el-button
|
||||
type="primary"
|
||||
v-hasPermi="['warehouseout:add']"
|
||||
@click="showAddDialog"
|
||||
style="margin-left: 10px"
|
||||
>
|
||||
新增出仓记录
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-container">
|
||||
<el-table
|
||||
:data="rows"
|
||||
border
|
||||
v-loading="data.dataListLoading"
|
||||
element-loading-text="数据加载中..."
|
||||
style="width: 100%"
|
||||
:show-overflow-tooltip="true"
|
||||
>
|
||||
<el-table-column label="出仓单号" prop="outNumber" min-width="150" width="180">
|
||||
<template #default="scope">
|
||||
{{ scope.row.outNumber || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="中转仓" prop="warehouseName" min-width="120" width="150">
|
||||
<template #default="scope">
|
||||
{{ scope.row.warehouseName || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="进仓单号" prop="warehouseInNumber" min-width="150" width="180">
|
||||
<template #default="scope">
|
||||
{{ scope.row.warehouseInNumber || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="运单号" prop="deliveryNumber" min-width="120" width="150">
|
||||
<template #default="scope">
|
||||
{{ scope.row.deliveryNumber || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="车牌号" prop="licensePlate" min-width="100" width="120">
|
||||
<template #default="scope">
|
||||
{{ scope.row.licensePlate || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="司机姓名" prop="driverName" min-width="100" width="120">
|
||||
<template #default="scope">
|
||||
{{ scope.row.driverName || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="司机手机号" prop="driverMobile" min-width="120" width="150">
|
||||
<template #default="scope">
|
||||
{{ scope.row.driverMobile || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="目的地" prop="destinationLocation" min-width="150" width="200">
|
||||
<template #default="scope">
|
||||
{{ scope.row.destinationLocation || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="牛只数量(头)" prop="cattleCount" min-width="100" width="120">
|
||||
<template #default="scope">
|
||||
{{ scope.row.cattleCount || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="重量(公斤)" prop="weight" min-width="100" width="120">
|
||||
<template #default="scope">
|
||||
{{ scope.row.weight || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="出仓时间" prop="outTime" min-width="160" width="180">
|
||||
<template #default="scope">
|
||||
{{ scope.row.outTime || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" prop="status" min-width="80" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusType(scope.row.status)">
|
||||
{{ scope.row.statusDesc || '--' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" prop="createTime" min-width="160" width="180">
|
||||
<template #default="scope">
|
||||
{{ scope.row.createTime || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="200" width="250" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" v-hasPermi="['warehouseout:edit']" @click="showEditDialog(scope.row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button link type="primary" v-hasPermi="['warehouseout:query']" @click="showDetailDialog(scope.row)">
|
||||
详情
|
||||
</el-button>
|
||||
<el-button link type="danger" v-hasPermi="['warehouseout:delete']" @click="del(scope.row.id)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<div class="dataListOnEmpty">暂无数据</div>
|
||||
</template>
|
||||
</el-table>
|
||||
<Pagination
|
||||
v-if="data.total > 0"
|
||||
v-model:limit="form.pageSize"
|
||||
v-model:page="form.pageNum"
|
||||
:total="data.total"
|
||||
@pagination="handlePagination"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<warehouseOutDialog ref="dialogRef" @success="getDataList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import baseSearch from '@/components/common/searchCustom/index.vue';
|
||||
import Pagination from '@/components/Pagination/index.vue';
|
||||
import warehouseOutDialog from './warehouseOutDialog.vue';
|
||||
import { warehouseOutList, warehouseOutDel } from '@/api/warehouseOut.js';
|
||||
|
||||
const baseSearchRef = ref();
|
||||
const dialogRef = ref();
|
||||
const data = reactive({
|
||||
total: 0,
|
||||
dataListLoading: false,
|
||||
});
|
||||
const rows = ref([]);
|
||||
const form = reactive({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const formItemList = reactive([
|
||||
{
|
||||
label: '出仓单号',
|
||||
prop: 'outNumber',
|
||||
type: 'input',
|
||||
placeholder: '请输入出仓单号',
|
||||
span: 7,
|
||||
labelWidth: 100,
|
||||
},
|
||||
{
|
||||
label: '中转仓',
|
||||
prop: 'warehouseId',
|
||||
type: 'select',
|
||||
selectOptions: [],
|
||||
span: 7,
|
||||
labelWidth: 80,
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
selectOptions: [
|
||||
{ value: 1, text: '待出仓' },
|
||||
{ value: 2, text: '已出仓' },
|
||||
],
|
||||
span: 7,
|
||||
labelWidth: 80,
|
||||
},
|
||||
{
|
||||
label: '出仓时间',
|
||||
prop: 'outTime',
|
||||
type: 'daterange',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间',
|
||||
span: 7,
|
||||
labelWidth: 100,
|
||||
},
|
||||
]);
|
||||
|
||||
const searchFrom = () => {
|
||||
form.pageNum = 1;
|
||||
getDataList();
|
||||
};
|
||||
|
||||
const handlePagination = (paginationData) => {
|
||||
if (paginationData) {
|
||||
form.pageNum = paginationData.page || form.pageNum;
|
||||
form.pageSize = paginationData.limit || form.pageSize;
|
||||
}
|
||||
getDataList();
|
||||
};
|
||||
|
||||
const getDataList = () => {
|
||||
data.dataListLoading = true;
|
||||
const searchParams = baseSearchRef.value.penetrateParams();
|
||||
|
||||
const params = {
|
||||
...form,
|
||||
...searchParams,
|
||||
};
|
||||
|
||||
// 处理日期范围参数
|
||||
if (searchParams.outTime && Array.isArray(searchParams.outTime) && searchParams.outTime.length === 2) {
|
||||
params.startTime = searchParams.outTime[0];
|
||||
params.endTime = searchParams.outTime[1];
|
||||
delete params.outTime;
|
||||
}
|
||||
|
||||
warehouseOutList(params)
|
||||
.then((res) => {
|
||||
let responseData = res.data || res;
|
||||
|
||||
if (responseData && typeof responseData === 'object' && 'total' in responseData && 'rows' in responseData) {
|
||||
rows.value = responseData.rows || [];
|
||||
data.total = responseData.total || 0;
|
||||
} else if (responseData && responseData.data) {
|
||||
rows.value = responseData.data.rows || [];
|
||||
data.total = responseData.data.total || 0;
|
||||
} else {
|
||||
rows.value = [];
|
||||
data.total = 0;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('查询失败:', error);
|
||||
ElMessage.error('查询失败:' + (error.message || '未知错误'));
|
||||
rows.value = [];
|
||||
data.total = 0;
|
||||
})
|
||||
.finally(() => {
|
||||
data.dataListLoading = false;
|
||||
});
|
||||
};
|
||||
|
||||
const showAddDialog = () => {
|
||||
if (dialogRef.value) {
|
||||
dialogRef.value.onShowDialog(null);
|
||||
}
|
||||
};
|
||||
|
||||
const showEditDialog = (row) => {
|
||||
if (dialogRef.value) {
|
||||
dialogRef.value.onShowDialog(row);
|
||||
}
|
||||
};
|
||||
|
||||
const showDetailDialog = (row) => {
|
||||
if (dialogRef.value) {
|
||||
dialogRef.value.onShowDialog(row, true);
|
||||
}
|
||||
};
|
||||
|
||||
const del = (id) => {
|
||||
ElMessageBox.confirm('确定要删除这条出仓记录吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
warehouseOutDel(id)
|
||||
.then((res) => {
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除成功');
|
||||
getDataList();
|
||||
} else {
|
||||
ElMessage.error(res.msg || '删除失败');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('删除失败:', error);
|
||||
ElMessage.error('删除失败:' + (error.message || '未知错误'));
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// 用户取消删除
|
||||
});
|
||||
};
|
||||
|
||||
const getStatusType = (status) => {
|
||||
const typeMap = {
|
||||
1: 'info', // 待出仓 - 灰色
|
||||
2: 'success', // 已出仓 - 绿色
|
||||
};
|
||||
return typeMap[status] || 'info';
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getDataList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.operation-scroll-bar {
|
||||
background: #fff;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.operation-scroll-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.dataListOnEmpty {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,750 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="data.dialogVisible"
|
||||
:title="data.isDetail ? '出仓详情' : (data.editId ? '编辑出仓记录' : '新增出仓记录')"
|
||||
:before-close="handleClose"
|
||||
width="900px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form ref="formDataRef" :model="ruleForm" :rules="rules" label-width="120px" :disabled="data.isDetail">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="中转仓" prop="warehouseId">
|
||||
<el-select
|
||||
v-model="ruleForm.warehouseId"
|
||||
placeholder="请选择中转仓"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 100%"
|
||||
@change="handleWarehouseChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in warehouseList"
|
||||
:key="item.id"
|
||||
:label="item.warehouseName"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="进仓记录" prop="warehouseInId">
|
||||
<el-select
|
||||
v-model="ruleForm.warehouseInId"
|
||||
placeholder="请选择进仓记录(可选)"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 100%"
|
||||
@change="handleWarehouseInChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in warehouseInList"
|
||||
:key="item.id"
|
||||
:label="`${item.inNumber || '--'} - ${item.cattleCount || 0}头`"
|
||||
: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="deliveryId">
|
||||
<el-select
|
||||
v-model="ruleForm.deliveryId"
|
||||
placeholder="请选择运送清单"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 100%"
|
||||
@change="handleDeliveryChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in deliveryList"
|
||||
:key="item.id"
|
||||
:label="`${item.deliveryNumber || '--'} - ${item.licensePlate || '--'}`"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="订单" prop="orderId">
|
||||
<el-select
|
||||
v-model="ruleForm.orderId"
|
||||
placeholder="请选择订单(可多选)"
|
||||
multiple
|
||||
clearable
|
||||
filterable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in orderList"
|
||||
:key="item.id"
|
||||
:label="`订单${item.id} - 单价: ${item.firmPrice || '--'}元/斤`"
|
||||
: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="outTime">
|
||||
<el-date-picker
|
||||
v-model="ruleForm.outTime"
|
||||
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="24">
|
||||
<el-form-item label="目的地" prop="destinationLocation">
|
||||
<el-input
|
||||
v-model="ruleForm.destinationLocation"
|
||||
placeholder="请输入目的地"
|
||||
maxlength="255"
|
||||
style="width: calc(100% - 100px); margin-right: 10px;"
|
||||
/>
|
||||
<el-button type="primary" @click="openDestinationLocationMap">选择位置</el-button>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="目的地经度">
|
||||
<el-input v-model="ruleForm.destinationLon" placeholder="经度" disabled />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="目的地纬度">
|
||||
<el-input v-model="ruleForm.destinationLat" placeholder="纬度" disabled />
|
||||
</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="ruleForm.cattleCount"
|
||||
:min="1"
|
||||
:max="9999"
|
||||
placeholder="请输入牛只数量"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="重量(公斤)" prop="weight">
|
||||
<el-input-number
|
||||
v-model="ruleForm.weight"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
placeholder="请输入重量"
|
||||
style="width: 100%"
|
||||
>
|
||||
<template #append>kg</template>
|
||||
</el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="ruleForm.status" placeholder="请选择状态" style="width: 100%">
|
||||
<el-option label="待出仓" :value="1" />
|
||||
<el-option label="已出仓" :value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="照片">
|
||||
<el-upload
|
||||
v-model:file-list="photoFileList"
|
||||
action="#"
|
||||
list-type="picture-card"
|
||||
:auto-upload="false"
|
||||
:on-preview="handlePictureCardPreview"
|
||||
:on-remove="handlePhotoRemove"
|
||||
:before-upload="beforePhotoUpload"
|
||||
:limit="9"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="视频">
|
||||
<el-upload
|
||||
v-model:file-list="videoFileList"
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
:on-remove="handleVideoRemove"
|
||||
:before-upload="beforeVideoUpload"
|
||||
:limit="3"
|
||||
>
|
||||
<el-button type="primary">选择视频</el-button>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="ruleForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button v-if="!data.isDetail" :loading="data.saveLoading" type="primary" @click="onClickSave">
|
||||
保存
|
||||
</el-button>
|
||||
<el-button @click="handleClose">{{ data.isDetail ? '关闭' : '取消' }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 目的地地址选择地图 -->
|
||||
<el-dialog v-model="showDestinationLocationMap" title="选择目的地地址" width="900px">
|
||||
<baidu-map
|
||||
class="map"
|
||||
:center="ruleForm.destinationLon && ruleForm.destinationLat ? {lng: parseFloat(ruleForm.destinationLon), lat: parseFloat(ruleForm.destinationLat)} : {lng: 116.404, lat: 39.915}"
|
||||
:zoom="15"
|
||||
:scroll-wheel-zoom="true"
|
||||
@click="handleDestinationLocationClick"
|
||||
style="height: 500px"
|
||||
>
|
||||
<bm-marker
|
||||
v-if="ruleForm.destinationLon && ruleForm.destinationLat"
|
||||
:position="{lng: parseFloat(ruleForm.destinationLon), lat: parseFloat(ruleForm.destinationLat)}"
|
||||
:dragging="true"
|
||||
@dragging="handleDestinationMarkerDrag"
|
||||
/>
|
||||
<bm-map-type :map-types="['BMAP_NORMAL_MAP', 'BMAP_HYBRID_MAP']"></bm-map-type>
|
||||
</baidu-map>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showDestinationLocationMap = false">取消</el-button>
|
||||
<el-button type="primary" @click="showDestinationLocationMap = false">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 图片预览 -->
|
||||
<el-dialog v-model="showImageViewer" title="图片预览" width="800px">
|
||||
<el-image :src="imageViewerUrl" style="width: 100%" fit="contain" />
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Plus } from '@element-plus/icons-vue';
|
||||
import { BaiduMap, BmMapType, BmMarker } from 'vue-baidu-map-3x';
|
||||
import { warehouseOutAdd, warehouseOutEdit, warehouseOutDetail } from '@/api/warehouseOut.js';
|
||||
import { warehouseAll } from '@/api/warehouse.js';
|
||||
import { warehouseInList as warehouseInListApi } from '@/api/warehouseIn.js';
|
||||
import { orderPageQuery, shippingList } from '@/api/shipping.js';
|
||||
|
||||
const emits = defineEmits(['success']);
|
||||
const formDataRef = ref(null);
|
||||
const showDestinationLocationMap = ref(false);
|
||||
const showImageViewer = ref(false);
|
||||
const imageViewerUrl = ref('');
|
||||
const photoFileList = ref([]);
|
||||
const videoFileList = ref([]);
|
||||
|
||||
const warehouseList = ref([]);
|
||||
const warehouseInList = ref([]);
|
||||
const orderList = ref([]);
|
||||
const deliveryList = ref([]);
|
||||
|
||||
const data = reactive({
|
||||
dialogVisible: false,
|
||||
saveLoading: false,
|
||||
editId: null,
|
||||
isDetail: false,
|
||||
});
|
||||
|
||||
const ruleForm = reactive({
|
||||
id: null,
|
||||
warehouseId: null,
|
||||
warehouseInId: null,
|
||||
orderId: [],
|
||||
deliveryId: null,
|
||||
destinationLocation: '',
|
||||
destinationLon: '',
|
||||
destinationLat: '',
|
||||
cattleCount: null,
|
||||
weight: null,
|
||||
outTime: '',
|
||||
photos: '',
|
||||
videos: '',
|
||||
remark: '',
|
||||
status: 1,
|
||||
});
|
||||
|
||||
const rules = reactive({
|
||||
warehouseId: [
|
||||
{ required: true, message: '请选择中转仓', trigger: 'change' },
|
||||
],
|
||||
deliveryId: [
|
||||
{ required: true, message: '请选择运送清单', trigger: 'change' },
|
||||
],
|
||||
cattleCount: [
|
||||
{ required: true, message: '请输入牛只数量', trigger: 'blur' },
|
||||
{ type: 'number', min: 1, message: '牛只数量必须大于0', trigger: 'blur' },
|
||||
],
|
||||
outTime: [
|
||||
{ required: true, message: '请选择出仓时间', trigger: 'change' },
|
||||
],
|
||||
});
|
||||
|
||||
// 加载中转仓列表
|
||||
const loadWarehouseList = async () => {
|
||||
try {
|
||||
const res = await warehouseAll();
|
||||
if (res.code === 200) {
|
||||
warehouseList.value = res.data || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载中转仓列表失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载进仓记录列表(不限制状态,避免过滤掉待进仓记录)
|
||||
const loadWarehouseInList = async (warehouseId) => {
|
||||
try {
|
||||
const params = { pageNum: 1, pageSize: 1000 };
|
||||
if (warehouseId) {
|
||||
params.warehouseId = warehouseId;
|
||||
}
|
||||
const res = await warehouseInListApi(params);
|
||||
if (res.code === 200) {
|
||||
const responseData = res.data || res;
|
||||
if (responseData && typeof responseData === 'object' && 'rows' in responseData) {
|
||||
warehouseInList.value = responseData.rows || [];
|
||||
} else if (responseData && responseData.data && 'rows' in responseData.data) {
|
||||
warehouseInList.value = responseData.data.rows || [];
|
||||
} else {
|
||||
warehouseInList.value = [];
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载进仓记录列表失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载订单列表
|
||||
const loadOrderList = async () => {
|
||||
try {
|
||||
const res = await orderPageQuery({ pageNum: 1, pageSize: 1000 });
|
||||
if (res.code === 200) {
|
||||
const responseData = res.data || res;
|
||||
if (responseData && typeof responseData === 'object' && 'rows' in responseData) {
|
||||
orderList.value = responseData.rows || [];
|
||||
} else if (responseData && responseData.data && 'rows' in responseData.data) {
|
||||
orderList.value = responseData.data.rows || [];
|
||||
} else {
|
||||
orderList.value = [];
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载订单列表失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载运送清单列表
|
||||
const loadDeliveryList = async () => {
|
||||
try {
|
||||
const res = await shippingList({ pageNum: 1, pageSize: 1000 });
|
||||
if (res.code === 200) {
|
||||
const responseData = res.data || res;
|
||||
if (responseData && typeof responseData === 'object' && 'rows' in responseData) {
|
||||
deliveryList.value = responseData.rows || [];
|
||||
} else if (responseData && responseData.data && 'rows' in responseData.data) {
|
||||
deliveryList.value = responseData.data.rows || [];
|
||||
} else {
|
||||
deliveryList.value = [];
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载运送清单列表失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (formDataRef.value) {
|
||||
formDataRef.value.resetFields();
|
||||
}
|
||||
Object.assign(ruleForm, {
|
||||
id: null,
|
||||
warehouseId: null,
|
||||
warehouseInId: null,
|
||||
orderId: [],
|
||||
deliveryId: null,
|
||||
destinationLocation: '',
|
||||
destinationLon: '',
|
||||
destinationLat: '',
|
||||
cattleCount: null,
|
||||
weight: null,
|
||||
outTime: '',
|
||||
photos: '',
|
||||
videos: '',
|
||||
remark: '',
|
||||
status: 1,
|
||||
});
|
||||
photoFileList.value = [];
|
||||
videoFileList.value = [];
|
||||
data.editId = null;
|
||||
data.isDetail = false;
|
||||
data.dialogVisible = false;
|
||||
};
|
||||
|
||||
const onClickSave = () => {
|
||||
if (!formDataRef.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
formDataRef.value.validate((valid) => {
|
||||
if (!valid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
data.saveLoading = true;
|
||||
|
||||
// 处理订单ID(多个订单用逗号分隔)
|
||||
const orderIdStr = ruleForm.orderId && ruleForm.orderId.length > 0
|
||||
? ruleForm.orderId.join(',')
|
||||
: null;
|
||||
|
||||
// 处理照片和视频URL(多个用逗号分隔)
|
||||
const photosStr = photoFileList.value.map(file => file.url || file.response?.data?.url || '').filter(url => url).join(',');
|
||||
const videosStr = videoFileList.value.map(file => file.url || file.response?.data?.url || '').filter(url => url).join(',');
|
||||
|
||||
const params = {
|
||||
warehouseId: ruleForm.warehouseId,
|
||||
warehouseInId: ruleForm.warehouseInId,
|
||||
orderId: orderIdStr,
|
||||
deliveryId: ruleForm.deliveryId,
|
||||
destinationLocation: ruleForm.destinationLocation,
|
||||
destinationLon: ruleForm.destinationLon,
|
||||
destinationLat: ruleForm.destinationLat,
|
||||
cattleCount: ruleForm.cattleCount,
|
||||
weight: ruleForm.weight,
|
||||
outTime: ruleForm.outTime,
|
||||
photos: photosStr,
|
||||
videos: videosStr,
|
||||
remark: ruleForm.remark,
|
||||
};
|
||||
|
||||
if (data.editId) {
|
||||
// 编辑
|
||||
params.id = data.editId;
|
||||
params.status = ruleForm.status;
|
||||
warehouseOutEdit(params)
|
||||
.then((res) => {
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('编辑成功');
|
||||
handleClose();
|
||||
emits('success');
|
||||
} else {
|
||||
ElMessage.error(res.msg || '编辑失败');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
ElMessage.error('编辑失败:' + (error.message || '未知错误'));
|
||||
})
|
||||
.finally(() => {
|
||||
data.saveLoading = false;
|
||||
});
|
||||
} else {
|
||||
// 新增
|
||||
warehouseOutAdd(params)
|
||||
.then((res) => {
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('新增成功');
|
||||
handleClose();
|
||||
emits('success');
|
||||
} else {
|
||||
ElMessage.error(res.msg || '新增失败');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
ElMessage.error('新增失败:' + (error.message || '未知错误'));
|
||||
})
|
||||
.finally(() => {
|
||||
data.saveLoading = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onShowDialog = (row, isDetail = false) => {
|
||||
data.isDetail = isDetail || false;
|
||||
data.editId = null;
|
||||
|
||||
if (row) {
|
||||
data.editId = row.id;
|
||||
// 如果是详情模式,先获取详情数据
|
||||
if (isDetail) {
|
||||
warehouseOutDetail(row.id)
|
||||
.then((res) => {
|
||||
if (res.code === 200 && res.data) {
|
||||
const detailData = res.data;
|
||||
Object.assign(ruleForm, {
|
||||
id: detailData.id,
|
||||
warehouseId: detailData.warehouseId,
|
||||
warehouseInId: detailData.warehouseInId,
|
||||
orderId: detailData.orderId ? detailData.orderId.split(',').map(id => parseInt(id)) : [],
|
||||
deliveryId: detailData.deliveryId,
|
||||
destinationLocation: detailData.destinationLocation || '',
|
||||
destinationLon: detailData.destinationLon || '',
|
||||
destinationLat: detailData.destinationLat || '',
|
||||
cattleCount: detailData.cattleCount,
|
||||
weight: detailData.weight,
|
||||
outTime: detailData.outTime || '',
|
||||
photos: detailData.photos || '',
|
||||
videos: detailData.videos || '',
|
||||
remark: detailData.remark || '',
|
||||
status: detailData.status !== undefined ? detailData.status : 1,
|
||||
});
|
||||
// 处理照片和视频文件列表
|
||||
if (detailData.photos) {
|
||||
photoFileList.value = detailData.photos.split(',').map(url => ({ url, name: 'photo' }));
|
||||
}
|
||||
if (detailData.videos) {
|
||||
videoFileList.value = detailData.videos.split(',').map(url => ({ url, name: 'video' }));
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
ElMessage.error('获取详情失败:' + (error.message || '未知错误'));
|
||||
});
|
||||
} else {
|
||||
// 编辑模式,直接使用传入的数据
|
||||
Object.assign(ruleForm, {
|
||||
id: row.id,
|
||||
warehouseId: row.warehouseId,
|
||||
warehouseInId: row.warehouseInId,
|
||||
orderId: row.orderId ? row.orderId.split(',').map(id => parseInt(id)) : [],
|
||||
deliveryId: row.deliveryId,
|
||||
destinationLocation: row.destinationLocation || '',
|
||||
destinationLon: row.destinationLon || '',
|
||||
destinationLat: row.destinationLat || '',
|
||||
cattleCount: row.cattleCount,
|
||||
weight: row.weight,
|
||||
outTime: row.outTime || '',
|
||||
photos: row.photos || '',
|
||||
videos: row.videos || '',
|
||||
remark: row.remark || '',
|
||||
status: row.status !== undefined ? row.status : 1,
|
||||
});
|
||||
// 处理照片和视频文件列表
|
||||
if (row.photos) {
|
||||
photoFileList.value = row.photos.split(',').map(url => ({ url, name: 'photo' }));
|
||||
}
|
||||
if (row.videos) {
|
||||
videoFileList.value = row.videos.split(',').map(url => ({ url, name: 'video' }));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 新增模式,重置表单
|
||||
Object.assign(ruleForm, {
|
||||
id: null,
|
||||
warehouseId: null,
|
||||
warehouseInId: null,
|
||||
orderId: [],
|
||||
deliveryId: null,
|
||||
destinationLocation: '',
|
||||
destinationLon: '',
|
||||
destinationLat: '',
|
||||
cattleCount: null,
|
||||
weight: null,
|
||||
outTime: '',
|
||||
photos: '',
|
||||
videos: '',
|
||||
remark: '',
|
||||
status: 1,
|
||||
});
|
||||
photoFileList.value = [];
|
||||
videoFileList.value = [];
|
||||
}
|
||||
|
||||
data.dialogVisible = true;
|
||||
};
|
||||
|
||||
// 中转仓选择变化
|
||||
const handleWarehouseChange = (warehouseId) => {
|
||||
// 加载该中转仓的进仓记录
|
||||
loadWarehouseInList(warehouseId);
|
||||
};
|
||||
|
||||
// 进仓记录选择变化
|
||||
const handleWarehouseInChange = (warehouseInId) => {
|
||||
// 如果选择了进仓记录,可以自动填充一些信息
|
||||
if (warehouseInId) {
|
||||
const warehouseIn = warehouseInList.value.find(item => item.id === warehouseInId);
|
||||
if (warehouseIn) {
|
||||
// 可以自动填充牛只数量等信息
|
||||
if (!ruleForm.cattleCount && warehouseIn.cattleCount) {
|
||||
ruleForm.cattleCount = warehouseIn.cattleCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 运送清单选择变化
|
||||
const handleDeliveryChange = (deliveryId) => {
|
||||
// 可以在这里添加逻辑
|
||||
};
|
||||
|
||||
// 打开目的地地图选择地址
|
||||
const openDestinationLocationMap = () => {
|
||||
if (ruleForm.destinationLocation && ruleForm.destinationLocation.trim()) {
|
||||
showDestinationLocationMap.value = true;
|
||||
setTimeout(() => {
|
||||
if (window.BMap && window.BMap.Geocoder) {
|
||||
const geocoder = new window.BMap.Geocoder();
|
||||
geocoder.getPoint(ruleForm.destinationLocation, (point) => {
|
||||
if (point) {
|
||||
ruleForm.destinationLon = point.lng;
|
||||
ruleForm.destinationLat = point.lat;
|
||||
ElMessage.success('已定位到该地址');
|
||||
} else {
|
||||
ElMessage.warning('未找到该地址,请在地图上手动选择');
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 500);
|
||||
} else {
|
||||
showDestinationLocationMap.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
// 地图点击事件
|
||||
const handleDestinationLocationClick = (e) => {
|
||||
ruleForm.destinationLon = e.point.lng;
|
||||
ruleForm.destinationLat = e.point.lat;
|
||||
if (window.BMap && window.BMap.Geocoder) {
|
||||
const geocoder = new window.BMap.Geocoder();
|
||||
geocoder.getLocation(e.point, (res) => {
|
||||
if (res) {
|
||||
ruleForm.destinationLocation = res.address;
|
||||
ElMessage.success('已设置目的地地址');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 标记拖拽事件
|
||||
const handleDestinationMarkerDrag = (e) => {
|
||||
ruleForm.destinationLon = e.point.lng;
|
||||
ruleForm.destinationLat = e.point.lat;
|
||||
if (window.BMap && window.BMap.Geocoder) {
|
||||
const geocoder = new window.BMap.Geocoder();
|
||||
geocoder.getLocation(e.point, (res) => {
|
||||
if (res) {
|
||||
ruleForm.destinationLocation = res.address;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 图片预览
|
||||
const handlePictureCardPreview = (file) => {
|
||||
imageViewerUrl.value = file.url || file.response?.data?.url || '';
|
||||
showImageViewer.value = true;
|
||||
};
|
||||
|
||||
// 删除照片
|
||||
const handlePhotoRemove = (file) => {
|
||||
// 文件列表会自动更新
|
||||
};
|
||||
|
||||
// 删除视频
|
||||
const handleVideoRemove = (file) => {
|
||||
// 文件列表会自动更新
|
||||
};
|
||||
|
||||
// 上传前验证照片
|
||||
const beforePhotoUpload = (file) => {
|
||||
const isImage = file.type.startsWith('image/');
|
||||
const isLt10M = file.size / 1024 / 1024 < 10;
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error('只能上传图片文件!');
|
||||
return false;
|
||||
}
|
||||
if (!isLt10M) {
|
||||
ElMessage.error('图片大小不能超过 10MB!');
|
||||
return false;
|
||||
}
|
||||
return false; // 阻止自动上传,手动处理
|
||||
};
|
||||
|
||||
// 上传前验证视频
|
||||
const beforeVideoUpload = (file) => {
|
||||
const isVideo = file.type.startsWith('video/');
|
||||
const isLt100M = file.size / 1024 / 1024 < 100;
|
||||
|
||||
if (!isVideo) {
|
||||
ElMessage.error('只能上传视频文件!');
|
||||
return false;
|
||||
}
|
||||
if (!isLt100M) {
|
||||
ElMessage.error('视频大小不能超过 100MB!');
|
||||
return false;
|
||||
}
|
||||
return false; // 阻止自动上传,手动处理
|
||||
};
|
||||
|
||||
// 监听对话框打开,加载数据
|
||||
watch(() => data.dialogVisible, (newVal) => {
|
||||
if (newVal) {
|
||||
loadWarehouseList();
|
||||
loadWarehouseInList();
|
||||
loadOrderList();
|
||||
loadDeliveryList();
|
||||
}
|
||||
});
|
||||
|
||||
// 暴露方法给父组件调用
|
||||
defineExpose({
|
||||
onShowDialog,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.map {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -231,6 +231,11 @@
|
||||
"resolved" "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz"
|
||||
"version" "3.6.1"
|
||||
|
||||
"@dimforge/rapier3d-compat@~0.12.0":
|
||||
"integrity" "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="
|
||||
"resolved" "https://registry.npmmirror.com/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz"
|
||||
"version" "0.12.0"
|
||||
|
||||
"@element-plus/icons-vue@^2.3.1":
|
||||
"integrity" "sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg=="
|
||||
"resolved" "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.1.tgz"
|
||||
@@ -413,6 +418,18 @@
|
||||
"resolved" "https://registry.npmmirror.com/@tsconfig/node16/-/node16-1.0.4.tgz"
|
||||
"version" "1.0.4"
|
||||
|
||||
"@tweenjs/tween.js@~23.1.3":
|
||||
"integrity" "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="
|
||||
"resolved" "https://registry.npmmirror.com/@tweenjs/tween.js/-/tween.js-23.1.3.tgz"
|
||||
"version" "23.1.3"
|
||||
|
||||
"@types/d3-geo@^3.1.0":
|
||||
"integrity" "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="
|
||||
"resolved" "https://registry.npmmirror.com/@types/d3-geo/-/d3-geo-3.1.0.tgz"
|
||||
"version" "3.1.0"
|
||||
dependencies:
|
||||
"@types/geojson" "*"
|
||||
|
||||
"@types/estree@^1.0.0":
|
||||
"integrity" "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
|
||||
"resolved" "https://registry.npmmirror.com/@types/estree/-/estree-1.0.5.tgz"
|
||||
@@ -423,6 +440,11 @@
|
||||
"resolved" "https://registry.npmmirror.com/@types/event-emitter/-/event-emitter-0.3.5.tgz"
|
||||
"version" "0.3.5"
|
||||
|
||||
"@types/geojson@*":
|
||||
"integrity" "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="
|
||||
"resolved" "https://registry.npmmirror.com/@types/geojson/-/geojson-7946.0.16.tgz"
|
||||
"version" "7946.0.16"
|
||||
|
||||
"@types/json-schema@^7.0.9":
|
||||
"integrity" "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
|
||||
"resolved" "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz"
|
||||
@@ -477,6 +499,11 @@
|
||||
"resolved" "https://registry.npmmirror.com/@types/sortablejs/-/sortablejs-1.15.8.tgz"
|
||||
"version" "1.15.8"
|
||||
|
||||
"@types/stats.js@*":
|
||||
"integrity" "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA=="
|
||||
"resolved" "https://registry.npmmirror.com/@types/stats.js/-/stats.js-0.17.4.tgz"
|
||||
"version" "0.17.4"
|
||||
|
||||
"@types/svgo@^2.6.1":
|
||||
"integrity" "sha512-l4cmyPEckf8moNYHdJ+4wkHvFxjyW6ulm9l4YGaOxeyBWPhBOT0gvni1InpFPdzx1dKf/2s62qGITwxNWnPQng=="
|
||||
"resolved" "https://registry.npmmirror.com/@types/svgo/-/svgo-2.6.4.tgz"
|
||||
@@ -484,6 +511,19 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/three@^0.181.0":
|
||||
"integrity" "sha512-MLF1ks8yRM2k71D7RprFpDb9DOX0p22DbdPqT/uAkc6AtQXjxWCVDjCy23G9t1o8HcQPk7woD2NIyiaWcWPYmA=="
|
||||
"resolved" "https://registry.npmmirror.com/@types/three/-/three-0.181.0.tgz"
|
||||
"version" "0.181.0"
|
||||
dependencies:
|
||||
"@dimforge/rapier3d-compat" "~0.12.0"
|
||||
"@tweenjs/tween.js" "~23.1.3"
|
||||
"@types/stats.js" "*"
|
||||
"@types/webxr" "*"
|
||||
"@webgpu/types" "*"
|
||||
"fflate" "~0.8.2"
|
||||
"meshoptimizer" "~0.22.0"
|
||||
|
||||
"@types/web-bluetooth@^0.0.16":
|
||||
"integrity" "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ=="
|
||||
"resolved" "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz"
|
||||
@@ -494,6 +534,11 @@
|
||||
"resolved" "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz"
|
||||
"version" "0.0.20"
|
||||
|
||||
"@types/webxr@*":
|
||||
"integrity" "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg=="
|
||||
"resolved" "https://registry.npmmirror.com/@types/webxr/-/webxr-0.5.24.tgz"
|
||||
"version" "0.5.24"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^5.38.1":
|
||||
"integrity" "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag=="
|
||||
"resolved" "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz"
|
||||
@@ -904,6 +949,11 @@
|
||||
"resolved" "https://registry.npmmirror.com/@wangeditor/video-module/-/video-module-1.1.4.tgz"
|
||||
"version" "1.1.4"
|
||||
|
||||
"@webgpu/types@*":
|
||||
"integrity" "sha512-uk53+2ECGUkWoDFez/hymwpRfdgdIn6y1ref70fEecGMe5607f4sozNFgBk0oxlr7j2CRGWBEc3IBYMmFdGGTQ=="
|
||||
"resolved" "https://registry.npmmirror.com/@webgpu/types/-/types-0.1.67.tgz"
|
||||
"version" "0.1.67"
|
||||
|
||||
"@windicss/config@1.9.3":
|
||||
"integrity" "sha512-u8GUjsfC9r5X1AGYhzb1lX3zZj8wqk6SH1DYex8XUGmZ1M2UpvnUPOFi63XFViduspQ6l2xTX84QtG+lUzhEoQ=="
|
||||
"resolved" "https://registry.npmmirror.com/@windicss/config/-/config-1.9.3.tgz"
|
||||
@@ -1947,6 +1997,20 @@
|
||||
"es5-ext" "^0.10.64"
|
||||
"type" "^2.7.2"
|
||||
|
||||
"d3-array@2.5.0 - 3":
|
||||
"integrity" "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="
|
||||
"resolved" "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.4.tgz"
|
||||
"version" "3.2.4"
|
||||
dependencies:
|
||||
"internmap" "1 - 2"
|
||||
|
||||
"d3-geo@^3.1.1":
|
||||
"integrity" "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="
|
||||
"resolved" "https://registry.npmmirror.com/d3-geo/-/d3-geo-3.1.1.tgz"
|
||||
"version" "3.1.1"
|
||||
dependencies:
|
||||
"d3-array" "2.5.0 - 3"
|
||||
|
||||
"dargs@^7.0.0":
|
||||
"integrity" "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg=="
|
||||
"resolved" "https://registry.npmmirror.com/dargs/-/dargs-7.0.0.tgz"
|
||||
@@ -2886,6 +2950,11 @@
|
||||
dependencies:
|
||||
"reusify" "^1.0.4"
|
||||
|
||||
"fflate@~0.8.2":
|
||||
"integrity" "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="
|
||||
"resolved" "https://registry.npmmirror.com/fflate/-/fflate-0.8.2.tgz"
|
||||
"version" "0.8.2"
|
||||
|
||||
"figures@^2.0.0":
|
||||
"integrity" "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA=="
|
||||
"resolved" "https://registry.npmmirror.com/figures/-/figures-2.0.0.tgz"
|
||||
@@ -3600,6 +3669,11 @@
|
||||
"hasown" "^2.0.2"
|
||||
"side-channel" "^1.1.0"
|
||||
|
||||
"internmap@1 - 2":
|
||||
"integrity" "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="
|
||||
"resolved" "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz"
|
||||
"version" "2.0.3"
|
||||
|
||||
"is-accessor-descriptor@^1.0.1":
|
||||
"integrity" "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA=="
|
||||
"resolved" "https://registry.npmmirror.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz"
|
||||
@@ -4443,6 +4517,11 @@
|
||||
"resolved" "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz"
|
||||
"version" "1.4.1"
|
||||
|
||||
"meshoptimizer@~0.22.0":
|
||||
"integrity" "sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg=="
|
||||
"resolved" "https://registry.npmmirror.com/meshoptimizer/-/meshoptimizer-0.22.0.tgz"
|
||||
"version" "0.22.0"
|
||||
|
||||
"micromatch@^4.0.2", "micromatch@^4.0.4", "micromatch@^4.0.5":
|
||||
"integrity" "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q=="
|
||||
"resolved" "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.7.tgz"
|
||||
@@ -6183,6 +6262,11 @@
|
||||
"resolved" "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz"
|
||||
"version" "0.2.0"
|
||||
|
||||
"three@^0.181.2":
|
||||
"integrity" "sha512-k/CjiZ80bYss6Qs7/ex1TBlPD11whT9oKfT8oTGiHa34W4JRd1NiH/Tr1DbHWQ2/vMUypxksLnF2CfmlmM5XFQ=="
|
||||
"resolved" "https://registry.npmmirror.com/three/-/three-0.181.2.tgz"
|
||||
"version" "0.181.2"
|
||||
|
||||
"throttle-debounce@^5.0.0":
|
||||
"integrity" "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A=="
|
||||
"resolved" "https://registry.npmmirror.com/throttle-debounce/-/throttle-debounce-5.0.2.tgz"
|
||||
|
||||
Reference in New Issue
Block a user