feat: 完善盘点出入库流程
This commit is contained in:
28
pda/api/warehouse/inventory.js
Normal file
28
pda/api/warehouse/inventory.js
Normal 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
|
||||
})
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user