Browse Source

2026-04-07

pda生产领料模块增加【替代】功能
master
fengyuan_yang 2 weeks ago
parent
commit
727b39c26e
  1. 5
      src/api/production.js
  2. 152
      src/views/modules/production-pick/productionPickingDetail.vue
  3. 235
      src/views/modules/sys/role-add-or-update.vue

5
src/api/production.js

@ -30,6 +30,11 @@ export const confirmProductionPicking = data => createAPI(`production/outbound/c
*/
export const getOutboundMaterialList = data => createAPI(`production/outbound/materialList`, 'post', data)
/**
* 获取 BOM 替代料清单存储过程 GetBomAlternativePartDetails
*/
export const getBomAlternativePartDetails = data => createAPI(`production/outbound/bomAlternativePartDetails`, 'post', data)
/**
* 获取出库单物料明细列表
* @param {Object} data - 查询参数 {site, outboundNo}

152
src/views/modules/production-pick/productionPickingDetail.vue

@ -141,6 +141,7 @@
<div class="col-required-qty">需求数量</div>
<div class="col-picked-qty">已领数量</div>
<div class="col-picked-qty">扫描数量</div>
<div class="col-action">操作</div>
</div>
<div class="table-body">
@ -157,6 +158,9 @@
<div class="col-required-qty">{{ item.requiredQty || 0 }}</div>
<div class="col-picked-qty">{{ item.pickedQty || 0 }}</div>
<div class="col-picked-qty">{{ item.scansQty || 0 }}</div>
<div class="col-action">
<span class="row-action-link" @click.stop="openAlternativeDialog(item)">替代</span>
</div>
</div>
</div>
</div>
@ -173,13 +177,67 @@
<button class="btn-close" @click="closeMaterialDialog">关闭</button>
</div>
</div>
</div>
<!-- 替代料弹窗布局与物料清单一致 -->
<div v-if="showAlternativeDialog" class="alternative-material-overlay">
<div class="material-modal">
<div class="modal-header">
<span class="modal-title">替代料</span>
<i class="el-icon-close close-btn" @click="closeAlternativeDialog"></i>
</div>
<div class="modal-body">
<div v-if="alternativeListLoading" class="loading-container">
<i class="el-icon-loading"></i>
<span>加载中...</span>
</div>
<div v-else-if="alternativeMaterialList.length > 0" class="material-table">
<div class="table-header">
<div class="col-no">NO.</div>
<div class="col-material-code">物料编码</div>
<div class="col-part-name">物料名称</div>
<div class="col-required-qty">需求数量</div>
<div class="col-picked-qty">已领数量</div>
<div class="col-picked-qty">扫描数量</div>
</div>
<div class="table-body">
<div
v-for="(item, index) in alternativeMaterialList"
:key="index"
class="table-row"
>
<div class="col-no">{{ index + 1 }}</div>
<div class="col-material-code clickable-part" @click="showStockDialogFn(item)">{{ item.materialCode || item.partNo }}</div>
<div class="col-part-name">
<span class="part-name-text" @click.stop="showPartNameTip(item.materialName, $event)">{{ item.materialName || '-' }}</span>
</div>
<div class="col-required-qty">{{ item.requiredQty || 0 }}</div>
<div class="col-picked-qty">{{ item.pickedQty || 0 }}</div>
<div class="col-picked-qty">{{ item.scansQty || 0 }}</div>
</div>
</div>
</div>
<!-- 物料名称提示框 -->
<div v-if="showPartNameTooltip" class="part-name-tooltip" :style="tooltipStyle" @click.stop>
<div class="tooltip-content">{{ currentPartName }}</div>
<div v-else class="empty-material">
<i class="el-icon-document"></i>
<p>暂无物料数据</p>
</div>
</div>
<div class="modal-footer">
<button class="btn-close" @click="closeAlternativeDialog">关闭</button>
</div>
</div>
</div>
<!-- 物料名称提示框主清单 / 替代料共用 -->
<div v-if="showPartNameTooltip" class="part-name-tooltip" :style="tooltipStyle" @click.stop>
<div class="tooltip-content">{{ currentPartName }}</div>
</div>
<!-- 可用库存弹窗 -->
<div v-if="showStockDialog" class="stock-overlay">
<div class="stock-modal">
@ -216,7 +274,7 @@
</template>
<script>
import { getOutboundDetails, validateLabelWithOutbound, confirmProductionPicking, getOutboundMaterialList, getScannedLabelList } from "@/api/production.js";
import { getOutboundDetails, validateLabelWithOutbound, confirmProductionPicking, getOutboundMaterialList, getBomAlternativePartDetails, getScannedLabelList } from "@/api/production.js";
import { getInventoryStock } from "@/api/inbound.js";
import { getCurrentWarehouse } from '@/utils'
import moment from 'moment';
@ -232,6 +290,10 @@ export default {
showMaterialDialog: false,
materialList: [],
materialListLoading: false,
showAlternativeDialog: false,
alternativeMaterialList: [],
alternativeListLoading: false,
substituteSourcePartNo: '',
isRemoveMode: false, //
relatedNo: '',
showStockDialog: false,
@ -413,6 +475,53 @@ export default {
this.hidePartNameTip();
},
openAlternativeDialog(rowItem) {
this.substituteSourcePartNo = rowItem.materialCode || rowItem.partNo;
if (!this.substituteSourcePartNo) {
this.$message.error('无法识别物料编码');
return;
}
this.showAlternativeDialog = true;
this.loadAlternativeMaterialList();
},
loadAlternativeMaterialList() {
if (!this.outboundInfo.site || !this.buNo || !this.outboundNo || !this.substituteSourcePartNo) {
this.$message.error('缺少必要参数,无法获取替代料');
return;
}
this.alternativeListLoading = true;
const params = {
site: this.outboundInfo.site,
buNo: this.buNo,
outboundNo: this.outboundNo,
warehouseId: getCurrentWarehouse(),
relatedNo: this.relatedNo,
partNo: this.substituteSourcePartNo
};
getBomAlternativePartDetails(params).then(({ data }) => {
this.alternativeListLoading = false;
if (data && data.code === 0) {
this.alternativeMaterialList = data.data || [];
} else {
this.$message.error(data.msg || '获取替代料失败');
this.alternativeMaterialList = [];
}
}).catch(error => {
this.alternativeListLoading = false;
console.error('获取替代料失败:', error);
this.$message.error('获取替代料失败');
this.alternativeMaterialList = [];
});
},
closeAlternativeDialog() {
this.showAlternativeDialog = false;
this.hidePartNameTip();
this.alternativeMaterialList = [];
this.substituteSourcePartNo = '';
},
//
showPartNameTip(partName, event) {
if (!partName || partName === '-') return;
@ -457,7 +566,7 @@ export default {
orderNo: this.outboundInfo.relatedNo,
orderLineNo: '',
partNo: partNo,
warehouseId: localStorage.getItem('warehouseId') || ''
warehouseId: getCurrentWarehouse() || ''
};
getInventoryStock(params).then(({ data }) => {
this.stockLoading = false;
@ -1053,7 +1162,36 @@ export default {
.clickable-part { color: #17B3A3; font-weight: 500; cursor: pointer; text-decoration: underline; }
.clickable-part:hover { color: #0d8f7f; }
.stock-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); z-index: 10000; display: flex; align-items: center; justify-content: center; padding: 20px; }
.alternative-material-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 10040;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.material-table .col-action {
flex: 0.55;
text-align: center;
min-width: 44px;
font-size: 12px;
flex-shrink: 0;
}
.row-action-link {
color: #17B3A3;
font-weight: 500;
cursor: pointer;
text-decoration: underline;
}
.row-action-link:active {
color: #0d8f7f;
}
.stock-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); z-index: 10050; display: flex; align-items: center; justify-content: center; padding: 20px; }
.stock-modal { background: white; border-radius: 12px; width: 100%; max-width: 800px; max-height: 80vh; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); overflow: hidden; display: flex; flex-direction: column; }
.stock-modal .modal-header { background: #17B3A3; color: white; padding: 5px 16px; display: flex; justify-content: space-between; align-items: center; min-height: 28px; }
.stock-modal .modal-body { flex: 1; overflow: auto; padding: 0; }
@ -1113,7 +1251,7 @@ export default {
/* 物料名称提示框样式 */
.part-name-tooltip {
position: fixed;
z-index: 10001;
z-index: 10060;
transform: translateY(-100%);
max-width: 200px;
animation: tooltipFadeIn 0.2s ease;

235
src/views/modules/sys/role-add-or-update.vue

@ -1,37 +1,70 @@
<template>
<el-dialog
class="sl-menu-item"
width="500px"
class="sl-menu-item custom-role-dialog"
width="800px"
:title="!dataForm.id ? buttons.add :buttons.edit "
:close-on-click-modal="false"
:visible.sync="visible">
<el-form :model="dataForm" :rules="dataRule" ref="dataForm" @keyup.enter.native="dataFormSubmit()"
label-width="80px">
<el-form-item :label="buttons.roleName||'角色名称'" prop="roleName">
<el-input style="width: 150px" v-model="dataForm.roleName"></el-input>
</el-form-item>
<el-form-item :label="buttons.remark||'备注'" prop="remark">
<el-input style="width: 150px" v-model="dataForm.remark" placeholder="备注"></el-input>
</el-form-item>
<el-form-item size="mini" :label="buttons.authorize||'授权'">
<el-row>
<el-col class="down-tree" :span="14">
<el-tree
:data="menuList"
:props="menuListTreeProps"
node-key="menuId"
ref="menuListTree"
:default-expand-all="false"
show-checkbox>
</el-tree>
<div class="form-section">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="buttons.roleName||'角色名称'" prop="roleName">
<el-input style="width: 100%" v-model="dataForm.roleName" placeholder="请输入角色名称"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="buttons.remark||'备注'" prop="remark">
<el-input style="width: 100%" v-model="dataForm.remark" placeholder="请输入备注信息"></el-input>
</el-form-item>
</el-col>
</el-row>
</div>
<div class="auth-section">
<el-form-item size="mini" :label="buttons.authorize||'权限授权'">
<el-row>
<el-col :span="24">
<el-input
placeholder="输入菜单或按钮名称进行过滤"
v-model="filterText"
clearable
prefix-icon="el-icon-search"
@keyup.enter.native.stop
class="filter-input">
</el-input>
</el-col>
</el-row>
<el-row>
<el-col class="down-tree" :span="24">
<el-tree
:data="menuList"
:props="menuListTreeProps"
node-key="menuId"
ref="menuListTree"
:default-expand-all="false"
:filter-node-method="filterNode"
@check="handleTreeCheck"
show-checkbox>
<span class="custom-tree-node" slot-scope="{ node, data }" style="display: flex; align-items: center; width: 100%;">
<span style="margin-right: 20px; white-space: nowrap;">{{ node.label }}</span>
<span v-if="data.buttonList && data.buttonList.length > 0" style="flex: 1; overflow-x: auto;">
<el-checkbox-group v-model="data.checkedButtons" @change="handleButtonChange(data)" @click.native.stop style="display: inline-block;">
<el-checkbox v-for="btn in data.buttonList" :key="btn.menuId" :label="Number(btn.menuId)">{{ btn.name }}</el-checkbox>
</el-checkbox-group>
</span>
</span>
</el-tree>
</el-col>
</el-row>
</el-form-item>
</el-form-item>
</div>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="visible = false">{{ buttons.close || '关闭' }}</el-button>
<el-button type="primary" @click="dataFormSubmit()">{{ buttons.submit || '确定' }}</el-button>
<el-button type="primary" @click="visible = false">{{ buttons.close || '关闭' }}</el-button>
</span>
</el-dialog>
</template>
@ -45,6 +78,7 @@ export default {
data() {
return {
visible: false,
filterText: '',
menuList: [],
menuListTreeProps: {
label: 'name',
@ -80,8 +114,82 @@ export default {
tempKey: -666666 // key, tree. #
}
},
watch: {
filterText(val) {
this.$refs.menuListTree.filter(val);
}
},
methods: {
filterNode(value, data) {
if (!value) return true;
let matchButton = data.buttonList && data.buttonList.some(btn => btn.name.indexOf(value) !== -1);
return data.name.indexOf(value) !== -1 || matchButton;
},
processMenuTree(treeList) {
for (let i = 0; i < treeList.length; i++) {
let node = treeList[i]
this.$set(node, 'buttonList', [])
this.$set(node, 'checkedButtons', [])
if (node.children && node.children.length > 0) {
let normalChildren = []
for (let child of node.children) {
if (child.type === 2) {
node.buttonList.push(child)
} else {
normalChildren.push(child)
}
}
node.children = normalChildren
this.processMenuTree(node.children)
}
}
},
populateCheckedButtons(treeList, checkedIds, treeCheckedKeys) {
for (let node of treeList) {
if (checkedIds.includes(Number(node.menuId))) {
if (!node.children || node.children.length === 0) {
treeCheckedKeys.push(Number(node.menuId))
}
}
if (node.buttonList && node.buttonList.length > 0) {
node.checkedButtons = node.buttonList
.filter(btn => checkedIds.includes(Number(btn.menuId)))
.map(btn => Number(btn.menuId))
}
if (node.children) {
this.populateCheckedButtons(node.children, checkedIds, treeCheckedKeys)
}
}
},
handleTreeCheck(data, checkedInfo) {
let isChecked = checkedInfo.checkedKeys.includes(data.menuId)
this.toggleButtons(data, isChecked)
},
toggleButtons(node, isChecked) {
if (isChecked) {
node.checkedButtons = node.buttonList ? node.buttonList.map(b => Number(b.menuId)) : []
} else {
node.checkedButtons = []
}
if (node.children) {
node.children.forEach(child => this.toggleButtons(child, isChecked))
}
},
handleButtonChange(data) {
if (data.checkedButtons.length > 0) {
this.$refs.menuListTree.setChecked(data.menuId, true, false)
}
},
getAllCheckedButtons(treeList, result) {
for (let node of treeList) {
if (node.checkedButtons && node.checkedButtons.length > 0) {
result.push(...node.checkedButtons)
}
if (node.children) {
this.getAllCheckedButtons(node.children, result)
}
}
},
init(id) {
this.getFunctionButtonList()
this.dataForm.id = id || 0
@ -90,7 +198,9 @@ export default {
method: 'get',
params: this.$http.adornParams()
}).then(({data}) => {
this.menuList = treeDataTranslate(data, 'menuId')
let treeData = treeDataTranslate(data, 'menuId')
this.processMenuTree(treeData)
this.menuList = treeData
}).then(() => {
this.visible = true
this.$nextTick(() => {
@ -114,10 +224,10 @@ export default {
console.log(data.role.menuIdList)
let x1 = data.role.menuIdList.map(Number)
console.log(x1)
let treeCheckedKeys = []
this.populateCheckedButtons(this.menuList, x1, treeCheckedKeys)
this.$nextTick(() => {
for (let x1Element of x1) {
this.$refs.menuListTree.setChecked(x1Element, true, false)
}
this.$refs.menuListTree.setCheckedKeys(treeCheckedKeys)
})
}
})
@ -134,14 +244,14 @@ export default {
dataFormSubmit() {
this.$refs['dataForm'].validate((valid) => {
if (valid) {
// let temp = {
// readOnly: 'N',
// canDelete: 'N',
// canModified: 'N',
// fullControl: 'N',
// menuId: this.tempKey
// }
// let menusList = [].concat(this.$refs.menuListTree.getCheckedNodes(), [temp], this.$refs.menuListTree.getHalfCheckedNodes())
let buttonIds = []
this.getAllCheckedButtons(this.menuList, buttonIds)
let treeKeys = this.$refs.menuListTree.getCheckedKeys()
let halfKeys = this.$refs.menuListTree.getHalfCheckedKeys()
let finalMenuIds = [...new Set([...treeKeys, ...halfKeys, ...buttonIds])]
if (this.tempKey) {
finalMenuIds.push(this.tempKey)
}
this.$http({
url: this.$http.adornUrl(`/sys/role/${!this.dataForm.id ? 'save' : 'update'}`),
method: 'post',
@ -149,8 +259,7 @@ export default {
'roleId': this.dataForm.id || undefined,
'roleName': this.dataForm.roleName,
'remark': this.dataForm.remark,
'menuIdList': [].concat(this.$refs.menuListTree.getCheckedKeys(), [this.tempKey], this.$refs.menuListTree.getHalfCheckedKeys())
// 'menuIdList': menusList
'menuIdList': finalMenuIds
})
}).then(({data}) => {
if (data && data.code === 0) {
@ -186,13 +295,51 @@ export default {
</script>
<style lang="scss">
.down-tree {
height: 300px;
display: block;
overflow-y: scroll;
.custom-role-dialog {
.el-dialog__body {
padding: 20px 30px;
}
.form-section {
margin-bottom: 10px;
}
.auth-section {
border-top: 1px solid #ebeef5;
padding-top: 20px;
}
.filter-input {
margin-bottom: 15px;
}
.down-tree {
height: 400px;
display: block;
overflow-y: auto;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 10px;
background-color: #fafafa;
}
.el-tree {
background-color: transparent;
}
.custom-tree-node {
.el-checkbox {
margin-right: 15px;
}
.el-checkbox__label {
padding-left: 5px;
color: #606266;
}
}
/* 优化树形节点高度和样式 */
.el-tree-node__content {
height: auto;
min-height: 36px;
padding-top: 4px;
padding-bottom: 4px;
}
.el-tree-node__content:hover {
background-color: #f0f7ff;
}
}
</style>
Loading…
Cancel
Save