云
9 天以前 1ed040591afdcbe3c5df42ad5e02e2cfa56edd87
src/views/mes/qc/seedQuality/index.vue
@@ -11,6 +11,7 @@
import {
  resolveSeedQuality,
  submitSeedQualityGroup,
  getSeedIndicatorRanges,
} from '#/api/mes/qc/seedQuality';
import { getWarehouseSimpleList } from '#/api/mes/wm/warehouse';
@@ -54,17 +55,31 @@
const qualifiedWarehouseId = ref<number>();
const indicators = ref<IndicatorForm[]>([
  { code: 'purity', label: '纯度', unit: '%', minValue: 0, maxValue: 100 },
  { code: 'cleanliness', label: '净度', unit: '%', minValue: 0, maxValue: 100 },
  { code: 'germination_rate', label: '发芽率', unit: '%', minValue: 0, maxValue: 100 },
  { code: 'moisture', label: '水分', unit: '%', minValue: 0, maxValue: 100 },
  { code: 'purity', label: '纯度', unit: '%' },
  { code: 'cleanliness', label: '净度', unit: '%' },
  { code: 'germination_rate', label: '发芽率', unit: '%' },
  { code: 'moisture', label: '水分', unit: '%' },
]);
const rangesReady = computed(() => indicators.value.every((item) =>
  item.minValue !== undefined && item.maxValue !== undefined));
async function loadRanges() {
  const ranges = await getSeedIndicatorRanges();
  for (const item of indicators.value) {
    const range = ranges.find((row) => row.indicatorCode === item.code);
    item.minValue = range?.minValue === undefined ? undefined : Number(range.minValue);
    item.maxValue = range?.maxValue === undefined ? undefined : Number(range.maxValue);
  }
}
const pendingCount = computed(() => target.value?.pendingBagIds?.length ?? 0);
const completedCount = computed(() => target.value?.completedBagIds?.length ?? 0);
const enteredCount = computed(() => indicators.value.filter((item) => item.value !== undefined && item.value !== null).length);
const passedCount = computed(() => indicators.value.filter(isPassed).length);
const allEntered = computed(() => enteredCount.value === indicators.value.length);
const allEntered = computed(() => indicators.value.every((item) =>
  item.value !== undefined && item.value !== null
  && item.minValue !== undefined && item.minValue !== null
  && item.maxValue !== undefined && item.maxValue !== null));
const overallPassed = computed(() => allEntered.value && passedCount.value === indicators.value.length);
const isGroupScan = computed(() => scanType.value !== 'BAG');
const qualifiedWarehouseOptions = computed(() =>
@@ -99,8 +114,6 @@
  reason.value = '';
  indicators.value.forEach((item) => {
    item.value = undefined;
    item.minValue = 0;
    item.maxValue = 100;
  });
}
@@ -129,6 +142,7 @@
  resolving.value = true;
  submittedCount.value = undefined;
  try {
    await loadRanges().catch(() => undefined);
    const data = await resolveSeedQuality(value, scanType.value);
    if (data.canceled) {
      target.value = undefined;
@@ -163,7 +177,7 @@
    return;
  }
  if (!allEntered.value) {
    message.warning('请先填写四项指标的实测值');
    message.warning('请填写四项指标的实测值和本次合格上下限');
    return;
  }
  if (indicators.value.some((item) => item.minValue === undefined || item.maxValue === undefined || item.minValue > item.maxValue)) {
@@ -187,8 +201,8 @@
    Modal.confirm({
      title: overallPassed.value ? '确认提交合格结果?' : '确认提交不合格结果?',
      content: overallPassed.value
        ? `本次 ${count} 袋将统一判定合格,并从暂存库存转入所选合格库。${scopeNote}`
        : `本次 ${count} 袋将从暂存库存中清除并标记不合格,同时保留不合格原因用于质量分析。${scopeNote}`,
        ? `本次 ${count} 袋将统一记录为初检合格,待初检审核通过后复检;复检审核通过后才转入合格库。${scopeNote}`
        : `本次 ${count} 袋将记录为初检不合格,保留暂存库存;初检审核后决定是否进入复检。${scopeNote}`,
      okText: '确认提交',
      cancelText: '返回修改',
      okButtonProps: { danger: !overallPassed.value },
@@ -216,7 +230,7 @@
      })),
    });
    submittedCount.value = result.processedCount;
    message.success(`本次 ${result.processedCount} 袋已提交:${passed ? '质检合格,已转入所选合格库' : '质检不合格,已记录原因并移除暂存库存'}`);
    message.success(`本次 ${result.processedCount} 袋已提交初检,待初检审核`);
    if (pageRoot.value && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
      gsap.fromTo('.result-card', { scale: 0.97, autoAlpha: 0.5 }, { scale: 1, autoAlpha: 1, duration: 0.35, ease: 'power2.out' });
    }
@@ -232,6 +246,11 @@
}
onMounted(async () => {
  try {
    await loadRanges();
  } catch (error) {
    message.error(errorText(error));
  }
  try {
    const warehouses = await getWarehouseSimpleList();
    qualifiedWarehouses.value = (warehouses ?? []).filter(
@@ -263,7 +282,7 @@
      <div>
        <div class="eyebrow">MES / 质量管理</div>
        <h1>种子质检</h1>
        <p>选择袋码、托盘码或批次码识别待检范围,录入纯度、净度、发芽率和水分,系统自动判定检验结论</p>
        <p>识别待检范围并录入四项指标;初检审核后复检,复检审核后入库或处置</p>
      </div>
      <div class="header-badge"><IconifyIcon icon="ep:checked" /> 袋码 / 托盘码 / 批次码</div>
    </header>
@@ -315,17 +334,18 @@
            </a-form-item>
          </div>
          <p v-if="!qualifiedWarehouses.length" class="target-warning">暂无可用合格库,请联系管理员配置后再提交质检。</p>
          <a-alert v-if="!rangesReady" class="target-empty" type="info" show-icon message="本次请手动填写合格范围" description="管理员尚未配置完整默认范围;仍可手动填写四项指标的合格上下限。" />
          <a-divider orientation="left">指标结果</a-divider>
          <div class="indicator-list">
            <div v-for="item in indicators" :key="item.code" class="indicator-row" :class="{ failed: !isPassed(item) }">
            <div v-for="item in indicators" :key="item.code" class="indicator-row" :class="{ failed: allEntered && !isPassed(item) }">
              <div class="indicator-name"><span class="indicator-dot" :class="isPassed(item) ? 'pass' : 'fail'" /><strong>{{ item.label }}</strong><small>{{ item.unit }}</small></div>
              <a-input-number v-model:value="item.value" class="indicator-input" :disabled="loading" :min="0" :precision="3" placeholder="实测值" />
              <span class="range-separator">范围</span>
              <a-input-number v-model:value="item.minValue" class="range-input" :disabled="loading" :min="0" :precision="3" placeholder="下限" />
              <a-input-number v-model:value="item.minValue" class="range-input" :disabled="loading" :min="0" :max="100" :precision="3" placeholder="下限" />
              <span>—</span>
              <a-input-number v-model:value="item.maxValue" class="range-input" :disabled="loading" :min="0" :precision="3" placeholder="上限" />
              <a-tag :color="isPassed(item) ? 'success' : 'error'">{{ isPassed(item) ? '合格' : '超出范围' }}</a-tag>
              <a-input-number v-model:value="item.maxValue" class="range-input" :disabled="loading" :min="0" :max="100" :precision="3" placeholder="上限" />
              <a-tag :color="!allEntered ? 'default' : isPassed(item) ? 'success' : 'error'">{{ !allEntered ? '待录入' : isPassed(item) ? '合格' : '超出范围' }}</a-tag>
            </div>
          </div>
@@ -338,7 +358,7 @@
        <template #title><div class="card-title"><IconifyIcon icon="ep:data-analysis" />判定说明</div></template>
        <div class="result-hero" :class="overallPassed ? 'passed' : allEntered ? 'failed-result' : 'pending-result'"><IconifyIcon :icon="overallPassed ? 'ep:circle-check-filled' : allEntered ? 'ep:warning-filled' : 'ep:edit-pen'" /><strong>{{ overallPassed ? '当前结果合格' : allEntered ? '当前存在不合格项' : '等待录入指标' }}</strong><span>{{ allEntered ? '实测值需处于最小值与最大值之间' : `已录入 ${enteredCount} / 4 项实测值` }}</span></div>
        <div class="rule-list"><div><IconifyIcon icon="ep:check" /><span>先识别码值再录入指标,四项指标必须全部提交</span></div><div><IconifyIcon icon="ep:check" /><span>边界值按合格处理</span></div><div><IconifyIcon icon="ep:check" /><span>托盘码、批次码的结论应用于本次全部待检袋</span></div><div><IconifyIcon icon="ep:check" /><span>不合格结果需保留原因</span></div><div><IconifyIcon icon="ep:check" /><span>质检员与检验时间由服务端记录</span></div></div>
        <a-alert message="库存流转由后端处理" description="合格袋将从暂存库存转入所选合格库;不合格袋将直接清除暂存库存并保留不合格原因。提交时服务端会重新核对本次待检袋清单,清单变化需重新识别。" type="info" show-icon />
        <a-alert message="复检审核完成后流转库存" description="初检及复检期间袋码留在暂存库。初检审核通过后进入复检;复检合格并审核通过才转入合格库,复检不合格经确认后才报废。" type="info" show-icon />
        <div v-if="submittedCount" class="success-note"><IconifyIcon icon="ep:success-filled" /><span>最近提交质检:<strong>{{ submittedCount }} 袋</strong></span></div>
      </a-card>
    </section>