7 changed files with 719 additions and 17 deletions
-
13src/api/haianAutoWarehouse/labelWriteOff.js
-
2src/router/index.js
-
7src/views/main.vue
-
2src/views/modules/automatedWarehouse/palletSortingNoAgv.vue
-
578src/views/modules/haianAutoWarehouse/haianLabelWriteOff.vue
-
80src/views/modules/haianAutoWarehouse/haianPalletRelocate.vue
-
54src/views/modules/inventory/label-query.vue
@ -0,0 +1,13 @@ |
|||
import { createAPI } from "@/utils/httpRequest.js"; |
|||
|
|||
/** |
|||
* 海安标签特殊销账:扫描标签查询库存信息,不改业务数据 - rqrq |
|||
* @param {Object} data 必须包含 site、serialNo - rqrq |
|||
*/ |
|||
export const queryLabelForWriteOff = data => createAPI(`/haiAnWcsIntegration/queryLabelForWriteOff`, 'post', data) |
|||
|
|||
/** |
|||
* 海安标签特殊销账:确认后把 in_stock_flag 改成 N;标签仍在托盘明细时由后端拒绝 - rqrq |
|||
* @param {Object} data 必须包含 site、serialNo - rqrq |
|||
*/ |
|||
export const writeOffLabel = data => createAPI(`/haiAnWcsIntegration/writeOffLabel`, 'post', data) |
|||
@ -0,0 +1,578 @@ |
|||
<template> |
|||
<div> |
|||
<div class="pda-container"> |
|||
<div class="header-bar"> |
|||
<div class="header-left" :class="{ 'header-action-disabled': pageBusy }" @click="handleBack"> |
|||
<i class="el-icon-arrow-left"></i> |
|||
<span>标签特殊销账</span> |
|||
</div> |
|||
<div class="header-right" :class="{ 'header-action-disabled': pageBusy }" @click="goHome"> |
|||
首页 |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="table-body" style="max-height: 500px; overflow-y: auto;"> |
|||
<div class="main-content form-section"> |
|||
<!-- 未通过密码前禁止扫码,避免未授权销账 - rqrq --> |
|||
<div class="input-group"> |
|||
<el-input |
|||
v-model="labelCode" |
|||
placeholder="请扫描标签编码" |
|||
class="form-input" |
|||
clearable |
|||
inputmode="none" |
|||
autocomplete="off" |
|||
autocorrect="off" |
|||
spellcheck="false" |
|||
:disabled="!unlocked || pageBusy" |
|||
@keyup.enter.native="handleLabelScan" |
|||
ref="labelInput" |
|||
/> |
|||
</div> |
|||
|
|||
<div v-if="labelInfo" class="info-section"> |
|||
<div class="info-title"> |
|||
<i class="el-icon-document">标签信息</i> |
|||
<!-- 原标签查询页打印按钮改为销账,确认后才改在库标识 - rqrq --> |
|||
<button |
|||
class="print-btn" |
|||
@click="handleWriteOff" |
|||
:disabled="writeOffLoading || pageBusy" |
|||
> |
|||
<i :class="writeOffLoading ? 'el-icon-loading' : 'el-icon-sold-out'"></i> |
|||
{{ writeOffLoading ? '销账中...' : '销账' }} |
|||
</button> |
|||
</div> |
|||
|
|||
<div class="info-row"> |
|||
<span class="info-label">标签编码:</span> |
|||
<span class="info-value">{{ labelInfo.unitId || '-' }}</span> |
|||
</div> |
|||
<div class="info-row"> |
|||
<span class="info-label">工厂编码:</span> |
|||
<span class="info-value">{{ labelInfo.site || '-' }}</span> |
|||
</div> |
|||
<div class="info-row"> |
|||
<span class="info-label">物料编码:</span> |
|||
<span class="info-value">{{ labelInfo.partNo || '-' }}</span> |
|||
</div> |
|||
<div class="info-row"> |
|||
<span class="info-label">库存类型:</span> |
|||
<span class="info-value">{{ formatInStockFlag(labelInfo.inStockFlag) }}</span> |
|||
</div> |
|||
<div class="info-row"> |
|||
<span class="info-label">数量:</span> |
|||
<span class="info-value">{{ formatQty(labelInfo.qty) }}</span> |
|||
</div> |
|||
<div class="info-row"> |
|||
<span class="info-label">批次号:</span> |
|||
<span class="info-value">{{ labelInfo.batchNo || '-' }}</span> |
|||
</div> |
|||
<div class="info-row"> |
|||
<span class="info-label">仓库:</span> |
|||
<span class="info-value">{{ labelInfo.warehouseId || '-' }}</span> |
|||
</div> |
|||
<div class="info-row"> |
|||
<span class="info-label">库位:</span> |
|||
<span class="info-value">{{ labelInfo.locationId || '-' }}</span> |
|||
</div> |
|||
<div class="info-row"> |
|||
<span class="info-label">WDR:</span> |
|||
<span class="info-value">{{ labelInfo.wdr || '-' }}</span> |
|||
</div> |
|||
<div class="info-row"> |
|||
<span class="info-label">engChgLevel:</span> |
|||
<span class="info-value">{{ labelInfo.engChgLevel || '-' }}</span> |
|||
</div> |
|||
<div class="info-row"> |
|||
<span class="info-label">expiredDate:</span> |
|||
<span class="info-value">{{ formatDate(labelInfo.expiredDate) }}</span> |
|||
</div> |
|||
<div class="info-row"> |
|||
<span class="info-label">创建时间:</span> |
|||
<span class="info-value">{{ formatDate(labelInfo.createdDate) }}</span> |
|||
</div> |
|||
<div class="info-row"> |
|||
<span class="info-label">上次盘点日期:</span> |
|||
<span class="info-value">{{ formatDate(labelInfo.lastCountDate) }}</span> |
|||
</div> |
|||
<div class="info-row" v-if="labelInfo.reserveOrderRef1"> |
|||
<span class="info-label">预定申请单号:</span> |
|||
<span class="info-value">{{ labelInfo.reserveOrderRef1 || '-' }}</span> |
|||
</div> |
|||
<div class="info-row" v-if="labelInfo.reserveOrderRef3"> |
|||
<span class="info-label">预定订单号:</span> |
|||
<span class="info-value">{{ labelInfo.reserveOrderRef3 || '-' }}</span> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 底部只保留清空,扫描结果和销账按钮分开,避免误触 - rqrq --> |
|||
<div v-if="labelInfo" class="bottom-actions"> |
|||
<button class="action-btn secondary" :disabled="pageBusy" @click="clearData"> |
|||
清空 |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 进入页 0.5 秒后弹出;取消回菜单;密码错误不关弹框 - rqrq --> |
|||
<el-dialog |
|||
title="请输入密码" |
|||
:visible.sync="passwordDialogVisible" |
|||
width="90%" |
|||
:close-on-click-modal="false" |
|||
:close-on-press-escape="false" |
|||
:show-close="false" |
|||
:append-to-body="true" |
|||
:modal-append-to-body="true" |
|||
> |
|||
<el-input |
|||
v-model="passwordInput" |
|||
type="password" |
|||
placeholder="请输入密码" |
|||
autocomplete="new-password" |
|||
ref="passwordInput" |
|||
@keyup.enter.native="confirmPassword" |
|||
/> |
|||
<div slot="footer" class="dialog-footer"> |
|||
<button class="dialog-btn cancel" @click="cancelPassword">取消</button> |
|||
<button class="dialog-btn confirm" @click="confirmPassword">确定</button> |
|||
</div> |
|||
</el-dialog> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { queryLabelForWriteOff, writeOffLabel } from '@/api/haianAutoWarehouse/labelWriteOff' |
|||
|
|||
// 前端写死校验,不传后端、不落库 - rqrq |
|||
const WRITE_OFF_PASSWORD = '124578' |
|||
|
|||
export default { |
|||
name: 'HaianLabelWriteOff', |
|||
data () { |
|||
return { |
|||
site: this.$store.state.user.site, |
|||
labelCode: '', |
|||
labelInfo: null, |
|||
scanLoading: false, |
|||
writeOffLoading: false, |
|||
unlocked: false, |
|||
passwordDialogVisible: false, |
|||
passwordInput: '', |
|||
passwordTimer: null |
|||
} |
|||
}, |
|||
computed: { |
|||
pageBusy () { |
|||
return this.scanLoading || this.writeOffLoading |
|||
} |
|||
}, |
|||
methods: { |
|||
/** |
|||
* 返回:未解锁当取消密码处理回菜单;已解锁清空后返回上一页 - rqrq |
|||
*/ |
|||
handleBack () { |
|||
if (this.pageBusy) { |
|||
return |
|||
} |
|||
if (!this.unlocked) { |
|||
this.cancelPassword() |
|||
return |
|||
} |
|||
this.resetPage(false) |
|||
this.$router.back() |
|||
}, |
|||
goHome () { |
|||
if (this.pageBusy) { |
|||
return |
|||
} |
|||
this.resetPage(false) |
|||
this.$router.push({ path: '/' }) |
|||
}, |
|||
/** |
|||
* 每次进入都清扫描结果、密码框和定时器,避免 keepAlive 或重复进入残留上一张标签 - rqrq |
|||
*/ |
|||
resetPage (needFocus = true) { |
|||
this.clearPasswordTimer() |
|||
this.site = this.$store.state.user.site |
|||
this.labelCode = '' |
|||
this.labelInfo = null |
|||
this.scanLoading = false |
|||
this.writeOffLoading = false |
|||
this.unlocked = false |
|||
this.passwordDialogVisible = false |
|||
this.passwordInput = '' |
|||
if (needFocus) { |
|||
this.schedulePasswordDialog() |
|||
} |
|||
}, |
|||
/** |
|||
* 进入页面 0.5 秒后再弹密码框,避免路由切换时弹框被立刻关掉 - rqrq |
|||
*/ |
|||
schedulePasswordDialog () { |
|||
this.clearPasswordTimer() |
|||
this.passwordTimer = setTimeout(() => { |
|||
this.passwordDialogVisible = true |
|||
this.$nextTick(() => { |
|||
if (this.$refs.passwordInput) { |
|||
this.$refs.passwordInput.focus() |
|||
} |
|||
}) |
|||
}, 500) |
|||
}, |
|||
clearPasswordTimer () { |
|||
if (this.passwordTimer) { |
|||
clearTimeout(this.passwordTimer) |
|||
this.passwordTimer = null |
|||
} |
|||
}, |
|||
/** |
|||
* 密码正确才解锁扫码;错误只提示,弹框继续挡着 - rqrq |
|||
*/ |
|||
confirmPassword () { |
|||
if (!this.passwordInput) { |
|||
this.$alert('请输入密码', '提示', { confirmButtonText: '确定' }) |
|||
return |
|||
} |
|||
if (this.passwordInput !== WRITE_OFF_PASSWORD) { |
|||
this.passwordInput = '' |
|||
this.$alert('密码错误', '提示', { confirmButtonText: '确定' }) |
|||
return |
|||
} |
|||
this.unlocked = true |
|||
this.passwordDialogVisible = false |
|||
this.passwordInput = '' |
|||
this.focusLabelInput() |
|||
}, |
|||
/** |
|||
* 取消密码等于放弃本功能,直接回菜单,不停留在销账页 - rqrq |
|||
*/ |
|||
cancelPassword () { |
|||
this.resetPage(false) |
|||
this.$router.push({ path: '/' }) |
|||
}, |
|||
handleLabelScan () { |
|||
if (!this.unlocked) { |
|||
return |
|||
} |
|||
if (!this.labelCode || !this.labelCode.trim()) { |
|||
this.$message.error('请扫描有效的标签编码') |
|||
return |
|||
} |
|||
this.scanLoading = true |
|||
// site 来自登录工厂,serialNo 为扫到的标签;失败清空并重新聚焦 - rqrq |
|||
queryLabelForWriteOff({ |
|||
site: this.site, |
|||
serialNo: this.labelCode.trim() |
|||
}).then(({ data }) => { |
|||
if (data && data.code === 0) { |
|||
this.labelInfo = data.row || null |
|||
this.$message.success('查询成功') |
|||
} else { |
|||
this.$alert((data && data.msg) || '标签不存在', '提示', { confirmButtonText: '确定' }) |
|||
this.labelCode = '' |
|||
this.labelInfo = null |
|||
this.focusLabelInput() |
|||
} |
|||
}).catch((error) => { |
|||
this.labelCode = '' |
|||
this.labelInfo = null |
|||
this.$message.error((error && error.message) || '查询异常') |
|||
this.focusLabelInput() |
|||
}).finally(() => { |
|||
this.scanLoading = false |
|||
}) |
|||
}, |
|||
/** |
|||
* 先确认再销账;后端会再校验不在海安托盘明细且尚未是N - rqrq |
|||
*/ |
|||
handleWriteOff () { |
|||
if (!this.labelInfo || !this.labelInfo.unitId) { |
|||
this.$alert('没有可销账的标签信息', '提示', { confirmButtonText: '确定' }) |
|||
return |
|||
} |
|||
this.$confirm('是否确认销账?', '提示', { |
|||
confirmButtonText: '确定', |
|||
cancelButtonText: '取消', |
|||
type: 'warning', |
|||
closeOnClickModal: false |
|||
}).then(() => { |
|||
this.doWriteOff() |
|||
}).catch(() => { |
|||
// 用户取消确认,页面数据保持不变 - rqrq |
|||
}) |
|||
}, |
|||
doWriteOff () { |
|||
this.writeOffLoading = true |
|||
writeOffLabel({ |
|||
site: this.site, |
|||
serialNo: this.labelInfo.unitId |
|||
}).then(({ data }) => { |
|||
if (data && data.code === 0) { |
|||
this.$message.success('销账成功') |
|||
this.clearData() |
|||
} else { |
|||
this.$alert((data && data.msg) || '销账失败', '提示', { confirmButtonText: '确定' }) |
|||
} |
|||
}).catch((error) => { |
|||
this.$message.error((error && error.message) || '销账异常') |
|||
}).finally(() => { |
|||
this.writeOffLoading = false |
|||
}) |
|||
}, |
|||
clearData () { |
|||
this.labelCode = '' |
|||
this.labelInfo = null |
|||
this.focusLabelInput() |
|||
}, |
|||
focusLabelInput () { |
|||
this.$nextTick(() => { |
|||
if (this.unlocked && this.$refs.labelInput) { |
|||
this.$refs.labelInput.focus() |
|||
} |
|||
}) |
|||
}, |
|||
formatInStockFlag (flag) { |
|||
if (flag === 'X') return '未入库' |
|||
if (flag === 'N') return '不在库' |
|||
if (flag === 'Y') return '在库' |
|||
if (flag === 'Q') return '隔离' |
|||
return flag || '-' |
|||
}, |
|||
/** |
|||
* 数量去尾零,空值按 0 展示 - rqrq |
|||
*/ |
|||
formatQty (qty) { |
|||
if (qty === null || qty === undefined || qty === '') { |
|||
return 0 |
|||
} |
|||
const num = Number(qty) |
|||
if (isNaN(num)) { |
|||
return qty |
|||
} |
|||
return parseFloat(String(num)) |
|||
}, |
|||
formatDate (date) { |
|||
if (!date) return '-' |
|||
const d = new Date(date) |
|||
if (isNaN(d.getTime())) { |
|||
return date |
|||
} |
|||
const year = d.getFullYear() |
|||
const month = String(d.getMonth() + 1).padStart(2, '0') |
|||
const day = String(d.getDate()).padStart(2, '0') |
|||
const hours = String(d.getHours()).padStart(2, '0') |
|||
const minutes = String(d.getMinutes()).padStart(2, '0') |
|||
return `${year}-${month}-${day} ${hours}:${minutes}` |
|||
} |
|||
}, |
|||
created () { |
|||
this.resetPage(true) |
|||
}, |
|||
activated () { |
|||
this.resetPage(true) |
|||
}, |
|||
beforeRouteLeave (to, from, next) { |
|||
this.resetPage(false) |
|||
next() |
|||
}, |
|||
beforeDestroy () { |
|||
this.clearPasswordTimer() |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.header-action-disabled { |
|||
opacity: 0.5; |
|||
pointer-events: none; |
|||
} |
|||
|
|||
.pda-container { |
|||
width: 100vw; |
|||
height: 120vh; |
|||
display: flex; |
|||
flex-direction: column; |
|||
background: #f5f5f5; |
|||
font-family: 'Arial', sans-serif; |
|||
} |
|||
|
|||
.header-bar { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
padding: 8px 16px; |
|||
background: #17B3A3; |
|||
color: white; |
|||
height: 40px; |
|||
min-height: 40px; |
|||
max-height: 40px; |
|||
} |
|||
|
|||
.header-left { |
|||
display: flex; |
|||
align-items: center; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.header-left i { |
|||
margin-right: 8px; |
|||
font-size: 18px; |
|||
} |
|||
|
|||
.header-left span { |
|||
font-size: 16px; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.header-right { |
|||
cursor: pointer; |
|||
font-size: 14px; |
|||
padding: 4px 8px; |
|||
border-radius: 4px; |
|||
} |
|||
|
|||
.table-body { |
|||
flex: 1; |
|||
overflow-y: auto; |
|||
} |
|||
|
|||
.main-content { |
|||
padding: 16px; |
|||
} |
|||
|
|||
.input-group { |
|||
margin-bottom: 1px !important; |
|||
} |
|||
|
|||
.form-input { |
|||
width: 100%; |
|||
height: 44px; |
|||
} |
|||
|
|||
.info-section { |
|||
background: white; |
|||
border-radius: 8px; |
|||
padding: 16px; |
|||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
|||
} |
|||
|
|||
.info-title { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
font-size: 16px; |
|||
font-weight: bold; |
|||
color: #17B3A3; |
|||
margin-bottom: 6px; |
|||
padding-bottom: 5px; |
|||
border-bottom: 2px solid #17B3A3; |
|||
} |
|||
|
|||
.info-title i { |
|||
margin-right: 8px; |
|||
font-size: 18px; |
|||
} |
|||
|
|||
.print-btn { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 4px; |
|||
padding: 6px 12px; |
|||
background: #17B3A3; |
|||
color: white; |
|||
border: none; |
|||
border-radius: 4px; |
|||
font-size: 14px; |
|||
cursor: pointer; |
|||
white-space: nowrap; |
|||
} |
|||
|
|||
.print-btn:disabled { |
|||
background: #ccc; |
|||
cursor: not-allowed; |
|||
opacity: 0.6; |
|||
} |
|||
|
|||
.info-row { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: flex-start; |
|||
padding: 10px 0; |
|||
border-bottom: 1px solid #f0f0f0; |
|||
} |
|||
|
|||
.info-row:last-child { |
|||
border-bottom: none; |
|||
} |
|||
|
|||
.info-label { |
|||
font-size: 14px; |
|||
color: #666; |
|||
min-width: 90px; |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
.info-value { |
|||
font-size: 14px; |
|||
font-weight: 500; |
|||
color: #333; |
|||
flex: 1; |
|||
text-align: right; |
|||
word-break: break-all; |
|||
} |
|||
|
|||
.bottom-actions { |
|||
display: flex; |
|||
gap: 12px; |
|||
padding-top: 16px; |
|||
} |
|||
|
|||
.action-btn { |
|||
flex: 1; |
|||
padding: 12px 24px; |
|||
border: none; |
|||
border-radius: 6px; |
|||
font-size: 16px; |
|||
font-weight: 500; |
|||
cursor: pointer; |
|||
min-height: 44px; |
|||
} |
|||
|
|||
.action-btn.secondary { |
|||
background: #f5f5f5; |
|||
color: #333; |
|||
border: 1px solid #ddd; |
|||
} |
|||
|
|||
.action-btn:disabled { |
|||
opacity: 0.6; |
|||
cursor: not-allowed; |
|||
} |
|||
|
|||
.dialog-footer { |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
gap: 8px; |
|||
} |
|||
|
|||
.dialog-btn { |
|||
min-height: 40px; |
|||
padding: 8px 16px; |
|||
border: none; |
|||
border-radius: 4px; |
|||
font-size: 14px; |
|||
} |
|||
|
|||
.dialog-btn.cancel { |
|||
background: #f5f5f5; |
|||
color: #333; |
|||
} |
|||
|
|||
.dialog-btn.confirm { |
|||
background: #17B3A3; |
|||
color: white; |
|||
} |
|||
</style> |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue