Files
FTC25009/pda/pages/warehouse/inventory/index.vue
2026-06-25 18:31:17 +08:00

577 lines
14 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page-container">
<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>
</view>
<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="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>
</view>
</view>
<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>
<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 {
taskNo: '',
barcode: '',
taskList: [],
inventoryInfo: null
}
},
onLoad(options) {
if (options.taskNo) {
this.taskNo = options.taskNo
}
this.loadTasks()
},
onShow() {
this.loadTasks()
},
methods: {
loadTasks() {
var that = this
getInventoryTaskList().then(function(res) {
that.taskList = res.data || []
}).catch(function() {
that.taskList = []
})
},
startScan() {
var that = this
uni.scanCode({
onlyFromCamera: false,
scanType: ['barCode', 'qrCode'],
success: function(res) {
that.barcode = res.result || ''
that.handleSearch()
}
})
},
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)
}
}
}
</script>
<style lang="scss" scoped>
page {
background-color: #f4f6f8;
min-height: 100%;
}
.page-container {
padding: 24rpx;
padding-bottom: 60rpx;
}
.scan-section,
.task-card,
.form-section {
background: #fff;
border-radius: 16rpx;
box-shadow: 0 2rpx 10rpx rgba(24, 38, 55, 0.05);
}
.scan-section {
padding: 28rpx;
margin-bottom: 28rpx;
}
.scan-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 22rpx;
}
.scan-title {
display: block;
font-size: 32rpx;
font-weight: 600;
color: #243126;
}
.scan-subtitle {
display: block;
margin-top: 6rpx;
font-size: 23rpx;
color: #7c8a80;
}
.scan-btn {
display: flex;
align-items: center;
padding: 8rpx 20rpx;
border: 1rpx solid #1f8f4d;
border-radius: 8rpx;
}
.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;
}
.empty-text {
font-size: 27rpx;
color: #a9b2ad;
}
.task-card {
padding: 24rpx;
margin-bottom: 18rpx;
}
.task-header,
.task-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.task-header {
margin-bottom: 16rpx;
}
.task-type {
font-size: 28rpx;
font-weight: 600;
color: #1f8f4d;
}
.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: #8b9690;
}
.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;
}
.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>