pda-uni-app-perkins/pages/stock-in/raw.vue

615 lines
16 KiB
Vue
Raw Permalink Normal View History

2025-10-31 22:49:51 +08:00
<template>
<view class="container">
<view class="header">
<button class="back" @click="back"><i class="icon icon-arrow_back" style="color:#05DCEF;font-size:36rpx"></i></button>
<text class="title">原材料入库</text>
</view>
<view class="content">
<view class="tip">
<view class="tip-icon"><i class="icon icon-info" style="color:#05DCEF"></i></view>
<text class="tip-text">请输入母托号并逐个绑定原材料最后确认入库</text>
</view>
<view class="field">
<text class="label">母托号 <text class="required">*</text></text>
<view class="input-wrap">
<i class="left-icon icon icon-qr_code_2" style="font-size:36rpx"></i>
<input class="input" placeholder="请扫描或输入母托号" v-model="vehicleNo" />
</view>
</view>
<view class="field">
<text class="label">原材料号</text>
<view class="input-wrap">
<i class="left-icon icon icon-qr_code_2" style="font-size:36rpx"></i>
<input class="input" placeholder="请扫描或输入原材料号" v-model="goodsId" />
</view>
</view>
<button class="action-button" :disabled="isBusy" @click="handleNextMaterial">
<i class="icon icon-arrow_forward" style="margin-right:10rpx"></i> 下一颗物料
</button>
<view class="field">
<text class="label">入库口 <text class="required">*</text></text>
<view class="input-wrap">
<i class="left-icon icon icon-qr_code_2" style="font-size:36rpx"></i>
<input class="input" placeholder="请扫描或输入入库口" v-model="standId" />
</view>
</view>
<view class="field">
<text class="label">原材料类型 <text class="required">*</text></text>
<view class="input-wrap" style="padding: 0 10rpx;">
<i class="left-icon icon icon-category" style="font-size:36rpx"></i>
<picker class="picker" @change="handleMaterialTypeChange" :value="materialTypeIndex" :range="materialTypeOptions">
<view class="picker-view">
{{ materialTypeOptions[materialTypeIndex] }}
</view>
</picker>
</view>
</view>
2025-10-31 22:49:51 +08:00
<view class="list-section">
<view class="list-header">
<text class="list-title">已绑定的原材料</text>
<text class="list-count">({{ orderInList.length }})</text>
</view>
<view v-if="!orderInList.length" class="empty-placeholder">
<i class="icon icon-info" style="margin-right:8rpx;color:#05DCEF"></i>
<text>暂无数据</text>
</view>
<view v-else class="card-group">
<view v-for="item in orderInList" :key="item.rowId" class="card">
<view class="card-header">
<view class="card-header-text">
<text class="card-title">物料号{{ item.goodsId }}</text>
<text class="card-subtitle">数量{{ formatNumber(item.goodsNum) }}</text>
</view>
<button class="card-action" :disabled="isBusy" @click="deleteItem(item)">
<i class="icon icon-delete" style="color:#ff4d4f;font-size:32rpx"></i>
</button>
</view>
<view class="card-row">
<text class="card-label">批次</text>
<text class="card-value">{{ item.batchNo || '-' }}</text>
</view>
<view class="card-row">
<text class="card-label">生产日期</text>
<text class="card-value">{{ formatDate(item.productionDate) }}</text>
</view>
<view class="card-row">
<text class="card-label">机型</text>
<text class="card-value">{{ item.model || '-' }}</text>
</view>
</view>
</view>
</view>
<button class="submit" :disabled="isBusy" @click="submitRawIn">
<i class="icon icon-task_alt" style="margin-right:10rpx"></i> 确认入库
</button>
</view>
</view>
</template>
<script>
import { WmsApiClient } from '@/common/wmsApi.js';
import { DialogUtils } from '@/utils/dialog.js';
export default {
data() {
return {
vehicleNo: '',
goodsId: '',
standId: '',
orderInList: [],
loadingAction: '',
materialTypeIndex: 0,
materialTypeOptions: ['整托', '散料'],
2025-10-31 22:49:51 +08:00
};
},
computed: {
isBusy() {
return this.loadingAction !== '';
},
},
methods: {
back() {
uni.navigateBack();
},
/**
* 解析错误信息
* @param {Error|string|object} error - 错误对象
* @param {string} fallback - 默认错误消息
* @returns {string} 格式化的错误消息
*/
2025-10-31 22:49:51 +08:00
resolveError(error, fallback = '请求失败') {
if (!error) return fallback;
if (typeof error === 'string') return error;
if (typeof error.message === 'string') return error.message;
if (typeof error.errMsg === 'string') return error.errMsg;
if (typeof error.code === 'number') return `${fallback} (${error.code})`;
return fallback;
},
/**
* 验证输入字段
* @param {string} vehicleNo - 母托号
* @param {string} standId - 入库口可选
* @returns {string|null} 验证错误消息验证通过返回 null
*/
validateInputs(vehicleNo, standId = null) {
const vehicle = (vehicleNo || '').trim();
if (!vehicle) {
return '请输入母托号';
}
if (standId !== null) {
const stand = (standId || '').trim();
if (!stand) {
return '请输入入库口';
}
}
return null;
},
2025-10-31 22:49:51 +08:00
formatNumber(value) {
const num = Number(value);
if (Number.isNaN(num)) return '0';
return num % 1 === 0 ? String(num) : num.toFixed(2);
},
formatDate(value) {
if (!value) return '-';
const parsed = new Date(value);
if (!Number.isNaN(parsed.getTime())) {
const y = parsed.getFullYear();
const m = String(parsed.getMonth() + 1).padStart(2, '0');
const d = String(parsed.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
}
const normalized = value.replace('T', ' ').split(' ')[0];
return normalized || '-';
},
/**
* 处理原材料类型选择变化
* @param {Object} event - picker 事件对象
*/
handleMaterialTypeChange(event) {
this.materialTypeIndex = event.detail.value;
},
/**
* 获取原材料类型对应的 int
* @returns {number} 1-整托, 2-散料
*/
getMaterialTypeValue() {
return this.materialTypeIndex + 1;
},
/**
* 显示确认弹窗
* @returns {Promise<boolean>} 用户是否确认提交
*/
showConfirmDialog() {
const materialType = this.materialTypeOptions[this.materialTypeIndex];
const content = `入库口:${this.standId}\n母托号${this.vehicleNo}\n原材料类型${materialType}\n已绑定物料${this.orderInList.length}\n\n请确认以上信息无误后提交`;
return DialogUtils.showConfirm(
'确认原材料入库',
content,
'确认提交',
'取消'
);
},
2025-10-31 22:49:51 +08:00
async handleNextMaterial() {
if (this.isBusy) return;
// 验证母托号
const validateError = this.validateInputs(this.vehicleNo);
if (validateError) {
DialogUtils.showWarningMessage('提示', validateError);
2025-10-31 22:49:51 +08:00
return;
}
const vehicle = this.vehicleNo.trim();
const goods = (this.goodsId || '').trim();
2025-10-31 22:49:51 +08:00
if (goods) {
this.loadingAction = 'binding';
uni.showLoading({ title: '正在绑定物料', mask: true });
try {
const bindRes = await WmsApiClient.bindVehicle({
vehicleNo: vehicle,
goodsId: goods,
});
const code = Number(bindRes.code);
if (code === 0) {
DialogUtils.showSuccessMessage('成功', '物料绑定成功');
this.goodsId = '';
} else {
DialogUtils.showWarningMessage('绑定未成功', `${bindRes.message || '操作失败'} (${bindRes.code})`);
}
} catch (error) {
DialogUtils.showErrorMessage('错误', this.resolveError(error));
} finally {
uni.hideLoading();
this.loadingAction = '';
}
}
this.loadingAction = 'query';
uni.showLoading({ title: '获取物料列表', mask: true });
try {
await this.loadOrderList(vehicle);
} catch (error) {
DialogUtils.showErrorMessage('错误', this.resolveError(error));
} finally {
uni.hideLoading();
this.loadingAction = '';
}
},
async loadOrderList(vehicle) {
const listRes = await WmsApiClient.getOrderInWithVehicleNo({ vehicleNo: vehicle });
const code = Number(listRes.code);
if (code === 0) {
const data = Array.isArray(listRes.returnData) ? listRes.returnData : [];
this.orderInList = data.map((item) => ({
...item,
goodsNum: item.goodsNum,
}));
} else {
DialogUtils.showWarningMessage('提示', `${listRes.message || '获取物料列表失败'} (${listRes.code})`);
}
},
async deleteItem(item) {
if (!item || !item.rowId) return;
if (this.isBusy) return;
// 验证母托号
const validateError = this.validateInputs(this.vehicleNo);
if (validateError) {
DialogUtils.showWarningMessage('提示', validateError);
return;
}
const vehicle = this.vehicleNo.trim();
2025-10-31 22:49:51 +08:00
this.loadingAction = 'deleting';
uni.showLoading({ title: '正在删除', mask: true });
try {
const res = await WmsApiClient.deleteOrderIn({ rowId: item.rowId });
const code = Number(res.code);
if (code === 0) {
DialogUtils.showSuccessMessage('成功', '物料删除成功');
await this.loadOrderList(vehicle);
} else {
DialogUtils.showWarningMessage('删除未成功', `${res.message || '删除失败'} (${res.code})`);
}
} catch (error) {
DialogUtils.showErrorMessage('错误', this.resolveError(error));
} finally {
uni.hideLoading();
this.loadingAction = '';
}
},
async submitRawIn() {
if (this.isBusy) return;
// 验证表单输入
const validateError = this.validateInputs(this.vehicleNo, this.standId);
if (validateError) {
DialogUtils.showWarningMessage('提示', validateError);
2025-10-31 22:49:51 +08:00
return;
}
// 验证已绑定物料
2025-10-31 22:49:51 +08:00
if (!this.orderInList.length) {
DialogUtils.showWarningMessage('提示', '请先绑定原材料');
return;
}
// 显示确认弹窗
const confirmed = await this.showConfirmDialog();
if (!confirmed) {
return;
}
2025-10-31 22:49:51 +08:00
this.loadingAction = 'submitting';
uni.showLoading({ title: '正在提交', mask: true });
try {
const vehicle = this.vehicleNo.trim();
const stand = this.standId.trim();
const res = await WmsApiClient.rawStockIn({
vehicleNo: vehicle,
standId: stand,
orderInType: this.getMaterialTypeValue(),
});
2025-10-31 22:49:51 +08:00
const code = Number(res.code);
if (code === 0) {
DialogUtils.showSuccessMessage('成功', '原材料入库成功');
this.vehicleNo = '';
this.goodsId = '';
this.standId = '';
this.materialTypeIndex = 0;
2025-10-31 22:49:51 +08:00
this.orderInList = [];
} else {
DialogUtils.showWarningMessage('入库未成功', `${res.message || '提交失败'} (${res.code})`);
}
} catch (error) {
DialogUtils.showErrorMessage('错误', this.resolveError(error));
} finally {
uni.hideLoading();
this.loadingAction = '';
}
},
},
};
</script>
<style scoped>
.header {
position: relative;
height: 120rpx;
padding: 0;
background: linear-gradient(90deg, var(--grad-primary-start), var(--grad-primary-mid));
display: flex;
align-items: center;
justify-content: center;
}
.container {
min-height: 100vh;
background: #F5F5F5;
}
.title {
color: #fff;
font-size: 32rpx;
font-weight: 600;
}
.content {
padding: 24rpx;
padding-bottom: 56rpx;
}
.tip {
flex-direction: row;
display: flex;
align-items: center;
background: #f2fdff;
border-radius: 12rpx;
padding: 24rpx;
color: #333;
margin-bottom: 28rpx;
}
.tip-icon {
width: 40rpx;
height: 40rpx;
border-radius: 20rpx;
background: #e6f7ff;
text-align: center;
line-height: 40rpx;
margin-right: 12rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
}
.tip-text {
color: #333;
font-size: 26rpx;
}
.field {
margin-bottom: 24rpx;
}
.label {
color: #333;
font-size: 30rpx;
margin-bottom: 12rpx;
display: block;
}
.required {
color: #ff4d4f;
}
.input-wrap {
height: 96rpx;
border-radius: 12rpx;
border: 1px solid #e6e6e6;
background: #fff;
padding: 0 20rpx;
display: flex;
align-items: center;
}
.left-icon {
color: #05DCEF;
font-size: 36rpx;
margin-right: 12rpx;
}
.input {
flex: 1;
height: 100%;
font-size: 30rpx;
}
.action-button {
width: 100%;
height: 92rpx;
border: none;
color: #fff;
border-radius: 46rpx;
background: linear-gradient(90deg, var(--grad-primary-start), var(--grad-primary-mid));
font-size: 32rpx;
box-shadow: 0 8rpx 22rpx rgba(5, 220, 239, .25);
text-align: center;
margin-bottom: 28rpx;
display: flex;
align-items: center;
justify-content: center;
}
.action-button[disabled] {
opacity: .6;
}
.list-section {
margin-top: 12rpx;
margin-bottom: 32rpx;
}
.list-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
}
.list-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
.list-count {
font-size: 28rpx;
color: #888;
}
.empty-placeholder {
height: 140rpx;
border-radius: 12rpx;
border: 1px dashed #d9d9d9;
color: #999;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
}
.card-group {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
box-shadow: 0 8rpx 22rpx rgba(0, 0, 0, .05);
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12rpx;
}
.card-header-text {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.card-title {
font-size: 30rpx;
color: #111;
font-weight: 600;
}
.card-subtitle {
font-size: 26rpx;
color: #555;
}
.card-action {
width: 64rpx;
height: 64rpx;
border-radius: 32rpx;
border: none;
background: rgba(255, 77, 79, .12);
display: flex;
align-items: center;
justify-content: center;
}
.card-action[disabled] {
opacity: .4;
}
.card-row {
display: flex;
align-items: center;
margin-top: 6rpx;
}
.card-label {
font-size: 26rpx;
color: #666;
margin-right: 8rpx;
}
.card-value {
font-size: 26rpx;
color: #333;
}
.submit {
width: 100%;
height: 92rpx;
border: none;
color: #fff;
border-radius: 46rpx;
background: linear-gradient(90deg, var(--grad-primary-start), var(--grad-primary-mid));
font-size: 32rpx;
box-shadow: 0 8rpx 22rpx rgba(52, 199, 89, .25);
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
.submit[disabled] {
opacity: .6;
}
.back {
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 80rpx;
height: 64rpx;
line-height: 64rpx;
border: none;
color: #05DCEF;
background: #fff;
border-radius: 12rpx;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
.picker {
flex: 1;
height: 100%;
display: flex;
align-items: center;
}
.picker-view {
height: 100%;
display: flex;
align-items: center;
font-size: 30rpx;
color: #333;
width: 100%;
}
2025-10-31 22:49:51 +08:00
</style>