Files
cashier_app/pageConsumables/batch_in.vue
2025-12-25 15:38:00 +08:00

774 lines
18 KiB
Vue
Raw 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">
<u-form ref="formRef" :model="form" :rules="rules" label-position="top">
<view class="card">
<u-form-item>
<view class="switch-wrap">
<view class="top">
<text class="t">第一步上传图片</text>
<view class="btn">
<u-button type="primary" :disabled="!img" shape="circle" text="开始解析" @click="startCheckOcrRes"></u-button>
</view>
</view>
<view class="info">
<my-upload-img v-model="img"></my-upload-img>
</view>
</view>
</u-form-item>
</view>
<view class="card" v-if="form.bodyList">
<u-form-item>
<view class="switch-wrap">
<view class="top">
<text class="t">第二步编辑信息</text>
</view>
<view class="top" style="margin-top: 10px">
<text class="t">供应商信息</text>
</view>
<view class="info">
<view style="flex: 1">
<selectVendor v-model="form.vendorId" />
</view>
</view>
</view>
</u-form-item>
<u-form-item>
<view class="switch-wrap">
<view class="top">
<text class="t">耗材信息</text>
<view class="btn">
<u-button type="primary" shape="circle" text="选择耗材" @click="openSelectCons" v-if="!stockInStatus"></u-button>
</view>
</view>
<view class="info">
<view class="list-wrap">
<view class="top-tips">
<text class="tb">{{ form.bodyList.length }}种耗材</text>
<text class="tb">
金额合计{{
multiplyAndFormat(
form.bodyList.reduce((sum, item) => sum + (item.purchasePrice || 0) * (item.inOutNumber || 0), 0),
1
)
}}
</text>
</view>
<view class="item" v-for="(item, index) in form.bodyList" :key="index">
<view class="name">
<text class="t">{{ item.conName }}</text>
<view class="error-text" v-if="stockInStatus">
<u-text type="success" v-if="item.conId" text="入库成功" size="12"></u-text>
<u-text v-else type="error" :text="`入库失败:${item.failReason}`" size="12"></u-text>
</view>
</view>
<view class="content">
<view class="ctt-item">
<text class="t1">单价</text>
<text class="t2">{{ item.purchasePrice }}</text>
</view>
<view class="ctt-item">
<text class="t1">单位</text>
<text class="t2">{{ item.unitName }}</text>
</view>
<view class="ctt-item">
<text class="t1">数量</text>
<text class="t2">{{ item.inOutNumber }}</text>
</view>
<view class="ctt-item">
<text class="t1">小计</text>
<text class="t2">{{ multiplyAndFormat(item.purchasePrice || 0, item.inOutNumber || 0) }}</text>
</view>
</view>
<view class="footer-btn-wrap">
<view>
<u-text type="primary" text="编辑" @click="editorFormHandle(item, index)"></u-text>
</view>
<view>
<u-text type="error" text="删除" @click="form.bodyList.splice(index, 1)"></u-text>
</view>
</view>
</view>
</view>
</view>
</view>
</u-form-item>
<u-form-item>
<view class="result_wrap" v-if="stockInStatus">
上传完成 {{ form.bodyList.length }}条数据其中成功
<u-text type="success" :text="form.bodyList.filter((item) => item.conId).length"></u-text>
失败
<u-text type="error" :text="form.bodyList.filter((item) => !item.conId).length"></u-text>
</view>
</u-form-item>
</view>
</u-form>
<u-popup :show="showEditorPopup" round="20" closeable @close="showEditorPopup = false">
<view class="editor-popup" v-if="form.bodyList && form.bodyList.length">
<view class="title">
<text class="t">{{ form.bodyList[editorIndex].conName }}</text>
</view>
<view class="form">
<u-form ref="editorFormRef" :model="editorForm" :rules="editorFormRules" label-width="60px">
<u-form-item label="名称" prop="conName" required>
<u-input v-model="editorForm.conName" :maxlength="50" placeholder="请输入耗材名称"></u-input>
</u-form-item>
<u-form-item label="单价" prop="purchasePrice" required>
<u-input
v-model="editorForm.purchasePrice"
placeholder="请输入耗材单价"
:maxlength="8"
@change="
(e) =>
mySetTimeout(() => {
editorForm.purchasePrice = filterNumberInput(e);
}, 50)
"
>
<template #suffix>
<text></text>
</template>
</u-input>
</u-form-item>
<u-form-item label="单位" prop="unitName" required>
<u-input v-model="editorForm.unitName" :maxlength="20" placeholder="请选择耗材单位"></u-input>
</u-form-item>
<u-form-item label="数量" prop="inOutNumber" required>
<u-input
v-model="editorForm.inOutNumber"
placeholder="请输入耗材数量"
:maxlength="8"
@change="
(e) =>
mySetTimeout(() => {
editorForm.inOutNumber = filterNumberInput(e, 1);
}, 50)
"
></u-input>
</u-form-item>
</u-form>
</view>
<view class="footer">
<view class="btn">
<u-button style="width: 100%" shape="circle" @click="showEditorPopup = false">取消</u-button>
</view>
<view class="btn">
<u-button type="primary" style="width: 100%" shape="circle" @click="editorFormSubmitHandle">确认</u-button>
</view>
</view>
</view>
</u-popup>
<my-footer-btn type="horizontal" showCancel @confirm="submitHandle" @cancel="backHandle"></my-footer-btn>
<!-- 选择耗材 -->
<selectCons ref="selectConsRef" @success="selectConsSuccess" />
<view class="loading-page" v-if="checkLoading">
<view class="loader"></view>
<text class="t">{{ loadingText }}</text>
<view class="btn">
<u-button type="primary" @click="closeCheckOcrHandle">取消查询</u-button>
</view>
</view>
</view>
</template>
<script setup>
import _ from 'lodash';
import { ref, computed } from 'vue';
import { multiplyAndFormat, filterNumberInput } from '@/utils/index.js';
import { stockOcr, ocrResult } from '@/http/api/product.js';
import { consStockIn } from '@/http/api/cons.js';
import selectVendor from './components/select-vendor.vue';
import selectCons from './components/select-cons.vue';
const selectConsRef = ref(null);
function selectConsSuccess(e) {
console.log('selectVendorHandle===', e);
let arr = e.map((item) => ({
id: item.id,
conId: item.id,
conName: item.conName,
purchasePrice: item.price,
unitName: item.conUnit,
inOutNumber: 1
}));
form.value.bodyList.push(...arr);
}
function openSelectCons() {
const comp = selectConsRef.value;
if (comp && typeof comp.show === 'function') {
comp.show();
} else {
console.warn('selectConsRef not ready or show() not available', comp);
}
}
// 封装全局定时器,适配多端
const mySetTimeout = (fn, delay) => {
if (typeof uni !== 'undefined') {
return setTimeout(fn, delay); // 小程序
} else {
return window.setTimeout(fn, delay); // H5
}
};
// 编辑耗材 start
const showEditorPopup = ref(false);
const editorFormRef = ref(null);
const editorIndex = ref(0);
const editorForm = ref({
conName: '',
purchasePrice: '',
unitName: '',
inOutNumber: ''
});
const editorFormRules = ref({
conName: [
{
required: true,
validator: (rule, value, callback) => {
if (editorForm.value.conName === '') {
return callback(new Error('请输入耗材名称'));
} else {
return true;
}
}
}
],
purchasePrice: [
{
required: true,
validator: (rule, value, callback) => {
if (editorForm.value.purchasePrice === '') {
return callback(new Error('请输入耗材单价'));
} else {
return true;
}
}
}
],
unitName: [
{
required: true,
validator: (rule, value, callback) => {
if (editorForm.value.unitName === '') {
return callback(new Error('请选择耗材单位'));
} else {
return true;
}
}
}
],
inOutNumber: [
{
required: true,
validator: (rule, value, callback) => {
if (editorForm.value.inOutNumber === '') {
return callback(new Error('请输入耗材数量'));
} else {
return true;
}
}
}
]
});
// 显示编辑
function editorFormHandle(item, index) {
editorForm.value = _.cloneDeep(item);
editorIndex.value = index;
showEditorPopup.value = true;
}
function editorFormSubmitHandle() {
editorFormRef.value
.validate()
.then(() => {
form.value.bodyList[editorIndex.value] = _.cloneDeep(editorForm.value);
showEditorPopup.value = false;
})
.catch(() => {});
}
// 编辑耗材 end
const resId = ref(null);
const img = ref('');
const stockInStatus = ref(false);
const form = ref({});
// const form = ref({
// actualPaymentAmount: null,
// amountPayable: 1680,
// batchNo: 'XS25110410003',
// bodyList: [
// { conId: '', conName: '哈根达斯-小杯(草莓)', failReason: '', inOutNumber: 24, purchasePrice: 35, subTotal: 840, unitName: '杯' },
// { conId: '', conName: '哈根达斯-小杯(香草)', failReason: '', inOutNumber: 24, purchasePrice: 35, subTotal: 840, unitName: '杯' },
// { conId: '', conName: '哈根达斯-小杯(比利时巧克力)', failReason: '', inOutNumber: 12, purchasePrice: 0, subTotal: 0, unitName: '杯' },
// { conId: '', conName: '哈根达斯-小杯(夏威夷果仁)', failReason: '', inOutNumber: 12, purchasePrice: 0, subTotal: 0, unitName: '杯' }
// ],
// inOutDate: '2025-11-04',
// ocrSaleOrder: {
// customerName: '大客河景餐厅',
// date: '2025-11-04',
// documentType: '销售单',
// items: [
// { conName: '哈根达斯-小杯(草莓)', inOutNumber: '24', purchasePrice: '35', spec: '81g', subTotal: '840', unitName: '杯' },
// { conName: '哈根达斯-小杯(香草)', inOutNumber: '24', purchasePrice: '35', spec: '81g', subTotal: '840', unitName: '杯' },
// { conName: '哈根达斯-小杯(比利时巧克力)', inOutNumber: '12', purchasePrice: '0', spec: '81g', subTotal: '0', unitName: '杯' },
// { conName: '哈根达斯-小杯(夏威夷果仁)', inOutNumber: '12', purchasePrice: '0', spec: '81g', subTotal: '0', unitName: '杯' }
// ],
// operator: '陈钦楠',
// orderNumber: 'XS25110410003',
// remark: '',
// totalAmount: '1680'
// },
// paymentDate: null,
// remark: '',
// unInCons: [],
// vendorId: null
// });
const rules = ref({});
// 查询OCR结果
async function startCheckOcrRes() {
try {
resId.value = await stockOcr({ url: img.value });
stockInStatus.value = false;
startQueryInterval()
.then((res) => {
// console.log('查询成功', JSON.stringify(res));
form.value = res;
})
.catch((error) => {
console.error('查询失败', error);
});
} catch (error) {
console.log(error);
}
}
// 开始查询ocr结果
// 启动查询方法每5秒查询一次五分钟后超时停止查询
const checkLoading = ref(false);
const speed = 10000; // 查询间隔时间,单位毫秒
const timeout = 300000; // 超时时间,单位毫秒
// const timeout = 15000; // 超时时间,单位毫秒
const interval = ref(null);
const checkNumber = ref(0);
const loadingText = computed(() => `${speed / 1000}秒查询1次已查询${checkNumber.value}`);
function startQueryInterval() {
return new Promise((resolve, reject) => {
if (!resId.value) {
reject(new Error('无效的 resId无法查询'));
return;
}
// 重置计数并显示 loading
checkNumber.value = 0;
checkLoading.value = true;
const startTime = Date.now();
interval.value = null;
async function checkOnce() {
try {
checkNumber.value++;
console.log('ocr checkNumber:', checkNumber.value);
checkLoading.value = true;
const res = await ocrResult({ id: resId.value });
if (res && res.batchNo) {
uni.showToast({
title: '查询成功',
icon: 'none'
});
setTimeout(() => {
checkLoading.value = false;
}, 1000);
clearInterval(interval.value);
resolve(res);
return;
}
// 如果还未超时,则继续等待下一轮查询
if (Date.now() - startTime >= timeout) {
setTimeout(() => {
checkLoading.value = false;
}, 1000);
uni.showToast({
title: '查询超时',
icon: 'none'
});
clearInterval(interval.value);
reject(new Error('查询超时'));
}
} catch (error) {
uni.showToast({
title: '查询失败',
icon: 'none'
});
setTimeout(() => {
checkLoading.value = false;
}, 1000);
clearInterval(interval.value);
reject(error);
}
}
// 立即执行一次查询,然后按间隔继续查询
checkOnce();
interval.value = setInterval(checkOnce, speed);
});
}
// 取消查询
function closeCheckOcrHandle() {
checkLoading.value = false;
clearInterval(interval.value);
}
// 提交批量入库
async function submitHandle() {
try {
console.log('submitHandle', form.value);
uni.showLoading({
title: '提交中...',
mask: true
});
const res = await consStockIn(form.value);
form.value = res;
stockInStatus.value = true;
setTimeout(() => {
uni.showToast({
title: '提交成功',
icon: 'none'
});
}, 100);
} catch (error) {
console.log(error);
}
uni.hideLoading();
}
// 返回
function backHandle() {
uni.navigateBack();
}
</script>
<style>
page {
background-color: #f8f8f8;
}
</style>
<style scoped lang="scss">
.container {
padding: 28upx;
}
.card {
background-color: #fff;
border-radius: 20px;
padding: 28upx;
&:not(:last-child) {
margin-bottom: 28upx;
}
}
.switch-wrap {
flex: 1;
width: 100%;
.top {
display: flex;
align-items: center;
justify-content: space-between;
&.column {
align-items: flex-start;
flex-direction: column;
}
.two {
display: flex;
flex-direction: column;
padding-right: 28upx;
}
.t {
font-size: 32upx;
color: #333;
font-weight: bold;
}
.tips {
font-size: 24upx;
color: #666;
}
}
.info {
padding-top: 16upx;
display: flex;
align-items: center;
gap: 16upx;
.i {
font-size: 28upx;
color: #666;
}
.ipt {
flex: 1;
}
.t {
font-size: 24upx;
color: #666;
}
.list-wrap {
flex: 1;
.top-tips {
.t {
font-size: 28upx;
color: #333;
}
}
.item {
margin-top: 28upx;
background-color: #f8f8f8;
border-radius: 12upx;
padding: 20upx;
.name {
display: flex;
flex-direction: column;
.t {
font-size: 28upx;
color: #333;
}
.error-text {
display: flex;
}
}
.content {
display: flex;
padding: 28upx 0;
.ctt-item {
flex: 1;
display: flex;
flex-direction: column;
&:last-child {
align-items: flex-end;
}
.t1 {
color: #666;
font-size: 28upx;
}
.t2 {
color: #333;
font-size: 28upx;
}
}
}
.footer-btn-wrap {
display: flex;
gap: 28upx;
justify-content: flex-end;
.btn {
width: 160upx;
}
}
}
}
.package-wrap {
flex: 1;
.list {
.item {
border: 1px solid #ececec;
border-radius: 12upx;
padding: 20upx;
&:not(:first-child) {
margin-top: 28upx;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 20upx;
.t {
font-size: 28upx;
color: #333;
}
.del {
display: flex;
align-items: center;
gap: 12upx;
}
}
.input-wrap {
display: flex;
gap: 20upx;
padding-bottom: 20upx;
.btn {
display: flex;
align-items: center;
gap: 12upx;
.t {
color: #318afe;
}
}
}
.table-wrap {
padding-bottom: 20upx;
margin-bottom: 20upx;
.tab-head {
background-color: #f8f8f8;
}
.tr {
display: flex;
gap: 12upx;
padding: 20upx;
border-bottom: 1px solid #ececec;
.td {
flex: 1;
&:nth-child(1) {
flex: 2;
}
&:last-child {
flex: 0.5;
display: flex;
align-items: center;
justify-content: flex-end;
}
.t {
font-size: 28upx;
color: #333;
}
.del {
color: red;
font-size: 28upx;
}
}
}
}
.select_num_wrap {
padding-bottom: 28upx;
.label {
padding-bottom: 20upx;
.t {
color: #333;
font-size: 32upx;
}
}
}
}
}
.package-wrap-btn {
display: flex;
justify-content: center;
padding-top: 28upx;
.btn {
display: flex;
gap: 8upx;
align-items: center;
.t {
font-size: 28upx;
color: #318afe;
font-weight: bold;
}
}
}
}
.step-wrap {
flex: 1;
.table-wrap {
padding-bottom: 20upx;
margin-bottom: 20upx;
.tab-head {
background-color: #f8f8f8;
}
.tr {
display: flex;
gap: 12upx;
padding: 20upx;
border-bottom: 1px solid #ececec;
.td {
flex: 1;
display: flex;
gap: 20upx;
align-items: center;
&:last-child {
flex: 0.6;
}
.t {
font-size: 28upx;
color: #333;
}
.edit {
color: #318afe;
font-size: 28upx;
}
.del {
color: red;
font-size: 28upx;
}
}
}
}
}
}
}
.editor-popup {
padding: 0 28upx;
.title {
padding: 28upx 0;
display: flex;
justify-content: center;
.t {
font-size: 32upx;
font-weight: bold;
}
}
.form {
padding: 28upx;
}
.footer {
display: flex;
gap: 28upx;
.btn {
flex: 1;
}
}
}
.result_wrap {
display: flex;
}
.loading-page {
width: 100vw;
height: 100vh;
display: flex;
gap: 20upx;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #fff;
position: fixed;
top: 0;
left: 0;
z-index: 999;
padding-bottom: 10vh;
.loader {
width: 50px;
aspect-ratio: 1;
border-radius: 50%;
background: radial-gradient(farthest-side, #ffa516 94%, #0000) top/8px 8px no-repeat, conic-gradient(#0000 30%, #ffa516);
-webkit-mask: radial-gradient(farthest-side, #0000 calc(100% - 8px), #000 0);
animation: l13 1s infinite linear;
}
@keyframes l13 {
100% {
transform: rotate(1turn);
}
}
.t {
font-size: 28upx;
color: #666;
}
.btn {
position: absolute;
bottom: 10%;
left: 50%;
transform: translateX(-50%);
}
}
</style>