615 lines
16 KiB
Vue
615 lines
16 KiB
Vue
<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>
|
||
|
||
<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: ['整托', '散料'],
|
||
};
|
||
},
|
||
computed: {
|
||
isBusy() {
|
||
return this.loadingAction !== '';
|
||
},
|
||
},
|
||
methods: {
|
||
back() {
|
||
uni.navigateBack();
|
||
},
|
||
/**
|
||
* 解析错误信息
|
||
* @param {Error|string|object} error - 错误对象
|
||
* @param {string} fallback - 默认错误消息
|
||
* @returns {string} 格式化的错误消息
|
||
*/
|
||
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;
|
||
},
|
||
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,
|
||
'确认提交',
|
||
'取消'
|
||
);
|
||
},
|
||
async handleNextMaterial() {
|
||
if (this.isBusy) return;
|
||
|
||
// 验证母托号
|
||
const validateError = this.validateInputs(this.vehicleNo);
|
||
if (validateError) {
|
||
DialogUtils.showWarningMessage('提示', validateError);
|
||
return;
|
||
}
|
||
|
||
const vehicle = this.vehicleNo.trim();
|
||
const goods = (this.goodsId || '').trim();
|
||
|
||
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();
|
||
|
||
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);
|
||
return;
|
||
}
|
||
|
||
// 验证已绑定物料
|
||
if (!this.orderInList.length) {
|
||
DialogUtils.showWarningMessage('提示', '请先绑定原材料');
|
||
return;
|
||
}
|
||
|
||
// 显示确认弹窗
|
||
const confirmed = await this.showConfirmDialog();
|
||
if (!confirmed) {
|
||
return;
|
||
}
|
||
|
||
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(),
|
||
});
|
||
const code = Number(res.code);
|
||
if (code === 0) {
|
||
DialogUtils.showSuccessMessage('成功', '原材料入库成功');
|
||
this.vehicleNo = '';
|
||
this.goodsId = '';
|
||
this.standId = '';
|
||
this.materialTypeIndex = 0;
|
||
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%;
|
||
}
|
||
</style>
|