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

615 lines
16 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="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>