You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1276 lines
42 KiB

<template>
<div class="pda-container">
<div class="header-bar">
<div class="header-left" :class="{ 'header-action-disabled': interfaceBusy }" @click="handleBack">
<i class="el-icon-arrow-left"></i>
<span>直接组盘</span>
</div>
<div class="header-right" :class="{ 'header-action-disabled': interfaceBusy }" @click="goHome">首页</div>
</div>
<div class="table-body" style="max-height: 500px; overflow-y: auto;">
<div class="main-content form-section">
<div class="input-group">
<label class="input-label">栈板编码</label>
<div style="display: flex; gap: 8px;">
<el-input
v-model="palletCode"
placeholder="请扫描栈板编码"
class="form-input"
style="flex: 0.75;"
clearable
inputmode="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
@keyup.enter.native="handlePalletScan"
ref="palletInput"
/>
<button class="action-btn secondary" style="flex: 0.25; margin: 0;" @click="handleCallPallet" :disabled="interfaceBusy">
Call栈板
</button>
</div>
</div>
<div v-if="palletScanned" class="input-group">
<div style="display: flex; gap: 8px; align-items: end;">
<div style="flex: 0.65;">
<label class="input-label">栈板类型</label>
<el-select
v-model="currentPalletType"
placeholder="请选择栈板类型"
style="width: 100%;"
:disabled="palletTypeDisabled || interfaceBusy"
@change="handlePalletTypeChange"
>
<el-option
v-for="type in palletTypeOptions"
:key="type.palletType"
:label="`${type.palletType} - ${type.typeDesc}`"
:value="type.palletType"
/>
</el-select>
</div>
<div style="flex: 0.35;">
<label class="input-label">自动分拣</label>
<el-select
v-model="currentAutoSort"
placeholder="请选择"
style="width: 100%;"
:disabled="autoSortDisabled || interfaceBusy"
@change="handleAutoSortChange"
>
<el-option label="是" value="Y"></el-option>
<el-option label="否" value="N"></el-option>
</el-select>
</div>
</div>
</div>
<div v-if="palletScanned" class="input-group">
<el-checkbox
v-model="forceFullPalletOut"
:disabled="palletTypeDisabled || interfaceBusy"
@change="handleForceFullPalletOutChange"
style="width: 100%;"
>
是否强制整托出库
</el-checkbox>
</div>
<div v-if="palletScanned" class="input-group">
<label class="input-label">当前站点</label>
<div style="display: flex; gap: 8px;">
<el-input v-model="currentPalletStation" placeholder="栈板当前站点" class="form-input" readonly style="flex: 0.75;" />
<button class="action-btn secondary" style="flex: 0.25; margin: 0; white-space: nowrap;" @click="refreshTable" :disabled="interfaceBusy">
刷新
</button>
</div>
</div>
<div v-if="palletScanned" class="input-group">
<div style="display: flex; gap: 8px; align-items: center;">
<div style="flex: 0.75;">
<el-radio-group v-model="operationType" style="display: flex;" :disabled="interfaceBusy">
<el-radio label="in" style="margin-right: 0px; white-space: nowrap;">扫进</el-radio>
<el-radio label="out" style="white-space: nowrap;">扫出</el-radio>
</el-radio-group>
</div>
<button class="action-btn secondary" style="flex: 0.25; margin: 0; white-space: nowrap;" :disabled="interfaceBusy || currentPalletType === ''" @click="showScanModal">
扫描条码
</button>
</div>
</div>
</div>
<div v-if="palletScanned" class="rma-list">
<div class="list-title-row" style="display: flex; gap: 8px; align-items: center; padding: 0;">
<div class="list-title" style="flex: 0.4; margin: 0;">
<button class="action-btn primary" style="margin: 0;" @click="showDetailModal" :disabled="interfaceBusy">
明细
</button>
</div>
<button class="action-btn warning" style="flex: 0.3; margin: 0;" @click="showScanOutModal" :disabled="interfaceBusy || scanOutList.length === 0">
扫出({{ scanOutList.length }})
</button>
<!-- 与海安组盘入库唯一差异:此处改为“推送组盘”,并且只允许area/location均为空的托盘触发 - rqrq -->
<button class="action-btn secondary" style="flex: 0.3; margin: 0;" @click="openPushAssemblyDialog" :disabled="interfaceBusy || !canPushAssembly">
{{ pushAssemblyLoading ? '处理中...' : '推送组盘' }}
</button>
</div>
<div class="list-title" style="flex: 0.5; margin: 0;">
<label style="margin-left: 5px;">条码数:{{ detailList.length }}</label>
</div>
<div class="detail-table">
<div class="table-header">
<div class="col-position">位置</div>
<div class="col-layer">层数</div>
<div class="col-serial">标签号</div>
</div>
<div v-for="(detail, index) in detailList" :key="index" class="table-row">
<div class="col-position">{{ detail.position }}</div>
<div class="col-layer">{{ detail.layer }}</div>
<div class="col-serial">{{ detail.serialNo }}</div>
</div>
<div v-if="detailList.length === 0" class="table-row empty-row">
<div class="empty-hint">暂无栈板明细数据</div>
</div>
</div>
</div>
</div>
<el-dialog
:title="'已扫描数量:' + detailList.length"
:visible.sync="detailModalVisible"
width="90%"
:close-on-click-modal="false"
:show-close="false"
:append-to-body="true"
>
<div class="table-body" style="max-height: 240px; overflow-y: auto;">
<div class="detail-table">
<div class="table-header">
<div class="col-position">位置</div>
<div class="col-layer">层数</div>
<div class="col-serial">标签号</div>
</div>
<div v-for="(detail, index) in detailList" :key="index" class="table-row">
<div class="col-position">{{ detail.position }}</div>
<div class="col-layer">{{ detail.layer }}</div>
<div class="col-serial">{{ detail.serialNo }}</div>
</div>
<div v-if="detailList.length === 0" class="table-row empty-row">
<div class="empty-hint">暂无栈板明细数据</div>
</div>
</div>
</div>
<div slot="footer" class="dialog-footer">
<button class="action-btn secondary" style="margin-left: 10px;" @click="detailModalVisible = false" :disabled="interfaceBusy">取消</button>
</div>
</el-dialog>
<el-dialog
:title="'扫出记录 (共' + scanOutList.length + '条)'"
:visible.sync="scanOutModalVisible"
width="90%"
:close-on-click-modal="false"
:show-close="false"
:append-to-body="true"
>
<div class="table-body" style="max-height: 300px; overflow-y: auto;">
<div class="detail-table">
<div class="table-header">
<div class="col-position">位置</div>
<div class="col-layer">层数</div>
<div class="col-serial">标签号</div>
</div>
<div v-for="(detail, index) in scanOutList" :key="index" class="table-row">
<div class="col-position">{{ detail.position }}</div>
<div class="col-layer">{{ detail.layer }}</div>
<div class="col-serial">{{ detail.serialNo }}</div>
</div>
<div v-if="scanOutList.length === 0" class="table-row empty-row">
<div class="empty-hint">暂无扫出记录</div>
</div>
</div>
</div>
<div slot="footer" class="dialog-footer">
<button class="action-btn secondary" @click="scanOutModalVisible = false" :disabled="interfaceBusy">关闭</button>
</div>
</el-dialog>
<!-- 扫码模态框:与海安组盘入库保持一致 - rqrq -->
<el-dialog
title="扫描标签"
:visible.sync="scanModalVisible"
width="90%"
:close-on-click-modal="false"
:show-close="false"
:modal="true"
:modal-append-to-body="true"
:append-to-body="true"
>
<div class="scan-modal-content">
<div v-if="operationType === 'in'" class="modal-form">
<div v-if="!currentMixedMode && scanLayerOptions.length > 0" class="input-group">
<label class="input-label">层数(当前选择:第{{ scanLayer }}层)</label>
<div class="layer-grid">
<div
v-for="layer in scanLayerOptions"
:key="layer"
class="layer-item"
:class="{ 'layer-selected': scanLayer === layer }"
@click="handleLayerClick(layer)"
>
{{ layer }}
</div>
</div>
</div>
<div class="input-group">
<label class="input-label">
位置{{ currentSelectedPosition ? `(当前选择:${currentSelectedPosition})` : '' }}
<span v-if="positionGridLoading" style="color: #909399; font-size: 12px;">(加载中...)</span>
</label>
<div class="position-grid" :class="[{ 'position-grid-loading': positionGridLoading }, getPositionGridClass]">
<div
v-for="(item, index) in displayPositionGrid"
:key="index"
class="position-item"
:class="{
'position-disabled': positionGridLoading || !availablePositions.includes(item.position),
'position-selected': currentSelectedPosition === item.position,
'position-loading': positionGridLoading,
'position-placeholder': item.isPlaceholder
}"
@click="handlePositionClick(item.position)"
>
<template v-if="!item.isPlaceholder">{{ item.position }}({{ item.count }})</template>
</div>
</div>
</div>
</div>
<div class="input-group">
<label class="input-label">标签二维码</label>
<el-input
ref="scanInput"
v-model="scanCode"
placeholder="请扫描标签二维码"
class="form-input"
clearable
:disabled="operationType === 'in' && !currentSelectedPosition"
@keyup.enter.native="handleLabelScan"
inputmode="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
/>
</div>
</div>
<div slot="footer" class="dialog-footer">
<button class="action-btn secondary" style="margin-left: 10px;" @click="closeScanModal" :disabled="interfaceBusy">取消</button>
</div>
</el-dialog>
<!-- 调用空托盘:与海安组盘入库保持一致 - rqrq -->
<el-dialog
title="调用空托盘"
:visible.sync="callEmptyModalVisible"
width="90%"
:close-on-click-modal="false"
:show-close="false"
:append-to-body="true"
>
<div class="input-group">
<label class="input-label">区域</label>
<el-select v-model="callEmptyForm.areaId" placeholder="请选择区域" style="width: 100%;" @change="handleAreaChangeForCall">
<el-option v-for="a in areaOptions" :key="a.areaId" :label="a.areaDesc" :value="a.areaId" />
</el-select>
</div>
<div class="input-group">
<label class="input-label">站点</label>
<el-select v-model="callEmptyForm.stationCode" placeholder="请选择站点" style="width: 100%;" :disabled="!callEmptyForm.areaId">
<el-option v-for="s in stationOptions" :key="s.stationCode" :label="`${s.stationCode} (${s.status || '-'})`" :value="s.stationCode" />
</el-select>
</div>
<div class="input-group">
<label class="input-label">栈板类型</label>
<el-select v-model="callEmptyForm.palletType" placeholder="请选择栈板类型" style="width: 100%;">
<el-option v-for="t in callEmptyPalletTypeOptions" :key="t.palletType" :label="`${t.palletType} - ${t.typeDesc}`" :value="t.palletType" />
</el-select>
</div>
<div slot="footer" class="dialog-footer">
<button class="action-btn primary" @click="submitCallEmpty" :disabled="interfaceBusy || callEmptyLoading">{{ callEmptyLoading ? '处理中...' : '确认' }}</button>
<button class="action-btn secondary" style="margin-left: 8px;" @click="callEmptyModalVisible = false" :disabled="interfaceBusy || callEmptyLoading">取消</button>
</div>
</el-dialog>
<!-- 直接组盘推送弹窗:必须选择入库口1/入库口2 - rqrq -->
<el-dialog
title="请选择入库口"
:visible.sync="pushAssemblyDialogVisible"
width="90%"
:close-on-click-modal="false"
:show-close="false"
:append-to-body="true"
>
<div class="input-group">
<label class="input-label">入库口</label>
<el-select v-model="selectedInboundPort" placeholder="请选择入库口" style="width: 100%;">
<el-option label="入库口1" value="INBOUND_PORT_1"></el-option>
<el-option label="入库口2" value="INBOUND_PORT_2"></el-option>
</el-select>
</div>
<div slot="footer" class="dialog-footer">
<button class="action-btn primary" @click="submitPushAssembly" :disabled="interfaceBusy || !selectedInboundPort">
{{ pushAssemblyLoading ? '处理中...' : '确认推送' }}
</button>
<button class="action-btn secondary" style="margin-left: 8px;" @click="closePushAssemblyDialog" :disabled="interfaceBusy">取消</button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
checkPalletExists,
getPalletDetails,
savePalletDetail,
deletePalletDetail,
getPalletInfo,
getPalletTypeList,
updatePalletTypeAndAutoSort,
getAvailablePositionsForLayer,
callEmptyPalletToStation,
getAreaOptionsForChange,
getStationsByArea,
pushPalletAssemblyToInboundPort
} from '../../../api/haianAutoWarehouse/palletAssembly'
export default {
name: 'haianPalletAssembly',
data() {
return {
site: this.$store.state.user.site,
palletCode: '',
palletScanned: false,
operationType: 'in',
currentPalletFamily: '',
currentPalletType: '',
currentAutoSort: 'N',
currentPalletStation: '',
forceFullPalletOut: false,
palletTypeDisabled: false,
autoSortDisabled: false,
currentWcsAutoSort: '',
currentMaxLayer: 0,
positionOptions: [],
palletTypeOptions: [],
callEmptyPalletTypeOptions: [],
detailList: [],
scanOutList: [],
detailModalVisible: false,
scanOutModalVisible: false,
palletInfo: {
palletType: '',
autoSort: 'N',
locationCode: '',
area: ''
},
scanModalVisible: false,
scanCode: '',
scanPosition: '',
scanLayer: 1,
scanLayerOptions: [],
needRefreshOnClose: false,
positionGrid: [],
availablePositions: [],
currentSelectedPosition: '',
currentMixedMode: false,
positionGridLoading: false,
areaOptions: [],
stationOptions: [],
callEmptyModalVisible: false,
callEmptyLoading: false,
callEmptyForm: {
areaId: '',
stationCode: '',
palletType: ''
},
// 推送组盘弹窗状态 - rqrq
pushAssemblyDialogVisible: false,
selectedInboundPort: '',
// 接口锁状态:推送组盘时要禁用页面所有按钮 - rqrq
palletScanLoading: false,
palletConfigLoading: false,
scanRequestLoading: false,
pushAssemblyLoading: false
}
},
computed: {
getPositionGridClass() {
const len = this.positionGrid.length
if (len === 1) return 'position-grid-1'
if (len === 2) return 'position-grid-4'
if (len === 4) return 'position-grid-4'
return 'position-grid-9'
},
displayPositionGrid() {
const len = this.positionGrid.length
if (len === 2) {
const pos1 = this.positionGrid.find((p) => p.position === '1') || { position: '1', count: 0 }
const pos2 = this.positionGrid.find((p) => p.position === '2') || { position: '2', count: 0 }
return [
{ ...pos1, isPlaceholder: false },
{ position: '', count: 0, isPlaceholder: true },
{ position: '', count: 0, isPlaceholder: true },
{ ...pos2, isPlaceholder: false }
]
}
return this.positionGrid.map((p) => ({ ...p, isPlaceholder: false }))
},
// 直接组盘规则:area与location_code同时为空时才允许推送 - rqrq
canPushAssembly() {
if (!this.palletScanned) {
return false
}
if (!this.detailList || this.detailList.length === 0) {
return false
}
const area = this.palletInfo && this.palletInfo.area ? String(this.palletInfo.area).trim() : ''
const locationCode = this.palletInfo && this.palletInfo.locationCode ? String(this.palletInfo.locationCode).trim() : ''
return area === '' && locationCode === ''
},
interfaceBusy() {
return this.palletScanLoading || this.palletConfigLoading || this.scanRequestLoading || this.callEmptyLoading || this.pushAssemblyLoading
}
},
methods: {
ensureSite() {
if (!this.site) {
this.$alert('当前用户缺少site,无法执行海安组盘业务', '提示', { confirmButtonText: '确定' })
return false
}
return true
},
handleBack() {
if (this.interfaceBusy) {
return
}
this.resetPage(false)
this.$router.back()
},
goHome() {
if (this.interfaceBusy) {
return
}
this.resetPage(false)
this.$router.push({ path: '/' })
},
resetPage(needFocus = true) {
this.site = this.$store.state.user.site
this.palletCode = ''
this.palletScanned = false
this.operationType = 'in'
this.currentPalletFamily = ''
this.currentPalletType = ''
this.currentAutoSort = 'N'
this.currentPalletStation = ''
this.forceFullPalletOut = false
this.palletTypeDisabled = false
this.autoSortDisabled = false
this.currentWcsAutoSort = ''
this.currentMaxLayer = 0
this.positionOptions = []
this.palletTypeOptions = []
this.callEmptyPalletTypeOptions = []
this.detailList = []
this.scanOutList = []
this.detailModalVisible = false
this.scanOutModalVisible = false
this.palletInfo = {
palletType: '',
autoSort: 'N',
locationCode: '',
area: ''
}
this.scanModalVisible = false
this.scanCode = ''
this.scanPosition = ''
this.scanLayer = 1
this.scanLayerOptions = []
this.needRefreshOnClose = false
this.positionGrid = []
this.availablePositions = []
this.currentSelectedPosition = ''
this.currentMixedMode = false
this.positionGridLoading = false
this.areaOptions = []
this.stationOptions = []
this.callEmptyModalVisible = false
this.callEmptyLoading = false
this.callEmptyForm = {
areaId: '',
stationCode: '',
palletType: ''
}
this.pushAssemblyDialogVisible = false
this.selectedInboundPort = ''
this.palletScanLoading = false
this.palletConfigLoading = false
this.scanRequestLoading = false
this.pushAssemblyLoading = false
if (needFocus) {
this.$nextTick(() => {
if (this.$refs.palletInput) {
this.$refs.palletInput.focus()
}
})
}
},
handlePalletScan() {
if (!this.ensureSite() || this.interfaceBusy) {
return
}
if (!this.palletCode || !this.palletCode.trim()) {
this.$alert('请输入栈板编码', '提示', { confirmButtonText: '确定' })
return
}
this.palletScanLoading = true
checkPalletExists({
site: this.site,
palletId: this.palletCode.trim()
}).then(({ data }) => {
if (data && data.code === 0) {
this.palletCode = data.palletId
this.positionOptions = data.positions || []
this.palletScanned = true
this.scanOutList = []
this.scanCode = ''
this.scanPosition = ''
this.scanLayer = 1
this.currentSelectedPosition = ''
this.loadPalletInfo()
this.refreshTable()
} else {
this.$alert((data && data.msg) || '栈板校验失败', '错误', { confirmButtonText: '确定' })
}
}).catch((error) => {
this.$alert(error.message || '栈板校验失败', '错误', { confirmButtonText: '确定' })
}).finally(() => {
this.palletScanLoading = false
})
},
loadPalletInfo() {
getPalletInfo({
site: this.site,
palletId: this.palletCode
}).then(({ data }) => {
if (data && data.code === 0) {
const row = data.row || {}
this.currentPalletFamily = row.palletFamily || ''
this.currentPalletType = row.palletType || ''
this.currentAutoSort = row.autoSort || 'N'
this.currentPalletStation = row.locationCode || ''
this.forceFullPalletOut = Number(row.soreType) === 3
this.palletInfo.palletType = this.currentPalletType
this.palletInfo.autoSort = this.currentAutoSort
this.palletInfo.locationCode = row.locationCode || ''
this.palletInfo.area = row.area || ''
this.loadPalletTypeList()
}
})
},
loadPalletTypeList() {
getPalletTypeList({
site: this.site,
palletFamily: this.currentPalletFamily || '',
active: 'Y'
}).then(({ data }) => {
if (data && data.code === 0) {
this.palletTypeOptions = data.rows || []
const selectedType = this.palletTypeOptions.find((item) => item.palletType === this.currentPalletType)
this.currentWcsAutoSort = selectedType ? (selectedType.wcsAutoSort || 'N') : ''
this.currentMaxLayer = selectedType ? Number(selectedType.maxLayer || 0) : 0
this.updateAutoSortControl()
}
})
},
loadAllPalletTypeListForCallEmpty() {
getPalletTypeList({
site: this.site,
palletFamily: '',
active: 'Y'
}).then(({ data }) => {
if (data && data.code === 0) {
this.callEmptyPalletTypeOptions = data.rows || []
const hasSelectedType = this.callEmptyPalletTypeOptions.some((item) => item.palletType === this.callEmptyForm.palletType)
if (!hasSelectedType) {
this.callEmptyForm.palletType = ''
}
} else {
this.callEmptyPalletTypeOptions = []
this.callEmptyForm.palletType = ''
}
}).catch(() => {
this.callEmptyPalletTypeOptions = []
this.callEmptyForm.palletType = ''
})
},
refreshTable() {
getPalletDetails({
site: this.site,
palletId: this.palletCode,
position: '',
layer: null
}).then(({ data }) => {
if (data && data.code === 0) {
this.detailList = data.details || []
} else {
this.detailList = []
}
this.palletTypeDisabled = this.detailList.length > 0
this.updateAutoSortControl()
}).catch(() => {
this.detailList = []
this.palletTypeDisabled = false
this.updateAutoSortControl()
})
},
handlePalletTypeChange() {
const selectedType = this.palletTypeOptions.find((item) => item.palletType === this.currentPalletType)
this.currentWcsAutoSort = selectedType ? (selectedType.wcsAutoSort || 'N') : ''
this.currentMaxLayer = selectedType ? Number(selectedType.maxLayer || 0) : 0
if (this.currentPalletType === 'A0103' || this.currentPalletType === 'A0206') {
this.forceFullPalletOut = true
}
this.updateAutoSortControl()
this.savePalletTypeConfig()
},
handleAutoSortChange() {
this.savePalletTypeConfig()
},
handleForceFullPalletOutChange() {
this.savePalletTypeConfig()
},
updateAutoSortControl() {
if (this.forceFullPalletOut || this.currentPalletType === 'A0103') {
this.currentAutoSort = 'N'
this.autoSortDisabled = true
return
}
if (this.currentWcsAutoSort === 'N') {
this.currentAutoSort = 'N'
this.autoSortDisabled = true
return
}
this.autoSortDisabled = this.detailList.length > 0 || this.interfaceBusy
},
savePalletTypeConfig() {
if (this.interfaceBusy) {
return
}
this.palletConfigLoading = true
updatePalletTypeAndAutoSort({
site: this.site,
palletId: this.palletCode,
palletType: this.currentPalletType,
autoSort: this.currentAutoSort,
forceFullPalletOut: this.forceFullPalletOut
}).then(({ data }) => {
if (data && data.code === 0) {
this.palletInfo.palletType = this.currentPalletType
this.palletInfo.autoSort = this.currentAutoSort
this.$message.success('托盘配置已更新')
} else {
this.$alert((data && data.msg) || '托盘配置更新失败', '错误', { confirmButtonText: '确定' })
}
}).catch((error) => {
this.$alert(error.message || '托盘配置更新失败', '错误', { confirmButtonText: '确定' })
}).finally(() => {
this.palletConfigLoading = false
})
},
handleCallPallet() {
if (this.interfaceBusy) {
return
}
this.openCallEmptyModal()
},
showScanModal() {
this.scanModalVisible = true
this.scanCode = ''
this.currentSelectedPosition = ''
this.needRefreshOnClose = false
if (this.operationType === 'in') {
this.scanLayer = 1
this.loadLayerOptions()
} else {
this.$nextTick(() => {
if (this.$refs.scanInput) {
this.$refs.scanInput.focus()
}
})
}
},
showDetailModal() {
this.detailModalVisible = true
},
showScanOutModal() {
this.scanOutModalVisible = true
},
loadLayerOptions() {
getPalletDetails({
site: this.site,
palletId: this.palletCode,
position: '',
layer: null
}).then(({ data }) => {
if (data && data.code === 0) {
const details = data.details || []
let maxExistingLayer = 0
if (details.length > 0) {
maxExistingLayer = Math.max(...details.map((d) => d.layer || 0))
}
if (this.currentMaxLayer === 0) {
this.currentMixedMode = false
this.scanLayerOptions = Array.from({ length: maxExistingLayer + 1 }, (_, i) => i + 1)
} else {
this.currentMixedMode = false
this.scanLayerOptions = Array.from({ length: this.currentMaxLayer }, (_, i) => i + 1)
}
this.loadAvailablePositions()
} else {
this.$alert((data && data.msg) || '获取栈板信息失败', '错误', { confirmButtonText: '确定' })
}
}).catch(() => {
this.$alert('获取栈板信息失败', '错误', { confirmButtonText: '确定' })
})
},
loadAvailablePositions() {
this.positionGridLoading = true
this.availablePositions = []
getPalletDetails({
site: this.site,
palletId: this.palletCode,
position: '',
layer: this.scanLayer
}).then(({ data: detailData }) => {
const positionCountMap = {}
if (detailData && detailData.code === 0) {
const details = detailData.details || []
details.forEach((detail) => {
const pos = detail.position
positionCountMap[pos] = (positionCountMap[pos] || 0) + 1
})
}
return getAvailablePositionsForLayer({
site: this.site,
palletId: this.palletCode,
layer: this.scanLayer
}).then(({ data }) => {
if (data && data.code === 0) {
const result = data.data || {}
const positions = result.positions || []
this.availablePositions = result.availablePositions || []
this.positionGrid = positions.map((position) => ({
position,
count: positionCountMap[position] || 0
}))
} else {
this.availablePositions = []
this.positionGrid = []
this.$alert((data && data.msg) || '获取位置信息失败', '错误', { confirmButtonText: '确定' })
}
})
}).catch(() => {
this.availablePositions = []
this.positionGrid = []
this.$alert('获取位置信息失败', '错误', { confirmButtonText: '确定' })
}).finally(() => {
this.positionGridLoading = false
})
},
handleLayerClick(layer) {
if (this.scanLayer === layer) {
return
}
this.scanLayer = layer
this.currentSelectedPosition = ''
this.loadAvailablePositions()
},
handlePositionClick(position) {
if (this.positionGridLoading) {
this.$message.warning('位置信息加载中,请稍候')
return
}
if (!this.availablePositions.includes(position)) {
this.$message.warning('该位置不可用')
return
}
this.currentSelectedPosition = position
this.scanPosition = position
this.$nextTick(() => {
if (this.$refs.scanInput) {
this.$refs.scanInput.focus()
}
})
},
moveFocusToScanInput() {
this.$nextTick(() => {
if (this.$refs.scanInput) {
this.$refs.scanInput.focus()
}
})
},
closeScanModal() {
this.scanModalVisible = false
if (this.needRefreshOnClose) {
this.refreshTable()
this.needRefreshOnClose = false
}
},
handleLabelScan() {
if (!this.ensureSite() || this.interfaceBusy) {
return
}
if (!this.scanCode || !this.scanCode.trim()) {
this.scanCode = ''
this.$alert('请输入标签编码', '错误', { confirmButtonText: '确定' })
this.moveFocusToScanInput()
return
}
if (this.operationType === 'in') {
if (!this.scanPosition) {
this.scanCode = ''
this.$alert('请选择位置', '错误', { confirmButtonText: '确定' })
this.moveFocusToScanInput()
return
}
if (!this.scanLayer) {
this.scanCode = ''
this.$alert('请选择层数', '错误', { confirmButtonText: '确定' })
this.moveFocusToScanInput()
return
}
this.scanRequestLoading = true
savePalletDetail({
site: this.site,
palletId: this.palletCode,
position: this.scanPosition,
layer: this.scanLayer,
serialNo: this.scanCode,
sortFlag: 0
}).then(({ data }) => {
if (data && data.code === 0) {
this.$message.success('扫进成功')
this.needRefreshOnClose = true
this.scanCode = ''
this.loadLayerOptions()
this.moveFocusToScanInput()
} else {
this.scanCode = ''
this.$alert((data && data.msg) || '扫进失败', '错误', { confirmButtonText: '确定' })
this.moveFocusToScanInput()
}
}).catch((error) => {
this.scanCode = ''
this.$alert(error.message || '扫进失败', '错误', { confirmButtonText: '确定' })
this.moveFocusToScanInput()
}).finally(() => {
this.scanRequestLoading = false
})
} else {
const scanOutItem = this.detailList.find((item) => item.serialNo === this.scanCode)
this.scanRequestLoading = true
deletePalletDetail({
site: this.site,
palletId: this.palletCode,
serialNo: this.scanCode
}).then(({ data }) => {
if (data && data.code === 0) {
if (scanOutItem) {
this.scanOutList.push({
position: scanOutItem.position,
layer: scanOutItem.layer,
serialNo: scanOutItem.serialNo
})
}
this.$message.success('扫出成功')
this.needRefreshOnClose = true
this.scanCode = ''
this.moveFocusToScanInput()
} else {
this.scanCode = ''
this.$alert((data && data.msg) || '扫出失败', '错误', { confirmButtonText: '确定' })
this.moveFocusToScanInput()
}
}).catch((error) => {
this.scanCode = ''
this.$alert(error.message || '扫出失败', '错误', { confirmButtonText: '确定' })
this.moveFocusToScanInput()
}).finally(() => {
this.scanRequestLoading = false
})
}
},
loadAreaOptions() {
getAreaOptionsForChange({ site: this.site }).then(({ data }) => {
if (data && data.code === 0) {
this.areaOptions = data.rows || []
} else {
this.areaOptions = []
}
}).catch(() => {
this.areaOptions = []
})
},
openCallEmptyModal() {
if (!this.ensureSite()) {
return
}
this.callEmptyForm = { areaId: '', stationCode: '', palletType: this.currentPalletType || '' }
this.stationOptions = []
this.loadAreaOptions()
this.loadAllPalletTypeListForCallEmpty()
this.callEmptyModalVisible = true
},
handleAreaChangeForCall() {
this.callEmptyForm.stationCode = ''
if (!this.callEmptyForm.areaId) {
this.stationOptions = []
return
}
getStationsByArea({
site: this.site,
areaId: this.callEmptyForm.areaId
}).then(({ data }) => {
if (data && data.code === 0) {
this.stationOptions = data.rows || []
} else {
this.stationOptions = []
}
}).catch(() => {
this.stationOptions = []
})
},
submitCallEmpty() {
if (this.interfaceBusy) {
return
}
if (!this.callEmptyForm.areaId || !this.callEmptyForm.stationCode || !this.callEmptyForm.palletType) {
this.$alert('请完整选择区域、站点和栈板类型', '提示', { confirmButtonText: '确定' })
return
}
this.callEmptyLoading = true
callEmptyPalletToStation({
site: this.site,
// 调用空托盘下发时,targetArea传业务区域ID,由后端映射haian_area.wcs_area_code - rqrq
areaId: this.callEmptyForm.areaId,
targetArea: this.callEmptyForm.areaId,
stationCode: this.callEmptyForm.stationCode,
palletType: this.callEmptyForm.palletType
}).then(({ data }) => {
if (data && data.code === 0) {
this.$message.success('调用空托盘成功')
this.callEmptyModalVisible = false
} else {
this.$alert((data && data.msg) || '调用空托盘失败', '错误', { confirmButtonText: '确定' })
}
}).catch((error) => {
this.$alert(error.message || '调用空托盘失败', '错误', { confirmButtonText: '确定' })
}).finally(() => {
this.callEmptyLoading = false
})
},
// 推送组盘前校验:area/location任何一个有值都必须阻断 - rqrq
openPushAssemblyDialog() {
if (!this.ensureSite() || this.interfaceBusy) {
return
}
if (!this.palletScanned) {
this.$alert('请先扫描栈板', '提示', { confirmButtonText: '确定' })
return
}
if (!this.detailList || this.detailList.length === 0) {
this.$alert('栈板明细为空,请先扫进物料', '提示', { confirmButtonText: '确定' })
return
}
if (!this.canPushAssembly) {
this.$alert('推送组盘仅允许未绑定站点的栈板,当前栈板area或location_code存在值', '提示', { confirmButtonText: '确定' })
return
}
this.selectedInboundPort = ''
this.pushAssemblyDialogVisible = true
},
closePushAssemblyDialog() {
if (this.interfaceBusy) {
return
}
this.pushAssemblyDialogVisible = false
this.selectedInboundPort = ''
},
// 推送组盘:确认入库口后调用海安专用后端方法,调用期间保持全页面按钮锁定 - rqrq
submitPushAssembly() {
if (!this.selectedInboundPort) {
this.$alert('请选择入库口', '提示', { confirmButtonText: '确定' })
return
}
this.$confirm('确认推送组盘数据吗?', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.pushAssemblyLoading = true
pushPalletAssemblyToInboundPort({
site: this.site,
palletId: this.palletCode,
inboundPort: this.selectedInboundPort
}).then(({ data }) => {
if (data && data.code === 0) {
this.$message.success('推送组盘成功')
this.pushAssemblyDialogVisible = false
this.resetPage(true)
} else {
this.$alert((data && data.msg) || '推送组盘失败', '错误', { confirmButtonText: '确定' })
}
}).catch((error) => {
this.$alert(error.message || '推送组盘失败', '错误', { confirmButtonText: '确定' })
}).finally(() => {
this.pushAssemblyLoading = false
})
}).catch(() => {
// 用户取消确认时不清空数据,便于继续核对后再提交 - rqrq
})
}
},
activated() {
this.resetPage(true)
},
beforeRouteLeave(to, from, next) {
this.resetPage(false)
next()
},
mounted() {
this.resetPage(true)
}
}
</script>
<style scoped>
.header-action-disabled {
opacity: 0.5;
pointer-events: none;
}
.detail-table {
background: white;
border-radius: 6px;
overflow: hidden;
border: 1px solid #e0e0e0;
}
.table-header,
.table-row {
display: flex;
align-items: center;
padding: 8px;
border-bottom: 1px solid #e0e0e0;
}
.table-header {
background: #f5f5f5;
font-weight: bold;
font-size: 14px;
}
.col-position {
flex: 1;
text-align: center;
}
.col-layer {
flex: 1;
text-align: center;
}
.col-serial {
flex: 4;
text-align: center;
word-break: break-all;
}
.empty-row {
justify-content: center;
align-items: center;
padding: 20px;
border-bottom: none;
}
.empty-hint {
text-align: center;
color: #999;
width: 100%;
}
.scan-modal-content {
padding: 10px 0;
}
.modal-form {
margin-bottom: 16px;
}
.dialog-footer {
text-align: center;
}
.action-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
background-color: #ccc !important;
border-color: #ccc !important;
}
.action-btn.warning {
background-color: #f59e0b;
border-color: #f59e0b;
color: #fff;
}
::v-deep .el-dialog__wrapper {
z-index: 2000 !important;
}
::v-deep .el-overlay {
z-index: 2000 !important;
}
::v-deep .el-radio {
margin-right: 8px !important;
}
::v-deep .el-radio__label {
font-size: 14px;
}
.layer-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
}
.layer-item {
display: flex;
align-items: center;
justify-content: center;
min-width: 50px;
height: 45px;
padding: 0 12px;
background-color: #fff;
border: 2px solid #dcdfe6;
border-radius: 6px;
font-size: 15px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
user-select: none;
}
.layer-item:hover {
border-color: #409eff;
background-color: #ecf5ff;
}
.layer-item.layer-selected {
border-color: #409eff;
background-color: #409eff;
color: #fff;
}
.position-grid {
display: grid;
gap: 10px;
margin-top: 8px;
position: relative;
}
.position-grid.position-grid-1 {
grid-template-columns: 1fr;
grid-template-rows: 1fr;
}
.position-grid.position-grid-4 {
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
grid-auto-flow: column;
}
.position-grid.position-grid-9 {
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
grid-auto-flow: row;
}
.position-item.position-placeholder {
background-color: #f0f0f0 !important;
border: 1px dashed #d0d0d0 !important;
color: transparent !important;
cursor: not-allowed !important;
pointer-events: none !important;
}
.position-grid.position-grid-loading {
opacity: 0.7;
pointer-events: none;
}
.position-item {
display: flex;
align-items: center;
justify-content: center;
height: 60px;
background-color: #fff;
border: 2px solid #dcdfe6;
border-radius: 6px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
user-select: none;
}
.position-item:hover:not(.position-disabled) {
border-color: #409eff;
background-color: #ecf5ff;
}
.position-item.position-selected {
border-color: #409eff;
background-color: #409eff;
color: #fff;
}
.position-item.position-disabled {
background-color: #f5f7fa;
color: #c0c4cc;
cursor: not-allowed;
opacity: 0.6;
}
.position-item.position-loading {
background: linear-gradient(90deg, #f5f7fa 25%, #e4e7ed 50%, #f5f7fa 75%);
background-size: 200% 100%;
animation: loading 1.5s ease-in-out infinite;
}
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
</style>