完成中转仓管理

This commit is contained in:
xuqiuyun
2025-12-08 15:24:43 +08:00
parent e968fcf52a
commit 620975c04d
981 changed files with 154245 additions and 83 deletions

View File

@@ -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",

View 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);
});
}

View 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',
});
}

View 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',
});
}

View 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',
});
}

View 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',
});
}

View File

@@ -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';

View File

@@ -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)

View File

@@ -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 = [

View File

@@ -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);

View 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>

View File

@@ -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 实例
// 创建地图实例

View File

@@ -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 => {
})

View File

@@ -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 {

View 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>

View 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>

View File

@@ -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;

View File

@@ -447,7 +447,7 @@ const initTrackMap = async () => {
}
try {
const BMapGL = await BMPGL('xITbC7jegaAAuu4m9jC2Zx6eFbQJ29Rj');
const BMapGL = await BMPGL('3AN3VahoqaXUs32U8luXD2Dwn86KK5B7');
// 创建地图实例
trackMapInstance.value = new BMapGL.Map('trackMap');

View File

@@ -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({

View 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>

View 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>

View 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>

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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"