云
5 小时以前 fb4db4ae9b6a7f113ba049f6789e6916c0dcc276
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { onBeforeRouteLeave } from 'vue-router';
import { useUserStore } from '@vben/stores';
import { resolveSeedQuality, submitSeedQualityGroup, type SeedQualityScan } from '#/api/mes/qc/seedQuality';
import { getWarehouseSimpleList, type MesWmWarehouseApi } from '#/api/mes/wm/warehouse';
import { allowedMobilePath, extractCode } from '#/mobile/access';
import { errorText } from '#/mobile/api';
import Scanner from './scanner.vue';
import Picker from './picker.vue';
 
const busy = ref(false), error = ref(''), notice = ref(''), bag = ref<SeedQualityScan>();
const warehouses = ref<MesWmWarehouseApi.Warehouse[]>([]), warehouseId = ref<number>();
const reason = ref(''), pendingCode = ref('');
type ScanType = 'BAG' | 'PALLET' | 'BATCH';
const scanType = ref<ScanType>('BAG');
const typeNames = { BAG: '袋码', PALLET: '托盘码', BATCH: '生产批次码' };
const pendingType = ref<ScanType>('BAG'), pendingIds = ref<number[]>([]);
const storageKey = 'mom-mobile-quality-' + useUserStore().userInfo?.id;
const indicators = ref([
  { code: 'purity', name: '纯度', value: '', minValue: '0', maxValue: '100' },
  { code: 'cleanliness', name: '净度', value: '', minValue: '0', maxValue: '100' },
  { code: 'germination_rate', name: '发芽率', value: '', minValue: '0', maxValue: '100' },
  { code: 'moisture', name: '水分', value: '', minValue: '0', maxValue: '100' },
]);
const complete = computed(() => indicators.value.every(i => [i.value, i.minValue, i.maxValue].every(v => String(v).trim() !== '' && Number.isFinite(Number(v))) && Number(i.minValue) >= 0 && Number(i.maxValue) <= 100 && Number(i.value) >= 0 && Number(i.value) <= 100 && Number(i.minValue) <= Number(i.maxValue)));
const passed = computed(() => complete.value && indicators.value.every(i => Number(i.value) >= Number(i.minValue) && Number(i.value) <= Number(i.maxValue)));
function clearPending() { pendingCode.value = ''; pendingIds.value = []; sessionStorage.removeItem(storageKey); }
async function identify(raw: string) {
  if (busy.value || pendingCode.value) return;
  busy.value = true; error.value = ''; notice.value = ''; bag.value = undefined;
  try {
    const data = await resolveSeedQuality(extractCode(raw), scanType.value);
    if (data.canceled || !data.inbound) throw new Error('该对象已作废或有效袋码尚未全部入库,不能质检');
    if (!data.pendingBagIds?.length) throw new Error('该对象没有待质检袋码,请在质检记录中查看已有结果');
    bag.value = data; indicators.value.forEach(i => i.value = ''); reason.value = '';
  } catch (e) { error.value = errorText(e); } finally { busy.value = false; }
}
async function submit() {
  if (busy.value || pendingCode.value || !bag.value || !complete.value || !warehouseId.value) return;
  if (!passed.value && !reason.value.trim()) { error.value = '请填写不合格原因'; return; }
  const count = bag.value.pendingBagIds!.length;
  if (!window.confirm(passed.value ? `确认对本次 ${count} 袋统一判定合格?全部将转入所选合格库。` : `确认对本次 ${count} 袋统一判定不合格?全部将从暂存库存移除并标记报废。`)) return;
  error.value = ''; notice.value = '';
  pendingCode.value = bag.value.code; pendingType.value = scanType.value; pendingIds.value = [...bag.value.pendingBagIds!];
  sessionStorage.setItem(storageKey, JSON.stringify({ code: pendingCode.value, scanType: pendingType.value, bagIds: pendingIds.value })); busy.value = true;
  try {
    const result = await submitSeedQualityGroup({ code: bag.value.code, scanType: scanType.value, bagIds: pendingIds.value, qualifiedWarehouseId: warehouseId.value, reason: reason.value.trim() || undefined, indicators: indicators.value.map(i => ({ code: i.code, value: Number(i.value), minValue: Number(i.minValue), maxValue: Number(i.maxValue) })) });
    clearPending(); notice.value = `本次 ${result.processedCount} 袋` + (passed.value ? '质检合格,已转入合格库。' : '质检不合格,已记录原因并移除暂存库存。');
    bag.value = undefined; reason.value = ''; indicators.value.forEach(i => i.value = '');
  } catch (e) { error.value = '提交结果待确认:' + errorText(e) + '。请核实后再操作下一袋。'; } finally { busy.value = false; }
}
async function verify() {
  if (busy.value || !pendingCode.value) return;
  busy.value = true; error.value = '';
  try {
    const data = await resolveSeedQuality(pendingCode.value, pendingType.value);
    const completed = pendingIds.value.filter(id => data.completedBagIds?.includes(id));
    if (pendingIds.value.length ? completed.length === pendingIds.value.length : !!data.qualityId) { notice.value = '已核实:本次袋码均已有质检记录,请在质检记录中核对结果。'; bag.value = undefined; clearPending(); }
    else if (completed.length) error.value = '本次清单仅部分袋码已有质检记录,请核对质检记录后联系负责人处理,当前保留待确认操作。';
    else if (window.confirm('当前未查到本次袋码质检记录。确认原请求已结束后,可重新扫描并核对指标。继续?')) { clearPending(); bag.value = undefined; notice.value = '请重新扫描并录入指标;后端会拦截重复质检。'; }
  } catch (e) { error.value = errorText(e); } finally { busy.value = false; }
}
onMounted(async () => {
  const saved = sessionStorage.getItem(storageKey);
  if (saved) {
    try { const pending = JSON.parse(saved); if (pending && typeof pending === 'object' && pending.code) { pendingCode.value = pending.code; pendingType.value = pending.scanType; pendingIds.value = pending.bagIds || []; } else pendingCode.value = saved; }
    catch { pendingCode.value = saved; } // 兼容升级前保存的单袋待确认码值。
  }
  busy.value = true;
  try { warehouses.value = (await getWarehouseSimpleList()).filter(w => w.type === 20 && !w.frozen); }
  catch (e) { error.value = errorText(e); } finally { busy.value = false; }
});
onBeforeRouteLeave(() => !busy.value && (!bag.value || !!pendingCode.value || window.confirm('离开将放弃未提交的质检指标,是否继续?')));
</script>
<template>
  <p v-if="notice" class="m-success" role="status">{{notice}}</p>
  <p v-if="error" class="m-error" role="alert">{{error}}</p>
  <div v-if="pendingCode" class="m-panel"><h2 class="m-title">质检结果待确认</h2><p>{{pendingCode}}</p><button class="m-primary" :disabled="busy" @click="verify">核实质检结果</button></div>
  <template v-else>
    <label class="m-field"><span>质检扫码类型</span><select v-model="scanType" :disabled="busy" @change="bag = undefined; notice = ''; error = ''"><option value="BAG">袋码</option><option value="PALLET">托盘码</option><option value="BATCH">生产批次码</option></select></label>
    <p class="m-muted">扫描{{typeNames[scanType]}},核对待检袋数,统一录入四项指标。已检、作废袋码不会重复处理。</p>
    <Scanner :disabled="busy" @code="identify" />
    <form v-if="bag" @submit.prevent="submit">
      <div class="m-panel"><h2 class="m-title">本次检验对象</h2><dl><div class="m-row"><dt>对象类型</dt><dd>{{typeNames[scanType]}}</dd></div><div class="m-row"><dt>对象码值</dt><dd>{{bag.code}}</dd></div><div class="m-row"><dt>业务码</dt><dd>{{bag.businessCode || '—'}}</dd></div><div class="m-row"><dt>生产批次</dt><dd>{{bag.batchCode || '—'}}</dd></div><div class="m-row"><dt>有效袋数</dt><dd>{{bag.totalCount}}</dd></div><div class="m-row"><dt>已检袋数</dt><dd>{{bag.completedBagIds?.length || 0}}</dd></div><div class="m-row"><dt>本次待检</dt><dd>{{bag.pendingBagIds?.length || 0}} 袋</dd></div></dl><p v-if="scanType !== 'BAG'" class="m-warning">以下指标和检验结论将应用于本次全部待检袋码,请确认抽样结果适用于这{{bag.pendingBagIds?.length}}袋。</p></div>
      <fieldset v-for="i in indicators" :key="i.code" class="m-panel m-quality-indicator" :disabled="busy"><legend>{{i.name}}(%)</legend>
        <label class="m-field"><span>实测值</span><input v-model="i.value" type="number" inputmode="decimal" min="0" max="100" step="any" required placeholder="输入实测百分比" /></label>
        <div class="m-grid"><label class="m-field"><span>合格下限</span><input v-model="i.minValue" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label><label class="m-field"><span>合格上限</span><input v-model="i.maxValue" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label></div>
      </fieldset>
      <div class="m-panel"><p class="m-muted">上下限沿用现有质检规则录入方式,请按该品种检验标准核对。</p><Picker v-model="warehouseId" label="目标合格库" :options="warehouses" :disabled="busy" /><p v-if="!warehouses.length && !busy" class="m-warning">暂无可用合格库,请联系管理员配置。</p><label class="m-field"><span>{{complete && !passed ? '不合格原因(必填)' : '检验备注'}}</span><textarea v-model="reason" :disabled="busy" :required="complete && !passed" rows="3" maxlength="255" /></label></div>
      <div class="m-action-bar"><p v-if="complete" :class="passed ? 'm-success' : 'm-warning'">预判:{{passed ? '合格,提交后转入合格库' : '不合格,提交后移除暂存库存'}}。最终以服务端结果为准。</p><button class="m-primary" :disabled="busy || !complete || !warehouseId">{{busy ? '正在提交…' : '确认提交质检'}}</button></div>
    </form>
  </template>
  <RouterLink v-if="allowedMobilePath('/mobile/quality-records')" to="/mobile/quality-records" class="m-link">查看质检记录</RouterLink>
</template>