feat: 完善盘点出入库流程

This commit is contained in:
zgw
2026-06-25 18:31:17 +08:00
parent 4b8bc03343
commit c7c07c01cf
21 changed files with 1974 additions and 111 deletions

View File

@@ -0,0 +1,28 @@
import request from '@/utils/request'
// 获取盘点待处理任务列表
export function getInventoryTaskList(query) {
return request({
url: '/pda/inventoryTaskList',
method: 'get',
params: query
})
}
// 扫码查询盘点任务详情
export function getInventoryDetail(query) {
return request({
url: '/pda/inventoryDetail',
method: 'get',
params: query
})
}
// 确认盘点
export function confirmInventory(data) {
return request({
url: '/pda/inventoryConfirm',
method: 'post',
data: data
})
}

View File

@@ -1,61 +1,237 @@
<template>
<view class="page-container">
<view class="menu-section">
<view class="menu-card" @click="scanInventory">
<view class="card-left">
<view class="card-icon green">
<uni-icons type="scan" size="24" color="#fff"></uni-icons>
</view>
<view class="card-info">
<text class="card-title">扫码盘点</text>
<text class="card-desc">扫描托盘码进行库存盘点</text>
</view>
<view class="scan-section">
<view class="scan-header">
<view>
<text class="scan-title">盘点扫码</text>
<text class="scan-subtitle">WCS 出库到位后扫描托盘核对物料</text>
</view>
<view class="scan-btn" @click="startScan">
<uni-icons type="scan" size="18" color="#1f8f4d"></uni-icons>
<text class="scan-btn-text">扫码</text>
</view>
<uni-icons type="right" size="18" color="#ccc"></uni-icons>
</view>
<view class="menu-card reserve-card" @click="showReservedTip">
<view class="card-left">
<view class="card-icon gray">
<uni-icons type="compose" size="24" color="#fff"></uni-icons>
<view class="input-row">
<text class="input-label">任务号</text>
<input class="scan-input" v-model="taskNo" placeholder="选填" @confirm="handleSearch" />
</view>
<view class="input-row">
<text class="input-label">托盘码</text>
<input class="scan-input" v-model="barcode" placeholder="请输入或扫描托盘码" @confirm="handleSearch" />
</view>
<button class="query-btn" @click="handleSearch">查询盘点任务</button>
</view>
<view class="task-section">
<view class="section-header">
<text class="section-title">待盘点任务</text>
<text class="section-action" @click="loadTasks">刷新</text>
</view>
<view v-if="taskList.length === 0" class="empty-state">
<text class="empty-text">暂无待盘点任务</text>
</view>
<view v-else>
<view class="task-card" v-for="item in taskList" :key="item.jobId" @click="selectTask(item)">
<view class="task-header">
<text class="task-type">{{ item.typeName }}</text>
<text class="task-no">{{ item.taskNo }}</text>
</view>
<view class="card-info">
<text class="card-title">预留界面</text>
<text class="card-desc">后续盘点流程启用时接入</text>
<view class="task-body">
<view class="task-line">
<text class="line-label">托盘</text>
<text class="line-value">{{ item.trayCode || '-' }}</text>
</view>
<view class="task-line">
<text class="line-label">物料</text>
<text class="line-value">{{ item.materialCode || '-' }}</text>
</view>
<view class="task-line">
<text class="line-label">数量</text>
<text class="line-value highlight">{{ item.quantity || 0 }}</text>
</view>
</view>
<view class="task-footer">
<text class="task-time">{{ item.createTime }}</text>
<text class="task-status" :class="item.statusClass">{{ item.statusName }}</text>
</view>
</view>
<uni-icons type="right" size="18" color="#ddd"></uni-icons>
</view>
</view>
<view v-if="scanCode" class="result-card">
<view class="result-header">
<text class="result-title">最近扫描</text>
<text class="result-code">{{ scanCode }}</text>
<view class="form-section" v-if="inventoryInfo">
<text class="form-title">盘点信息</text>
<view class="info-row">
<text class="info-label">任务号</text>
<text class="info-value">{{ inventoryInfo.taskNo }}</text>
</view>
<text class="result-desc">扫码盘点明细待业务流程确认后接入。</text>
<view class="info-row">
<text class="info-label">托盘号</text>
<text class="info-value">{{ inventoryInfo.trayCode }}</text>
</view>
<view class="info-row">
<text class="info-label">原货位</text>
<text class="info-value">{{ inventoryInfo.locationCode || '-' }}</text>
</view>
<view class="info-row">
<text class="info-label">盘点总数</text>
<text class="info-value highlight">{{ calculateActualTotal() }}</text>
</view>
<view class="phase-tip" :class="{ ready: inventoryInfo.executionPhase === '5' }">
{{ inventoryInfo.executionPhase === '5' ? '已到达出口,可确认盘点' : '等待 WCS 出库完成后再确认' }}
</view>
</view>
<view class="form-section" v-if="inventoryInfo">
<text class="form-title">物料核对</text>
<view class="material-card" v-for="(item, index) in inventoryInfo.materials" :key="item.jobDetailId || index">
<view class="material-header">
<text class="material-code">{{ item.materialCode }}</text>
<text class="diff-badge" :class="{ changed: isChanged(item) }">
{{ isChanged(item) ? '有差异' : '一致' }}
</text>
</view>
<text class="material-name">{{ item.materialName || '-' }}</text>
<view class="material-meta">
<text>批次:{{ item.lotNo || '-' }}</text>
<text>账面:{{ item.quantity || 0 }}</text>
<text>库存:{{ item.stockQuantity || 0 }}</text>
</view>
<view class="material-confirm">
<text class="material-confirm-label">实盘数量</text>
<input class="material-input" v-model="item.actualQuantity" type="number" :placeholder="'账面 ' + item.quantity" />
</view>
</view>
</view>
<view class="btn-section" v-if="inventoryInfo">
<button class="btn btn-primary" @click="submitInventory">提交盘点</button>
<button class="btn btn-default" @click="resetForm">重置</button>
</view>
</view>
</template>
<script>
import { getInventoryTaskList, getInventoryDetail, confirmInventory } from '@/api/warehouse/inventory'
export default {
data() {
return {
scanCode: ''
taskNo: '',
barcode: '',
taskList: [],
inventoryInfo: null
}
},
onLoad(options) {
if (options.taskNo) {
this.taskNo = options.taskNo
}
this.loadTasks()
},
onShow() {
this.loadTasks()
},
methods: {
scanInventory() {
loadTasks() {
var that = this
getInventoryTaskList().then(function(res) {
that.taskList = res.data || []
}).catch(function() {
that.taskList = []
})
},
startScan() {
var that = this
uni.scanCode({
success: res => {
this.scanCode = res.result || ''
this.$modal.showToast('扫码成功')
onlyFromCamera: false,
scanType: ['barCode', 'qrCode'],
success: function(res) {
that.barcode = res.result || ''
that.handleSearch()
}
})
},
showReservedTip() {
this.$modal.showToast('功能预留')
selectTask(item) {
this.taskNo = item.taskNo || ''
this.barcode = item.trayCode || ''
this.handleSearch()
},
handleSearch() {
if (!this.taskNo && !this.barcode) {
this.$modal.msg('请输入任务号或托盘码')
return
}
var that = this
getInventoryDetail({
taskNo: this.taskNo,
barcode: this.barcode
}).then(function(res) {
var data = res.data || {}
var materials = (data.materials || []).map(function(item) {
return Object.assign({}, item, {
actualQuantity: String(item.quantity != null ? item.quantity : 0)
})
})
that.inventoryInfo = Object.assign({}, data, { materials: materials })
that.taskNo = data.taskNo || that.taskNo
that.barcode = data.trayCode || that.barcode
that.$modal.msgSuccess('盘点任务获取成功')
}).catch(function() {
that.inventoryInfo = null
})
},
submitInventory() {
if (!this.inventoryInfo || !this.inventoryInfo.materials || this.inventoryInfo.materials.length === 0) {
this.$modal.msg('未获取到盘点物料明细')
return
}
if (this.inventoryInfo.executionPhase !== '5') {
this.$modal.msg('WCS 出库完成后才能确认盘点')
return
}
var invalid = this.inventoryInfo.materials.find(function(item) {
return item.actualQuantity === '' || item.actualQuantity === null || Number(item.actualQuantity) < 0
})
if (invalid) {
this.$modal.msg('请输入每个物料的实盘数量')
return
}
var data = {
taskNo: this.inventoryInfo.taskNo || this.taskNo,
barcode: this.inventoryInfo.trayCode || this.barcode,
trayCode: this.inventoryInfo.trayCode || this.barcode,
materials: this.inventoryInfo.materials.map(function(item) {
return {
jobDetailId: item.jobDetailId,
stockDetailId: item.stockDetailId,
stockpdDetailId: item.stockpdDetailId,
quantity: item.actualQuantity
}
})
}
var that = this
confirmInventory(data).then(function() {
that.$modal.msgSuccess('盘点确认成功,已生成回库作业')
that.resetForm()
that.loadTasks()
})
},
resetForm() {
this.taskNo = ''
this.barcode = ''
this.inventoryInfo = null
},
calculateActualTotal() {
if (!this.inventoryInfo || !this.inventoryInfo.materials) {
return '0'
}
return String(this.inventoryInfo.materials.reduce(function(sum, item) {
return sum + Number(item.actualQuantity || 0)
}, 0))
},
isChanged(item) {
return Number(item.actualQuantity || 0) !== Number(item.quantity || 0)
}
}
}
@@ -63,101 +239,338 @@
<style lang="scss" scoped>
page {
background-color: #f5f7fa;
background-color: #f4f6f8;
min-height: 100%;
}
.page-container {
padding: 24rpx;
padding-bottom: 60rpx;
}
.menu-section {
margin-bottom: 32rpx;
}
.menu-card {
.scan-section,
.task-card,
.form-section {
background: #fff;
border-radius: 16rpx;
padding: 32rpx 28rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(24, 38, 55, 0.05);
}
.scan-section {
padding: 28rpx;
margin-bottom: 28rpx;
}
.scan-header {
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
align-items: flex-start;
margin-bottom: 22rpx;
}
.reserve-card {
opacity: 0.88;
.scan-title {
display: block;
font-size: 32rpx;
font-weight: 600;
color: #243126;
}
.card-left {
.scan-subtitle {
display: block;
margin-top: 6rpx;
font-size: 23rpx;
color: #7c8a80;
}
.scan-btn {
display: flex;
align-items: center;
flex: 1;
padding: 8rpx 20rpx;
border: 1rpx solid #1f8f4d;
border-radius: 8rpx;
}
.card-icon {
width: 72rpx;
height: 72rpx;
.scan-btn-text {
font-size: 24rpx;
color: #1f8f4d;
margin-left: 8rpx;
}
.input-row {
display: flex;
align-items: center;
border: 1rpx solid #e5e9ec;
border-radius: 10rpx;
margin-bottom: 16rpx;
padding: 0 18rpx;
}
.input-label {
width: 120rpx;
flex-shrink: 0;
font-size: 26rpx;
color: #657069;
}
.scan-input {
flex: 1;
min-width: 0;
height: 76rpx;
font-size: 28rpx;
color: #202724;
}
.query-btn {
margin-top: 6rpx;
height: 78rpx;
line-height: 78rpx;
border-radius: 12rpx;
background: #1f8f4d;
color: #fff;
font-size: 29rpx;
}
.task-section {
margin-bottom: 28rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 18rpx;
}
.section-title {
font-size: 30rpx;
font-weight: 600;
color: #25302a;
}
.section-action {
font-size: 26rpx;
color: #1f8f4d;
}
.empty-state {
background: #fff;
border-radius: 16rpx;
padding: 72rpx 0;
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
}
.card-icon.green {
background: linear-gradient(135deg, #52c41a, #73d13d);
.empty-text {
font-size: 27rpx;
color: #a9b2ad;
}
.card-icon.gray {
background: linear-gradient(135deg, #8c8c8c, #bfbfbf);
.task-card {
padding: 24rpx;
margin-bottom: 18rpx;
}
.card-info {
display: flex;
flex-direction: column;
}
.card-title {
font-size: 30rpx;
font-weight: 500;
color: #333;
margin-bottom: 8rpx;
}
.card-desc {
font-size: 24rpx;
color: #999;
}
.result-card {
background: #fff;
border-radius: 16rpx;
padding: 28rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
}
.result-header {
.task-header,
.task-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14rpx;
}
.result-title {
.task-header {
margin-bottom: 16rpx;
}
.task-type {
font-size: 28rpx;
font-weight: 500;
color: #333;
font-weight: 600;
color: #1f8f4d;
}
.result-code {
.task-no,
.task-time {
font-size: 23rpx;
color: #8b9690;
}
.task-line {
display: flex;
justify-content: space-between;
padding: 8rpx 0;
}
.line-label,
.info-label {
font-size: 26rpx;
color: #52c41a;
color: #8b9690;
}
.result-desc {
.line-value,
.info-value {
font-size: 26rpx;
color: #26302b;
max-width: 460rpx;
text-align: right;
word-break: break-all;
}
.highlight {
color: #1f8f4d;
font-weight: 600;
}
.task-footer {
margin-top: 14rpx;
padding-top: 14rpx;
border-top: 1rpx solid #eef1ef;
}
.task-status {
font-size: 22rpx;
padding: 5rpx 16rpx;
border-radius: 8rpx;
}
.task-status.pending {
color: #9b6b00;
background: #fff6dc;
}
.task-status.processing {
color: #1c6a9e;
background: #e8f4ff;
}
.form-section {
padding: 28rpx;
margin-bottom: 24rpx;
}
.form-title {
display: block;
font-size: 30rpx;
font-weight: 600;
color: #25302a;
margin-bottom: 20rpx;
padding-bottom: 16rpx;
border-bottom: 1rpx solid #eef1ef;
}
.info-row {
display: flex;
justify-content: space-between;
padding: 14rpx 0;
}
.phase-tip {
margin-top: 16rpx;
padding: 14rpx 18rpx;
border-radius: 10rpx;
background: #fff6dc;
color: #9b6b00;
font-size: 24rpx;
color: #999;
}
.phase-tip.ready {
background: #e8f6ee;
color: #1f8f4d;
}
.material-card {
padding: 20rpx 0;
border-bottom: 1rpx solid #eef1ef;
}
.material-card:last-child {
border-bottom: 0;
}
.material-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8rpx;
}
.material-code {
font-size: 28rpx;
font-weight: 600;
color: #25302a;
}
.diff-badge {
font-size: 22rpx;
color: #1f8f4d;
background: #e8f6ee;
border-radius: 8rpx;
padding: 4rpx 14rpx;
}
.diff-badge.changed {
color: #b54416;
background: #fff0e8;
}
.material-name {
display: block;
font-size: 26rpx;
color: #58625d;
margin-bottom: 10rpx;
word-break: break-all;
}
.material-meta {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
font-size: 24rpx;
color: #8b9690;
}
.material-confirm {
display: flex;
align-items: center;
margin-top: 16rpx;
}
.material-confirm-label {
width: 150rpx;
flex-shrink: 0;
font-size: 26rpx;
color: #657069;
}
.material-input {
flex: 1;
min-width: 0;
height: 70rpx;
box-sizing: border-box;
border: 1rpx solid #dfe5e1;
border-radius: 10rpx;
padding: 0 18rpx;
font-size: 27rpx;
color: #202724;
}
.btn-section {
padding: 8rpx 0 24rpx;
}
.btn {
font-size: 30rpx;
border-radius: 12rpx;
margin-bottom: 18rpx;
}
.btn-primary {
background: #1f8f4d;
color: #fff;
}
.btn-default {
background: #fff;
color: #657069;
border: 1rpx solid #dfe5e1;
}
</style>