This commit is contained in:
x 2025-07-18 16:03:17 +08:00
parent c60eff6a62
commit b006e5d38b
5 changed files with 576 additions and 0 deletions

View File

@ -0,0 +1,48 @@
package com.wms_main.controller.wms;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.wms_main.model.dto.query.LocationQuery;
import com.wms_main.model.dto.response.wms.WmsApiResponse;
import com.wms_main.model.dto.response.wms.BaseWmsApiResponse;
import com.wms_main.model.po.TAppLocation;
import com.wms_main.dao.ITAppLocationService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@ResponseBody
@CrossOrigin
@RequiredArgsConstructor
@RequestMapping("/locationManage")
public class LocationManageController {
private final ITAppLocationService locationService;
@PostMapping("/list")
public WmsApiResponse<Page<TAppLocation>> list(@RequestBody LocationQuery query) {
return WmsApiResponse.success(locationService.pageQuery(query));
}
@PostMapping("/add")
public BaseWmsApiResponse add(@RequestBody TAppLocation location) {
locationService.save(location);
return BaseWmsApiResponse.success();
}
@PostMapping("/update")
public BaseWmsApiResponse update(@RequestBody TAppLocation location) {
locationService.updateById(location);
return BaseWmsApiResponse.success();
}
@PostMapping("/delete")
public BaseWmsApiResponse delete(@RequestBody Map<String, Object> params) {
String locationId = params.get("locationId").toString();
LambdaQueryWrapper<TAppLocation> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(TAppLocation::getLocationId, locationId);
locationService.remove(wrapper);
return BaseWmsApiResponse.success();
}
}

View File

@ -0,0 +1,73 @@
package com.wms_main.controller.wms;
import com.wms_main.dao.ITSysMenuService;
import com.wms_main.model.dto.response.wms.WmsApiResponse;
import com.wms_main.model.po.TSysMenu;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import static com.wms_main.model.dto.response.wms.WmsApiResponse.success;
/**
* 菜单管理控制器临时用于添加仓库管理菜单
*/
@RestController
@ResponseBody
@CrossOrigin
@RequiredArgsConstructor
@RequestMapping("/wms/menu")
public class MenuController {
private final ITSysMenuService menuService;
/**
* 添加仓库管理菜单
*/
@PostMapping("/addLocationManageMenu")
public WmsApiResponse<String> addLocationManageMenu() {
try {
// 检查菜单是否已存在
TSysMenu existingMenu = menuService.getById("1-8");
if (existingMenu != null) {
return success("仓库管理菜单已存在", "菜单ID: 1-8");
}
// 创建新的菜单项
TSysMenu menu = new TSysMenu();
menu.setMenuId("1-8");
menu.setLabelName("仓库管理");
menu.setIconValue("");
menu.setPath("/locationManage");
menu.setParentId("1");
// 保存菜单
boolean result = menuService.save(menu);
if (result) {
return success("仓库管理菜单添加成功", "菜单ID: 1-8, 路径: /locationManage");
} else {
return WmsApiResponse.error("菜单添加失败", null);
}
} catch (Exception e) {
return WmsApiResponse.error("添加菜单时发生错误: " + e.getMessage(), null);
}
}
/**
* 检查仓库管理菜单是否存在
*/
@GetMapping("/checkLocationManageMenu")
public WmsApiResponse<TSysMenu> checkLocationManageMenu() {
try {
TSysMenu menu = menuService.getById("1-8");
if (menu != null) {
return success("仓库管理菜单已存在", menu);
} else {
return WmsApiResponse.error("仓库管理菜单不存在", null);
}
} catch (Exception e) {
return WmsApiResponse.error("检查菜单时发生错误: " + e.getMessage(), null);
}
}
}

View File

@ -0,0 +1,10 @@
package com.wms_main.model.dto.request;
import lombok.Data;
@Data
public class LocationStatusUpdateReq {
private String locationId;
private Integer isLock;
private Integer isOccupy;
}

View File

@ -0,0 +1,17 @@
import request from "@/http/request";
export const getLocationList = (params) => {
return request({
url: '/wms/locationManage/list',
method: 'post',
data: params
});
};
export const updateLocationStatus = (data) => {
return request({
url: '/wms/locationManage/updateStatus',
method: 'post',
data
});
};

View File

@ -0,0 +1,428 @@
<template>
<el-form :model="queryForm" inline style="margin-bottom: 16px; padding: 16px; background: #f8f9fa; border-radius: 8px; border: 1px solid #e9ecef; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
<el-form-item label="库位编号" style="margin-right: 16px;">
<el-input v-model="queryForm.locationId" placeholder="库位编号" style="width: 140px;" clearable />
</el-form-item>
<el-form-item label="排" style="margin-right: 16px;">
<el-input v-model="queryForm.lRow" placeholder="排" style="width: 100px;" clearable />
</el-form-item>
<el-form-item label="列" style="margin-right: 16px;">
<el-input v-model="queryForm.lCol" placeholder="列" style="width: 100px;" clearable />
</el-form-item>
<el-form-item label="层" style="margin-right: 16px;">
<el-input v-model="queryForm.lLayer" placeholder="层" style="width: 100px;" clearable />
</el-form-item>
<el-form-item label="深度" style="margin-right: 16px;">
<el-input v-model="queryForm.lDepth" placeholder="深度" style="width: 100px;" clearable />
</el-form-item>
<el-form-item label="锁定状态" style="margin-right: 16px;">
<el-select v-model="queryForm.locked" placeholder="锁定状态" clearable style="width: 120px;">
<el-option label="已锁定" :value="true" />
<el-option label="未锁定" :value="false" />
</el-select>
</el-form-item>
<el-form-item label="占用状态" style="margin-right: 16px;">
<el-select v-model="queryForm.occupied" placeholder="占用状态" clearable style="width: 120px;">
<el-option label="占用" :value="true" />
<el-option label="正常" :value="false" />
</el-select>
</el-form-item>
<el-form-item style="margin-right: 8px;">
<el-button type="primary" @click="search">查询</el-button>
</el-form-item>
<!-- 状态说明 - 第二排 -->
<div style="margin-top: 12px; font-size: 12px; color: #606266; text-align: left;">
<span style="color: #52c41a;">🟢 正常</span> |
<span style="color: #52c41a;">🔒 锁定</span> |
<span style="color: #ff4d4f;">🔴 占用</span>
</div>
</el-form>
<!-- 可视化界面移到表单下方 -->
<div class="location-visual" style="margin-top: 12px;">
<div v-for="row in groupedLocations" :key="row.row" class="row-group">
<div class="row-title">{{ row.row }}</div>
<div class="column-groups">
<div v-for="col in row.columns" :key="col.col" class="column-group">
<div class="column-title">{{ col.col }}</div>
<div class="layer-groups">
<div v-for="layer in col.layers" :key="layer.layer" class="layer-group">
<div class="layer-title">{{ layer.layer }}</div>
<div class="location-grid">
<div v-for="loc in layer.locations" :key="loc.locationId" class="location-cell" @click="showDetail(loc)" style="cursor:pointer;" :class="getLocationClass(loc)">
<div class="location-header">
<span class="location-id">{{ loc.locationId }}</span>
<div class="status-icons">
<span v-if="loc.isLock === 1" class="lock-icon" title="已锁定">🔒</span>
<span v-if="loc.isOccupy === 1" class="occupy-icon" title="占用中">🔴</span>
</div>
</div>
<div class="location-coords">:{{ loc.lRow }} :{{ loc.lCol }} :{{ loc.lLayer }} :{{ loc.lDepth }}</div>
<div class="location-status" :style="getLocationStatusStyle(loc)">{{ getLocationStatus(loc) }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-if="sortedLocationList.length === 0" style="color: #F56C6C; margin: 16px 0; text-align: center;">暂无可视化数据</div>
<div v-if="errorMsg" style="color: #F56C6C; margin: 16px 0; text-align: center;">{{ errorMsg }}</div>
<el-pagination
v-model:current-page="page.current"
v-model:page-size="page.size"
:total="page.total"
:page-sizes="[20, 50, 100, 200]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="search"
@size-change="search"
style="margin: 8px 0;"
/>
<!-- 详情弹窗和编辑弹窗保留 -->
<el-dialog v-model="dialogVisible" title="库位详情" width="350px" :close-on-click-modal="true">
<div v-if="currentLocation">
<div>库位编号{{ currentLocation.locationId }}</div>
<div>{{ currentLocation.lRow }}</div>
<div>{{ currentLocation.lCol }}</div>
<div>{{ currentLocation.lLayer }}</div>
<div>深度{{ currentLocation.lDepth }}</div>
<div>锁定状态<span :style="{color: currentLocation.isLock === 1 ? '#F56C6C' : '#67C23A'}">{{ currentLocation.isLock === 1 ? '已锁定' : '未锁定' }}</span></div>
<div>占用状态<span :style="{color: currentLocation.isOccupy === 1 ? '#F56C6C' : '#67C23A'}">{{ currentLocation.isOccupy === 1 ? '占用' : '正常' }}</span></div>
</div>
<template #footer>
<el-button type="primary" @click="onEdit">编辑</el-button>
<el-button type="danger" @click="onDelete">删除</el-button>
<el-button @click="onToggleLock(currentLocation)">{{ currentLocation && currentLocation.isLock === 1 ? '解锁' : '锁定' }}</el-button>
<el-button @click="onToggleOccupy(currentLocation)">{{ currentLocation && currentLocation.isOccupy === 1 ? '设为正常' : '设为占用' }}</el-button>
<el-button @click="dialogVisible = false">关闭</el-button>
</template>
</el-dialog>
<el-dialog v-model="editDialogVisible" title="编辑库位" width="350px" :close-on-click-modal="false">
<el-form v-if="editForm" :model="editForm" label-width="60px">
<el-form-item label="排"><el-input v-model="editForm.lRow" /></el-form-item>
<el-form-item label="列"><el-input v-model="editForm.lCol" /></el-form-item>
<el-form-item label="层"><el-input v-model="editForm.lLayer" /></el-form-item>
<el-form-item label="深度"><el-input v-model="editForm.lDepth" /></el-form-item>
</el-form>
<template #footer>
<el-button type="primary" @click="onEditSave">保存</el-button>
<el-button @click="editDialogVisible = false">取消</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue';
import { getLocationList, updateLocationStatus } from '@/api/locationManage.js';
const page = reactive({ current: 1, size: 20, total: 0 });
const queryForm = reactive({
locationId: null,
lRow: null,
lCol: null,
lLayer: null,
lDepth: null,
locked: null,
occupied: null
});
const locationList = ref([]);
const errorMsg = ref('');
const dialogVisible = ref(false);
const currentLocation = ref(null);
const editDialogVisible = ref(false);
const editForm = ref(null);
const showDetail = (loc) => {
//
if (loc.isLock === undefined) {
loc.isLock = localStorage.getItem(`location_${loc.locationId}_locked`) === 'true';
}
if (loc.isOccupy === undefined) {
loc.isOccupy = localStorage.getItem(`location_${loc.locationId}_occupied`) === 'true';
}
currentLocation.value = loc;
dialogVisible.value = true;
};
const onEdit = () => {
editForm.value = { ...currentLocation.value };
editDialogVisible.value = true;
};
const onEditSave = () => {
//
Object.assign(currentLocation.value, editForm.value);
editDialogVisible.value = false;
//
};
const onDelete = () => {
if (window.confirm('确定要删除该库位吗?')) {
//
dialogVisible.value = false;
//
}
};
const onToggleLock = async (loc) => {
const newLock = loc.isLock === 1 ? 0 : 1;
await updateLocationStatus({ locationId: loc.locationId, isLock: newLock });
queryForm.locationId = loc.locationId;
page.current = 1;
search();
};
const onToggleOccupy = async (loc) => {
const newOccupy = loc.isOccupy === 1 ? 0 : 1;
await updateLocationStatus({ locationId: loc.locationId, isOccupy: newOccupy });
queryForm.locationId = loc.locationId;
page.current = 1;
search();
};
const search = async () => {
errorMsg.value = '';
try {
//
const params = { ...queryForm, pageNo: page.current, pageSize: page.size };
if (params.locked !== null && params.locked !== undefined) {
params.isLock = params.locked ? 1 : 0;
} else {
delete params.isLock;
}
if (params.occupied !== null && params.occupied !== undefined) {
params.isOccupy = params.occupied ? 1 : 0;
} else {
delete params.isOccupy;
}
//
delete params.locked;
delete params.occupied;
const res = await getLocationList(params);
//
let lists = [];
let total = 0;
if (res.data) {
if (res.data.code === 0 && res.data.data) {
lists = res.data.data.lists || res.data.data.list || [];
total = res.data.data.total || res.data.data.count || 0;
} else if (Array.isArray(res.data)) {
lists = res.data;
total = res.data.length;
}
}
//
lists.forEach(loc => {
if (loc.isLock === undefined) {
loc.isLock = localStorage.getItem(`location_${loc.locationId}_locked`) === 'true';
}
if (loc.isOccupy === undefined) {
loc.isOccupy = localStorage.getItem(`location_${loc.locationId}_occupied`) === 'true';
}
});
locationList.value = lists;
page.total = total;
} catch (e) {
errorMsg.value = '接口请求失败,请检查网络或联系管理员';
locationList.value = [];
page.total = 0;
}
};
onMounted(search);
const sortedLocationList = computed(() => {
return locationList.value.slice().sort((a, b) => {
if (a.lRow !== b.lRow) return a.lRow - b.lRow;
if (a.lCol !== b.lCol) return a.lCol - b.lCol;
if (a.lLayer !== b.lLayer) return a.lLayer - b.lLayer;
return a.lDepth - b.lDepth;
});
});
const groupedLocations = computed(() => {
const groups = {};
sortedLocationList.value.forEach(loc => {
const row = loc.lRow;
const col = loc.lCol;
const layer = loc.lLayer;
if (!groups[row]) {
groups[row] = { row, columns: {} };
}
if (!groups[row].columns[col]) {
groups[row].columns[col] = { col, layers: {} };
}
if (!groups[row].columns[col].layers[layer]) {
groups[row].columns[col].layers[layer] = { layer, locations: [] };
}
groups[row].columns[col].layers[layer].locations.push(loc);
});
//
return Object.values(groups).map(rowGroup => ({
row: rowGroup.row,
columns: Object.values(rowGroup.columns).map(colGroup => ({
col: colGroup.col,
layers: Object.values(colGroup.layers).sort((a, b) => a.layer - b.layer)
})).sort((a, b) => a.col - b.col)
})).sort((a, b) => a.row - b.row);
});
const getLocationClass = (loc) => {
if (loc.isOccupy === 1) return 'location-occupied';
if (loc.isLock === 1) return 'location-locked';
return 'location-normal';
};
const getLocationStatus = (loc) => {
if (loc.isLock === 1 && loc.isOccupy === 1) return '锁定,占用';
if (loc.isOccupy === 1) return '占用';
if (loc.isLock === 1) return '锁定';
return '正常';
};
const getLocationStatusStyle = (loc) => {
if (loc.isLock === 1 || loc.isOccupy === 1) return { color: '#ff4d4f' };
return { color: '#52c41a' };
};
</script>
<style scoped>
.location-cell {
width: 100px;
height: 80px;
border: 1px solid #dcdfe6;
border-radius: 6px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 0;
background: #f5f7fa;
font-size: 13px;
padding: 4px 0;
position: relative;
}
.location-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 0 4px;
margin-bottom: 2px;
}
.location-id {
font-size: 11px;
font-weight: bold;
flex: 1;
text-align: center;
}
.status-icons {
display: flex;
gap: 2px;
position: absolute;
top: 2px;
right: 2px;
}
.lock-icon {
color: #52c41a;
font-size: 12px;
}
.occupy-icon {
color: #ff4d4f;
font-size: 12px;
}
.location-coords {
font-size: 10px;
color: #666;
margin-bottom: 2px;
}
.location-status {
font-size: 10px;
font-weight: bold;
margin-top: 2px;
}
.row-group {
margin-bottom: 20px;
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 12px;
background: #fafafa;
}
.row-title {
font-weight: bold;
font-size: 16px;
color: #303133;
margin-bottom: 8px;
padding: 4px 8px;
background: #e1f3d8;
border-radius: 4px;
display: inline-block;
}
.column-groups {
display: flex;
flex-direction: column;
gap: 16px;
}
.column-group {
border: 1px solid #dcdfe6;
border-radius: 6px;
padding: 8px;
background: white;
width: 100%;
}
.column-title {
font-weight: bold;
font-size: 14px;
color: #606266;
margin-bottom: 8px;
text-align: left;
}
.layer-groups {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 4px;
}
.layer-group {
border: 1px solid #f0f0f0;
border-radius: 4px;
padding: 4px;
background: #fafafa;
width: 100px;
}
.layer-title {
font-weight: bold;
font-size: 11px;
color: #909399;
margin-bottom: 4px;
text-align: left;
padding: 1px 4px;
background: #f5f7fa;
border-radius: 2px;
display: inline-block;
}
.location-grid {
display: flex;
flex-wrap: wrap;
gap: 2px;
justify-content: flex-start;
}
.location-normal {
background: #e8f5e8;
border-color: #52c41a;
color: #52c41a;
box-shadow: 0 2px 4px rgba(82, 196, 26, 0.2);
}
.location-locked {
background: #e8f5e8;
border-color: #52c41a;
color: #52c41a;
box-shadow: 0 2px 4px rgba(82, 196, 26, 0.2);
}
.location-occupied {
background: #fff2f0;
border-color: #ff4d4f;
color: #ff4d4f;
box-shadow: 0 2px 4px rgba(255, 77, 79, 0.2);
}
</style>