<script setup lang="ts">
|
import { onMounted, ref } from 'vue';
|
import {
|
getMiscIssue,
|
getMiscIssuePage,
|
type MesWmMiscIssueApi,
|
} from '#/api/mes/wm/miscissue';
|
import {
|
createMiscIssueLine,
|
getMiscIssueLinePage,
|
type MesWmMiscIssueLineApi,
|
} from '#/api/mes/wm/miscissue/line';
|
import type { MesWmMaterialStockApi } from '#/api/mes/wm/materialstock';
|
import {
|
errorText,
|
findStocks,
|
lookup,
|
traceQuantity,
|
writeBlockReason,
|
type TraceResult,
|
} from '#/mobile/api';
|
import Scanner from './scanner.vue';
|
|
defineOptions({ name: 'MobileMiscIssue' });
|
|
type Issue = MesWmMiscIssueApi.MiscIssue;
|
type Line = MesWmMiscIssueLineApi.MiscIssueLine;
|
|
const busy = ref(false);
|
const error = ref('');
|
const notice = ref('');
|
const code = ref('');
|
const rows = ref<Issue[]>([]);
|
const page = ref(0);
|
const total = ref(0);
|
const issue = ref<Issue>();
|
const lines = ref<Line[]>([]);
|
const trace = ref<TraceResult>();
|
const stock = ref<MesWmMaterialStockApi.MaterialStock>();
|
const quantity = ref(1);
|
let filters: { code?: string } = {};
|
|
async function load(reset = true) {
|
if (busy.value) return;
|
busy.value = true;
|
error.value = '';
|
const next = reset ? 1 : page.value + 1;
|
const query = reset ? { code: code.value.trim() || undefined } : filters;
|
try {
|
const data = await getMiscIssuePage({ ...query, status: 0, pageNo: next, pageSize: 20 });
|
rows.value = reset ? data.list : [...rows.value, ...data.list];
|
page.value = next;
|
total.value = data.total;
|
filters = query;
|
} catch (e) {
|
error.value = errorText(e);
|
} finally {
|
busy.value = false;
|
}
|
}
|
|
async function readLines(issueId: number) {
|
const all: Line[] = [];
|
for (let n = 1; ; n++) {
|
const data = await getMiscIssueLinePage({ issueId, pageNo: n, pageSize: 100 });
|
all.push(...data.list);
|
if (all.length >= data.total || !data.list.length) break;
|
}
|
return all;
|
}
|
|
async function open(id?: number) {
|
if (!id || busy.value) return;
|
busy.value = true;
|
error.value = '';
|
notice.value = '';
|
trace.value = undefined;
|
stock.value = undefined;
|
try {
|
const detail = await getMiscIssue(id);
|
if (!detail) throw new Error('出库单不存在或已被删除');
|
issue.value = detail;
|
lines.value = await readLines(id);
|
} catch (e) {
|
error.value = errorText(e);
|
} finally {
|
busy.value = false;
|
}
|
}
|
|
async function identify(raw: string) {
|
if (busy.value || !issue.value) return;
|
busy.value = true;
|
error.value = '';
|
notice.value = '';
|
trace.value = undefined;
|
stock.value = undefined;
|
try {
|
const found = await lookup(raw);
|
const reason = writeBlockReason(found);
|
if (reason) throw new Error(reason);
|
const stocks = await findStocks(found);
|
const matched = stocks.find((item) => (item.quantity ?? 0) > 0) ?? stocks[0];
|
if (!matched) throw new Error('未找到该追溯码对应的库存记录');
|
trace.value = found;
|
stock.value = matched;
|
const max = traceQuantity(found);
|
quantity.value = max && matched.quantity ? Math.min(max, matched.quantity) : (max ?? matched.quantity ?? 1);
|
} catch (e) {
|
error.value = errorText(e);
|
} finally {
|
busy.value = false;
|
}
|
}
|
|
async function confirm() {
|
const current = issue.value;
|
const matched = stock.value;
|
if (!current?.id || !matched?.id || busy.value) return;
|
if (!Number.isFinite(quantity.value) || quantity.value <= 0) {
|
error.value = '出库数量必须大于零';
|
return;
|
}
|
if (!window.confirm(`确认出库 ${matched.batchCode || ''} ${quantity.value}?`)) return;
|
busy.value = true;
|
error.value = '';
|
try {
|
await createMiscIssueLine({
|
issueId: current.id,
|
itemId: matched.itemId,
|
materialStockId: matched.id,
|
quantity: quantity.value,
|
batchCode: matched.batchCode,
|
batchId: matched.batchId,
|
warehouseId: matched.warehouseId,
|
locationId: matched.locationId,
|
areaId: matched.areaId,
|
remark: `扫码来源 ${trace.value?.code ?? ''}`,
|
});
|
notice.value = '已添加出库明细。';
|
trace.value = undefined;
|
stock.value = undefined;
|
lines.value = await readLines(current.id);
|
} catch (e) {
|
error.value = errorText(e);
|
} finally {
|
busy.value = false;
|
}
|
}
|
|
onMounted(() => load());
|
</script>
|
|
<template>
|
<p v-if="notice" class="m-success" role="status">{{ notice }}</p>
|
<p v-if="error" class="m-error" role="alert">{{ error }}</p>
|
<template v-if="issue">
|
<button class="m-secondary" :disabled="busy" @click="issue = undefined; trace = undefined; stock = undefined; load()">
|
返回出库单列表
|
</button>
|
<section class="m-panel" style="margin-top: 16px">
|
<h2 class="m-title">{{ issue.code }}</h2>
|
<dl>
|
<div class="m-row"><dt>名称</dt><dd>{{ issue.name || '—' }}</dd></div>
|
<div class="m-row"><dt>备注</dt><dd>{{ issue.remark || '—' }}</dd></div>
|
</dl>
|
</section>
|
<h2 class="m-section-title">出库明细</h2>
|
<p v-if="!lines.length" class="m-empty">该单暂无出库明细</p>
|
<article v-for="line in lines" :key="line.id" class="m-panel">
|
<h3 class="m-title">{{ line.itemName || line.itemCode }}</h3>
|
<p>出库数量 {{ line.quantity ?? 0 }}</p>
|
<p class="m-muted">批次 {{ line.batchCode || '未录入' }}</p>
|
</article>
|
<h2 class="m-section-title">扫码出库</h2>
|
<p class="m-muted">扫描袋上追溯二维码,按物料与批次匹配库存后新增一行出库明细。</p>
|
<Scanner v-if="!trace" :disabled="busy" @code="identify" />
|
<section v-else class="m-panel">
|
<h3 class="m-title">核对出库对象</h3>
|
<dl>
|
<div class="m-row"><dt>物料</dt><dd>{{ trace.data.itemName || '未录入' }}</dd></div>
|
<div class="m-row"><dt>批次</dt><dd>{{ stock?.batchCode || trace.data.batchCode || '未绑定' }}</dd></div>
|
<div class="m-row"><dt>库存数量</dt><dd>{{ stock?.quantity ?? '—' }}</dd></div>
|
<div class="m-row"><dt>码值</dt><dd style="overflow-wrap: anywhere">{{ trace.code }}</dd></div>
|
</dl>
|
<label class="m-field">
|
<span>出库数量</span>
|
<input v-model.number="quantity" type="number" inputmode="decimal" min="0.01" step="any" :disabled="busy" />
|
</label>
|
<div class="m-action-bar">
|
<button class="m-primary" :disabled="busy" @click="confirm">确认添加明细</button>
|
<button class="m-secondary" :disabled="busy" @click="trace = undefined; stock = undefined">重新扫描</button>
|
</div>
|
</section>
|
</template>
|
<template v-else>
|
<form class="m-panel" @submit.prevent="load()">
|
<label class="m-field"><span>出库单号</span><input v-model="code" :disabled="busy" placeholder="输入出库单号" /></label>
|
<button class="m-secondary" :disabled="busy">{{ busy ? '正在查询…' : '查询草稿出库单' }}</button>
|
</form>
|
<p class="m-muted" role="status">共 {{ total }} 张草稿出库单</p>
|
<p v-if="!rows.length && !busy && !error" class="m-empty">暂无草稿出库单</p>
|
<button v-for="row in rows" :key="row.id" class="m-order-button" :disabled="busy" @click="open(row.id)">
|
<strong>{{ row.code }}</strong><span>草稿</span><small>{{ row.name || '未录入名称' }}</small>
|
</button>
|
<button v-if="rows.length < total" class="m-secondary" :disabled="busy" @click="load(false)">
|
{{ busy ? '正在加载…' : '加载更多' }}
|
</button>
|
</template>
|
</template>
|