From 27f904344349c51e720be96e4afd9ca033a33676 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期五, 16 十月 2026 20:15:51 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/dev_新疆_塔河种业' into dev_新疆_塔河种业

---
 src/api/mes/pro/pallet/index.ts                   |    8 
 vite.config.ts                                    |    2 
 src/views/mes/pro/bagCode/modules/bind-pallet.vue |   35 +
 src/views/mes/pro/qrexport/qr-export-progress.vue |  189 ++++++++++++
 src/views/mes/qc/seedQuality/index.vue            |  230 ++++++++++++---
 src/views/mes/pro/qrexport/use-qr-export.ts       |  387 +++++++++++++++++++++++++
 6 files changed, 791 insertions(+), 60 deletions(-)

diff --git a/src/api/mes/pro/pallet/index.ts b/src/api/mes/pro/pallet/index.ts
index d02811d..f964b2f 100644
--- a/src/api/mes/pro/pallet/index.ts
+++ b/src/api/mes/pro/pallet/index.ts
@@ -129,6 +129,14 @@
   );
 }
 
+/** 鏌ヨ琚嬬爜缁戝畾鎵樼洏鏃剁殑鍙�夌洰鏍囨墭鐩樺垪琛紙鏈粦鎵规鐨勪复鏃舵墭鐩� + 姝e紡鎵樼洏锛屾帓闄ゅ凡浣滃簾锛� */
+export function getPalletBindableList(itemId?: number) {
+  return requestClient.get<MesProPalletApi.Pallet[]>(
+    '/mes/pro/pallet/bindable-list',
+    { params: { itemId } },
+  );
+}
+
 /** 鏌ヨ鏈粦瀹氭壒娆$殑涓存椂鎵樼洏鍒楄〃锛堜緵鎵规鏂板/琛ュ綍缁戝畾鎵樼洏涓嬫媺浣跨敤锛� */
 export function getUnboundPalletList() {
   return requestClient.get<MesProPalletApi.Pallet[]>(
diff --git a/src/views/mes/pro/bagCode/modules/bind-pallet.vue b/src/views/mes/pro/bagCode/modules/bind-pallet.vue
index fac78f0..bc1811f 100644
--- a/src/views/mes/pro/bagCode/modules/bind-pallet.vue
+++ b/src/views/mes/pro/bagCode/modules/bind-pallet.vue
@@ -1,5 +1,6 @@
 <script lang="ts" setup>
   import type { VbenFormSchema } from "#/adapter/form";
+  import type { MesProPalletApi } from "#/api/mes/pro/pallet";
 
   import { useVbenModal } from "@vben/common-ui";
   import { MesProPalletStatusEnum } from "@vben/constants";
@@ -8,19 +9,30 @@
 
   import { useVbenForm } from "#/adapter/form";
   import { bindBagCodePallet } from "#/api/mes/pro/bagCode";
-  import { getPalletSimpleList } from "#/api/mes/pro/pallet";
+  import {
+    getPalletBindableList,
+    getPalletSimpleList,
+  } from "#/api/mes/pro/pallet";
   import { $t } from "#/locales";
 
   defineOptions({ name: "MesProBagCodeBindPallet" });
 
-  /** 寮圭獥浼犲叆鏁版嵁 */
+  /** 寮圭獥浼犲叆鏁版嵁锛坆atchId 涓虹┖琛ㄧず鎵�閫夎鐮佸皻鏈綍鍏ョ敓浜ф壒娆★級 */
   interface ModalData {
-    batchId: number;
+    batchId?: number;
     batchCode?: string;
+    itemId?: number;
     ids: number[];
   }
 
   const emit = defineEmits(["success"]);
+
+  /** 鏈綍鍏ユ壒娆℃椂鐨勬墭鐩樺睍绀哄悕锛氭寮忔墭鐩樺甫鎵规鍙枫�佷复鏃舵墭鐩樺洖閫�鍒颁复鏃剁爜锛屼究浜庡湪娣峰悎鍒楄〃涓垎杈� */
+  function palletLabel(pallet: MesProPalletApi.Pallet) {
+    const code = pallet.code || pallet.tempCode || "-";
+    const batchPart = pallet.batchCode ? `鎵规 ${pallet.batchCode}` : "涓存椂鎵樼洏";
+    return `${code}锛�${batchPart} / 宸茶 ${pallet.bagCount ?? 0} 琚嬶級`;
+  }
 
   /** 鏋勫缓琛ㄥ崟锛堟墭鐩樹笅鎷夐�夐」鐢卞閮ㄥ姞杞芥敞鍏ワ級 */
   function buildSchema(palletOptions: { label: string; value: number }[]): VbenFormSchema[] {
@@ -90,20 +102,23 @@
       const data = modalApi.getData<ModalData>();
       modalApi.lock();
       try {
-        // 鍔犺浇璇ユ壒娆′笅鐨勬墭鐩橈紙鎺掗櫎宸蹭綔搴燂級
-        const pallets = await getPalletSimpleList(data.batchId);
+        // 宸插綍鍏ユ壒娆★細鍙垪鏈壒娆$殑鎵樼洏锛涙湭褰曞叆鎵规锛氬垪璇ュ搧绉嶅彲鐢ㄧ殑涓存椂鎵樼洏涓庢寮忔墭鐩橈紝
+        // 閫夊埌姝e紡鎵樼洏鏃剁敱鍚庣鍏堟妸杩欐壒琚嬬爜琛ュ綍杩涜鎵规锛屽啀瀹屾垚缁戝畾
+        const pallets = data.batchId
+          ? await getPalletSimpleList(data.batchId)
+          : await getPalletBindableList(data.itemId);
         const options = pallets
-          .filter(
-            (pallet) => pallet.status !== MesProPalletStatusEnum.CANCELED
-          )
+          .filter((pallet) => pallet.status !== MesProPalletStatusEnum.CANCELED)
           .map((pallet) => ({
-            label: `${pallet.code}锛堝凡瑁� ${pallet.bagCount ?? 0} 琚嬶級`,
+            label: data.batchId
+              ? `${pallet.code}锛堝凡瑁� ${pallet.bagCount ?? 0} 琚嬶級`
+              : palletLabel(pallet),
             value: pallet.id!,
           }));
         await formApi.setState({ schema: buildSchema(options) });
         await formApi.setValues({
           bagCount: data.ids.length,
-          batchCode: data.batchCode,
+          batchCode: data.batchCode || "鏈綍鍏ユ壒娆�",
         });
       } finally {
         modalApi.unlock();
diff --git a/src/views/mes/pro/qrexport/qr-export-progress.vue b/src/views/mes/pro/qrexport/qr-export-progress.vue
new file mode 100644
index 0000000..3d9281d
--- /dev/null
+++ b/src/views/mes/pro/qrexport/qr-export-progress.vue
@@ -0,0 +1,189 @@
+<script lang="ts" setup>
+  import { computed } from 'vue';
+
+  import { Button, Progress } from 'ant-design-vue';
+
+  import { useQrExport } from './use-qr-export';
+
+  defineOptions({ name: 'QrExportProgress' });
+
+  const { state, cancel, minimize, expand, close } = useQrExport();
+
+  /** 闈㈡澘鏄惁灞曠ず锛坕dle 琛ㄧず娌℃湁浠讳綍浠诲姟锛� */
+  const visible = computed(() => state.phase !== 'idle');
+
+  /** 杩涜涓殑闃舵鎵嶅厑璁稿彇娑� */
+  const cancelable = computed(() =>
+    ['counting', 'downloading', 'generating'].includes(state.phase),
+  );
+
+  const percent = computed(() => {
+    switch (state.phase) {
+      case 'counting': {
+        return 0;
+      }
+      case 'done': {
+        return 100;
+      }
+      case 'downloading': {
+        if (state.downloadTotal > 0) {
+          return Math.min(
+            100,
+            Math.round((state.downloadLoaded / state.downloadTotal) * 100),
+          );
+        }
+        return 0;
+      }
+      case 'generating': {
+        return state.progress;
+      }
+      default: {
+        return state.progress;
+      }
+    }
+  });
+
+  const progressStatus = computed(() => {
+    if (state.phase === 'failed') return 'exception';
+    if (state.phase === 'done') return 'success';
+    return 'active';
+  });
+
+  /** 鐢熸垚闃舵涓庝笅杞介樁娈甸兘涓嶅彲棰勭煡鏃堕暱锛岀敤涓嶇‘瀹氭�佽繘搴︽潯 */
+  const indeterminate = computed(
+    () => state.phase === 'counting' || state.phase === 'sync',
+  );
+
+  const title = computed(() => {
+    switch (state.phase) {
+      case 'canceled': {
+        return '瀵煎嚭宸插彇娑�';
+      }
+      case 'counting': {
+        return '姝e湪缁熻鏁伴噺';
+      }
+      case 'done': {
+        return '瀵煎嚭瀹屾垚';
+      }
+      case 'downloading': {
+        return '姝e湪涓嬭浇';
+      }
+      case 'failed': {
+        return '瀵煎嚭澶辫触';
+      }
+      case 'generating': {
+        return '姝e湪鐢熸垚浜岀淮鐮�';
+      }
+      case 'sync': {
+        return '姝e湪瀵煎嚭';
+      }
+      default: {
+        return '浜岀淮鐮佸鍑�';
+      }
+    }
+  });
+
+  const detail = computed(() => {
+    if (state.phase === 'failed') {
+      return state.errorMsg || '瀵煎嚭澶辫触';
+    }
+    if (state.phase === 'downloading') {
+      if (state.downloadTotal > 0) {
+        return `${formatSize(state.downloadLoaded)} / ${formatSize(state.downloadTotal)}`;
+      }
+      return `宸蹭笅杞� ${formatSize(state.downloadLoaded)}`;
+    }
+    return state.message;
+  });
+
+  /** 鏀惰捣鍚庢诞鏍囦笂浠嶈鑳界湅鍒拌繘搴︼紝閬垮厤鐢ㄦ埛浠ヤ负浠诲姟宸茬粡鍋滀簡 */
+  const pillText = computed(() =>
+    state.phase === 'generating' ? `${percent.value}%` : title.value,
+  );
+
+  function formatSize(bytes: number) {
+    if (!bytes) return '0 B';
+    if (bytes < 1024) return `${bytes} B`;
+    if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
+    if (bytes < 1024 * 1024 * 1024) {
+      return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
+    }
+    return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
+  }
+</script>
+
+<template>
+  <!-- 鏀惰捣鎬侊細灏忔诞鏍囷紝鐐瑰嚮鍙噸鏂板睍寮� -->
+  <div
+    v-if="visible && state.minimized"
+    class="fixed right-6 bottom-6 z-[2000] flex cursor-pointer items-center gap-2 rounded-full bg-white px-4 py-2 shadow-lg ring-1 ring-gray-200 hover:shadow-xl dark:bg-gray-800 dark:ring-gray-700"
+    @click="expand"
+  >
+    <span
+      class="inline-block h-2 w-2 rounded-full"
+      :class="{
+        'bg-blue-500': state.phase === 'generating' || state.phase === 'downloading',
+        'bg-green-500': state.phase === 'done',
+        'bg-red-500': state.phase === 'failed',
+        'bg-gray-400': state.phase === 'canceled',
+      }"
+    ></span>
+    <span class="text-sm text-gray-700 dark:text-gray-200">
+      浜岀淮鐮佸鍑� {{ pillText }}
+    </span>
+  </div>
+
+  <!-- 灞曞紑鎬侊細杩涘害闈㈡澘 -->
+  <div
+    v-else-if="visible"
+    class="fixed right-6 bottom-6 z-[2000] w-80 rounded-lg bg-white p-4 shadow-lg ring-1 ring-gray-200 dark:bg-gray-800 dark:ring-gray-700"
+  >
+    <div class="mb-2 flex items-center justify-between">
+      <span class="text-sm font-medium text-gray-800 dark:text-gray-100">
+        {{ title }}
+      </span>
+      <div class="flex items-center gap-1">
+        <!-- 杩涜涓彁渚涖�岄殣钘忋�嶏紝浠诲姟杞叆鍚庡彴缁х画锛屼笉浼氳涓柇 -->
+        <Button
+          v-if="state.phase !== 'done' && state.phase !== 'failed' && state.phase !== 'canceled'"
+          type="link"
+          size="small"
+          @click="minimize"
+        >
+          闅愯棌
+        </Button>
+        <Button
+          v-if="cancelable"
+          type="link"
+          size="small"
+          danger
+          @click="cancel"
+        >
+          鍙栨秷
+        </Button>
+        <Button
+          v-if="!cancelable"
+          type="link"
+          size="small"
+          @click="close"
+        >
+          鍏抽棴
+        </Button>
+      </div>
+    </div>
+
+    <Progress
+      :percent="percent"
+      :status="progressStatus"
+      :show-info="!indeterminate"
+      size="small"
+    />
+
+    <div class="mt-2 truncate text-xs text-gray-500 dark:text-gray-400">
+      {{ state.taskName }}
+    </div>
+    <div class="mt-1 text-xs text-gray-500 dark:text-gray-400">
+      {{ detail }}
+    </div>
+  </div>
+</template>
diff --git a/src/views/mes/pro/qrexport/use-qr-export.ts b/src/views/mes/pro/qrexport/use-qr-export.ts
new file mode 100644
index 0000000..fe78856
--- /dev/null
+++ b/src/views/mes/pro/qrexport/use-qr-export.ts
@@ -0,0 +1,387 @@
+import type { MesProQrExportApi } from '#/api/mes/pro/qrExport';
+
+import { reactive } from 'vue';
+
+import { useAppConfig } from '@vben/hooks';
+import { useAccessStore } from '@vben/stores';
+import { downloadFileFromBlobPart } from '@vben/utils';
+
+import { message } from 'ant-design-vue';
+
+import {
+  cancelQrExportTask,
+  getQrExportCount,
+  getQrExportDownloadPath,
+  getQrExportTask,
+  submitQrExportTask,
+} from '#/api/mes/pro/qrExport';
+
+/**
+ * 瀵煎嚭闃舵
+ * <p>
+ * counting 缁熻寮犳暟 / sync 灏忔壒閲忓悓姝ヤ笅杞戒腑 / generating 鍚庡彴鐢熸垚涓� /
+ * downloading 涓嬭浇浜х墿涓� / done 瀹屾垚 / canceled 宸插彇娑� / failed 澶辫触
+ */
+export type QrExportPhase =
+  | 'canceled'
+  | 'counting'
+  | 'done'
+  | 'downloading'
+  | 'failed'
+  | 'generating'
+  | 'idle'
+  | 'sync';
+
+export interface QrExportState {
+  bizType?: MesProQrExportApi.BizType;
+  /** 宸蹭笅杞藉瓧鑺� */
+  downloadLoaded: number;
+  /** 浜х墿鎬诲瓧鑺傦紙鍙栬嚜 Content-Length锛屽彇涓嶅埌鏃朵负 0 琛ㄧず鏈煡锛� */
+  downloadTotal: number;
+  errorMsg: string;
+  /** 宸茬敓鎴愬紶鏁� */
+  finished: number;
+  message: string;
+  /** 鏀惰捣涓烘诞鏍囷紙浠诲姟缁х画鍦ㄥ悗鍙拌窇锛� */
+  minimized: boolean;
+  phase: QrExportPhase;
+  /** 杩涘害鐧惧垎姣旓紙0~100锛� */
+  progress: number;
+  taskId?: number;
+  taskName: string;
+  /** 鏍囩鎬诲紶鏁� */
+  total: number;
+}
+
+const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
+
+/**
+ * 瀵煎嚭鐘舵�佹彁鍒版ā鍧楃骇鍗曚緥锛氳繘搴﹂潰鏉胯鏀惰捣銆佹垨椤甸潰缁勪欢琚攢姣侊紙鐢ㄦ埛鍒囪蛋鍐嶅垏鍥烇級鏃讹紝
+ * 杞涓庝笅杞介兘蹇呴』缁х画锛屼笉鑳藉洜涓虹粍浠跺嵏杞借�屼腑鏂��
+ */
+const state = reactive<QrExportState>(createInitialState());
+
+let pollTimer: null | ReturnType<typeof setInterval> = null;
+let downloadAbort: null | (() => void) = null;
+
+/**
+ * 鐩爣鏂囦欢鍙ユ焺
+ * <p>
+ * 澶ф壒閲忓鍑虹殑浜х墿鍙揪鏁扮櫨 MB锛岃嫢鏁村寘璇昏繘鍐呭瓨鍐嶈惤鐩橈紝鏍囩椤垫瀬鏄撹鎾戠垎銆傚洜姝や紭鍏堝湪鐐瑰嚮瀵煎嚭
+ * 锛堢敤鎴锋墜鍔夸粛鍦ㄦ湁鏁堟湡鍐咃級鏃跺彇鍒颁繚瀛樺彞鏌勶紝浜х墿杈逛笅杞借竟鍐欑洏锛屽唴瀛樺彧鍗犱竴涓垎鐗囥��
+ * 娴忚鍣ㄤ笉鏀寔鎴栫敤鎴锋斁寮冮�夋嫨鏃堕��鍥炲唴瀛樹笅杞姐��
+ */
+let saveHandle: any = null;
+
+/** 灏濊瘯鍙栧緱淇濆瓨鍙ユ焺锛涗笉鏀寔銆佽鍙栨秷鏃惰繑鍥� null锛堜笉瑙嗕负閿欒锛� */
+async function tryPickSaveFile(suggestedName: string): Promise<any> {
+  const picker = (window as any).showSaveFilePicker;
+  if (typeof picker !== 'function') {
+    return null;
+  }
+  try {
+    return await picker({
+      suggestedName,
+      types: [
+        { description: 'ZIP 鍘嬬缉鍖�', accept: { 'application/zip': ['.zip'] } },
+      ],
+    });
+  } catch {
+    return null;
+  }
+}
+
+function createInitialState(): QrExportState {
+  return {
+    downloadLoaded: 0,
+    downloadTotal: 0,
+    errorMsg: '',
+    finished: 0,
+    message: '',
+    minimized: false,
+    phase: 'idle',
+    progress: 0,
+    taskName: '',
+    total: 0,
+  };
+}
+
+function resetState() {
+  Object.assign(state, createInitialState());
+}
+
+function stopPolling() {
+  if (pollTimer !== null) {
+    clearInterval(pollTimer);
+    pollTimer = null;
+  }
+}
+
+/** 鏄惁澶勪簬杩涜涓紙闈㈡澘闇�瑕佸睍绀猴級 */
+function isActive() {
+  return [
+    'counting',
+    'generating',
+    'downloading',
+    'sync',
+  ].includes(state.phase);
+}
+
+/** 鐢卞閮ㄩ〉闈㈡敞鍏ョ殑銆屽皬鎵归噺鍚屾涓嬭浇銆嶅疄鐜帮紝閬垮厤鏈ā鍧椾緷璧栧叿浣撲笟鍔¢〉 */
+export interface StartOptions {
+  fileName?: string;
+  /** 灏忔壒閲忥紙涓嶈秴杩囧垎鐣岀嚎锛夋椂濡備綍鍚屾涓嬭浇锛岀敱璋冪敤椤垫彁渚� */
+  syncDownload?: () => Promise<void>;
+}
+
+/**
+ * 鍙戣捣瀵煎嚭
+ * <p>
+ * 鍏堥棶鍚庣瑕佸紶鏁帮細涓嶈秴杩囧垎鐣岀嚎鐩存帴鍚屾涓嬭浇锛堢鍑恒�佷笉鎺掗槦锛夛紱瓒呰繃鍒欒浆鍚庡彴浠诲姟骞跺睍绀鸿繘搴︺��
+ */
+async function start(
+  params: MesProQrExportApi.ExportParams,
+  options: StartOptions = {},
+) {
+  if (isActive()) {
+    message.warning('宸叉湁瀵煎嚭浠诲姟姝e湪杩涜锛岃绛夊緟瀹屾垚鎴栧彇娑堝悗鍐嶈瘯');
+    return;
+  }
+  resetState();
+  state.bizType = params.bizType;
+  state.taskName = options.fileName ?? '浜岀淮鐮�';
+  state.phase = 'counting';
+  state.message = '姝e湪缁熻寰呭鍑烘暟閲忊��';
+
+  try {
+    const count = await getQrExportCount(params);
+    state.total = count.totalCount;
+    if (count.totalCount === 0) {
+      state.phase = 'failed';
+      state.errorMsg = '娌℃湁鍙鍑虹殑浜岀淮鐮�';
+      state.message = '娌℃湁鍙鍑虹殑浜岀淮鐮�';
+      return;
+    }
+    if (count.mode === 'SYNC') {
+      await startSync(params, count, options);
+      return;
+    }
+    // 澶ф壒閲忥細瓒佺敤鎴锋墜鍔夸粛鏈夋晥鍏堝彇淇濆瓨鍙ユ焺锛屼骇鐗╁彲鐩存帴娴佸紡钀界洏
+    saveHandle = await tryPickSaveFile(`${state.taskName}.zip`);
+    await startAsync(params, count);
+  } catch (error: any) {
+    failWith(error);
+  }
+}
+
+/** 灏忔壒閲忥細娌跨敤鍘熸湁鍚屾涓嬭浇鎺ュ彛 */
+async function startSync(
+  _params: MesProQrExportApi.ExportParams,
+  count: MesProQrExportApi.ExportCount,
+  options: StartOptions,
+) {
+  state.phase = 'sync';
+  state.message = `鍏� ${count.totalCount} 寮狅紝姝e湪鐢熸垚鈥;
+  if (!options.syncDownload) {
+    state.phase = 'failed';
+    state.errorMsg = '缂哄皯鍚屾涓嬭浇瀹炵幇';
+    return;
+  }
+  await options.syncDownload();
+  state.phase = 'done';
+  state.progress = 100;
+  state.finished = count.totalCount;
+  state.message = `宸插鍑� ${count.totalCount} 寮燻;
+}
+
+/** 澶ф壒閲忥細鎻愪氦鍚庡彴浠诲姟骞惰疆璇㈣繘搴� */
+async function startAsync(
+  params: MesProQrExportApi.ExportParams,
+  count: MesProQrExportApi.ExportCount,
+) {
+  state.phase = 'generating';
+  state.message = `鍏� ${count.totalCount} 寮狅紝姝e湪鍚庡彴鐢熸垚鈥;
+  const task = await submitQrExportTask(params);
+  state.taskId = task.id;
+  state.taskName = task.taskName ?? state.taskName;
+  state.total = task.totalCount ?? count.totalCount;
+  state.finished = task.finishedCount ?? 0;
+  state.progress = task.progress ?? 0;
+  pollTask();
+}
+
+function pollTask() {
+  stopPolling();
+  pollTimer = setInterval(() => {
+    void refreshTask();
+  }, 1000);
+}
+
+async function refreshTask() {
+  if (!state.taskId) return;
+  try {
+    const task = await getQrExportTask(state.taskId);
+    state.total = task.totalCount ?? 0;
+    state.finished = task.finishedCount ?? 0;
+    state.progress = task.progress ?? 0;
+    state.taskName = task.taskName ?? state.taskName;
+    if (task.status === 30) {
+      // 寰呬笅杞�
+      stopPolling();
+      state.progress = 100;
+      await downloadProduct(task);
+      return;
+    }
+    if (task.status === 40) {
+      stopPolling();
+      state.phase = 'canceled';
+      state.message = '瀵煎嚭宸插彇娑�';
+      return;
+    }
+    if (task.status === 50) {
+      stopPolling();
+      state.phase = 'failed';
+      state.errorMsg = task.errorMsg ?? '瀵煎嚭澶辫触';
+      state.message = '瀵煎嚭澶辫触';
+      return;
+    }
+    state.phase = 'generating';
+    state.message = `姝e湪鐢熸垚锛�${state.finished} / ${state.total}锛夆�;
+  } catch (error: any) {
+    stopPolling();
+    failWith(error);
+  }
+}
+
+/** 涓嬭浇浜х墿锛氫紭鍏堟祦寮忓啓鍏ョ鐩橈紝閬垮厤鏁扮櫨 MB 鐨勫唴瀹规暣鍧楅┗鐣欏唴瀛� */
+async function downloadProduct(task: MesProQrExportApi.ExportTask) {
+  state.phase = 'downloading';
+  state.message = '姝e湪涓嬭浇鈥�';
+  state.downloadLoaded = 0;
+  state.downloadTotal = task.fileSize ?? 0;
+  const fileName = `${task.taskName ?? '浜岀淮鐮佸鍑�'}.zip`;
+  const url = `${apiURL}${getQrExportDownloadPath(task.id)}`;
+  const token = useAccessStore().accessToken;
+
+  const controller = new AbortController();
+  let canceled = false;
+  let writable: any = null;
+  downloadAbort = () => {
+    canceled = true;
+    controller.abort();
+  };
+
+  try {
+    const response = await fetch(url, {
+      headers: token ? { Authorization: `Bearer ${token}` } : {},
+      signal: controller.signal,
+    });
+    if (!response.ok || !response.body) {
+      throw new Error(`涓嬭浇澶辫触锛圚TTP ${response.status}锛塦);
+    }
+    const contentLength = Number(response.headers.get('content-length') ?? 0);
+    if (contentLength > 0) {
+      state.downloadTotal = contentLength;
+    }
+    const reader = response.body.getReader();
+    downloadAbort = () => {
+      canceled = true;
+      void reader.cancel();
+    };
+    const useDisk = Boolean(saveHandle);
+    if (useDisk) {
+      writable = await saveHandle.createWritable();
+    }
+    // 杈规敹杈瑰啓锛氳繘搴︽潵鑷湡瀹炲瓧鑺傛暟锛涜兘鍐欑洏灏卞彧鍗犱竴涓垎鐗囩殑鍐呭瓨
+    const chunks: Uint8Array[] = [];
+    let loaded = 0;
+    for (;;) {
+      const { done, value } = await reader.read();
+      if (done) break;
+      if (value) {
+        if (useDisk) {
+          await writable.write(value);
+        } else {
+          chunks.push(value);
+        }
+        loaded += value.length;
+        state.downloadLoaded = loaded;
+      }
+    }
+    if (canceled) {
+      if (useDisk) await writable.abort();
+      return;
+    }
+    if (useDisk) {
+      await writable.close();
+    } else {
+      const blob = new Blob(chunks as BlobPart[], { type: 'application/zip' });
+      downloadFileFromBlobPart({ fileName, source: blob });
+    }
+    state.phase = 'done';
+    state.message = `宸插鍑� ${state.total} 寮燻;
+  } catch (error: any) {
+    if (canceled) return;
+    failWith(error);
+  } finally {
+    downloadAbort = null;
+    saveHandle = null;
+  }
+}
+
+/** 鍙栨秷锛氱敓鎴愪腑鍙栨秷浠诲姟锛屼笅杞戒腑涓柇涓嬭浇 */
+async function cancel() {
+  if (state.phase === 'downloading') {
+    downloadAbort?.();
+    state.phase = 'canceled';
+    state.message = '宸插彇娑堜笅杞斤紙鏈嶅姟绔枃浠朵細琚嚜鍔ㄦ竻鐞嗭級';
+    return;
+  }
+  if (state.taskId && ['counting', 'generating'].includes(state.phase)) {
+    try {
+      await cancelQrExportTask(state.taskId);
+    } catch {
+      // 鍙栨秷澶辫触涓嶉樆鏂晫闈㈡敹灏撅細鏈嶅姟绔换鍔$殑瓒呮椂鍥炴敹鏈哄埗浼氬厹搴�
+    }
+    stopPolling();
+    state.phase = 'canceled';
+    state.message = '宸插彇娑堝鍑�';
+  }
+}
+
+/** 鏀惰捣涓烘诞鏍囷紝浠诲姟缁х画鍦ㄥ悗鍙拌窇 */
+function minimize() {
+  state.minimized = true;
+}
+
+/** 浠庢诞鏍囧睍寮�闈㈡澘 */
+function expand() {
+  state.minimized = false;
+}
+
+/** 鍏抽棴闈㈡澘骞舵竻绌虹姸鎬侊紙杩涜涓渶鍏堝彇娑堬級 */
+function close() {
+  stopPolling();
+  downloadAbort?.();
+  downloadAbort = null;
+  saveHandle = null;
+  resetState();
+}
+
+function failWith(error: any) {
+  state.phase = 'failed';
+  state.errorMsg = error?.message ?? error?.msg ?? '瀵煎嚭澶辫触';
+  state.message = '瀵煎嚭澶辫触';
+}
+
+export function useQrExport() {
+  return {
+    state,
+    isActive,
+    start,
+    cancel,
+    minimize,
+    expand,
+    close,
+  };
+}
diff --git a/src/views/mes/qc/seedQuality/index.vue b/src/views/mes/qc/seedQuality/index.vue
index ca55a98..6e2bc93 100644
--- a/src/views/mes/qc/seedQuality/index.vue
+++ b/src/views/mes/qc/seedQuality/index.vue
@@ -6,9 +6,27 @@
 import { MesWmWarehouseTypeEnum } from '@vben/constants';
 import { gsap } from 'gsap';
 
+import type { SeedQualityScan } from '#/api/mes/qc/seedQuality';
 import type { MesWmWarehouseApi } from '#/api/mes/wm/warehouse';
-import { submitSeedQuality } from '#/api/mes/qc/seedQuality';
+import {
+  resolveSeedQuality,
+  submitSeedQualityGroup,
+} from '#/api/mes/qc/seedQuality';
 import { getWarehouseSimpleList } from '#/api/mes/wm/warehouse';
+
+/** 鎵爜绫诲瀷锛氳鐮� / 鎵樼洏鐮� / 鎵规鐮侊紙涓庣敓浜х嚎鎵爜鍏ュ簱淇濇寔涓�鑷达級 */
+type ScanType = 'BAG' | 'PALLET' | 'BATCH';
+
+const SCAN_TYPE_LABEL: Record<ScanType, string> = {
+  BAG: '琚嬬爜',
+  PALLET: '鎵樼洏鐮�',
+  BATCH: '鎵规鐮�',
+};
+const SCAN_TYPE_OPTIONS = [
+  { label: '琚嬬爜', value: 'BAG' },
+  { label: '鎵樼洏鐮�', value: 'PALLET' },
+  { label: '鎵规鐮�', value: 'BATCH' },
+] as const;
 
 interface IndicatorForm {
   code: string;
@@ -20,11 +38,15 @@
 }
 
 const pageRoot = ref<HTMLElement>();
+const scanType = ref<ScanType>('BAG');
 const code = ref('');
-const batchCode = ref('');
+/** 宸茶瘑鍒殑璐ㄦ瀵硅薄锛氬甫鍑烘湁鏁堣鏁般�佸凡妫�琚嬫暟涓庢湰娆″緟妫�琚嬫竻鍗� */
+const target = ref<SeedQualityScan>();
 const reason = ref('');
+const resolving = ref(false);
 const loading = ref(false);
-const submittedId = ref<number>();
+/** 鏈�杩戜竴娆℃垚鍔熸彁浜ょ殑琚嬫暟锛堢敤浜庣粨鏋滃弽棣堬級 */
+const submittedCount = ref<number>();
 let animationContext: gsap.Context | undefined;
 
 /** 鍙�夊悎鏍间粨搴擄紙浠撳簱绫诲瀷=20 鍚堟牸搴擄級 */
@@ -38,10 +60,19 @@
   { code: 'moisture', label: '姘村垎', unit: '%', minValue: 0, maxValue: 100 },
 ]);
 
+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 overallPassed = computed(() => allEntered.value && passedCount.value === indicators.value.length);
+const isGroupScan = computed(() => scanType.value !== 'BAG');
+const qualifiedWarehouseOptions = computed(() =>
+  qualifiedWarehouses.value.map((item) => ({
+    label: item.code ? `${item.name}锛�${item.code}锛塦 : (item.name ?? ''),
+    value: item.id ?? 0,
+  })),
+);
 
 function isPassed(item: IndicatorForm) {
   return item.value !== undefined && item.value !== null
@@ -49,11 +80,23 @@
     && item.value >= item.minValue && item.value <= item.maxValue;
 }
 
-function resetForm(clearResult = true) {
-  code.value = '';
-  batchCode.value = '';
+/** 缁熶竴鎻愬彇鍚庣涓氬姟閿欒淇℃伅 */
+function errorText(error: unknown) {
+  const response = error as {
+    message?: string;
+    msg?: string;
+    response?: { data?: { message?: string; msg?: string } };
+  };
+  return response.response?.data?.message
+    ?? response.response?.data?.msg
+    ?? response.message
+    ?? response.msg
+    ?? '鎿嶄綔澶辫触锛岃绋嶅悗閲嶈瘯';
+}
+
+/** 娓呯┖鎸囨爣涓庝笉鍚堟牸鍘熷洜 */
+function resetIndicators() {
   reason.value = '';
-  if (clearResult) submittedId.value = undefined;
   indicators.value.forEach((item) => {
     item.value = undefined;
     item.minValue = 0;
@@ -61,21 +104,74 @@
   });
 }
 
+/** 鍒囨崲鎵爜绫诲瀷锛氱爜鍊间笌宸茶瘑鍒璞″悓鏃跺け鏁堬紝閬垮厤鐢ㄩ敊绫诲瀷鐨勭爜鍊兼彁浜� */
+function onScanTypeChange() {
+  code.value = '';
+  target.value = undefined;
+  submittedCount.value = undefined;
+  resetIndicators();
+}
+
+function resetForm() {
+  code.value = '';
+  target.value = undefined;
+  submittedCount.value = undefined;
+  resetIndicators();
+}
+
+/** 璇嗗埆鐮佸�硷細纭畾鏈璐ㄦ鑼冨洿锛堣鐮� 1 琚� / 鎵樼洏宸茶琚� / 鏁存壒琚嬫暟锛� */
+async function identify() {
+  const value = code.value.trim();
+  if (!value) {
+    message.warning(`璇疯緭鍏ユ垨鎵弿${SCAN_TYPE_LABEL[scanType.value]}`);
+    return;
+  }
+  resolving.value = true;
+  submittedCount.value = undefined;
+  try {
+    const data = await resolveSeedQuality(value, scanType.value);
+    if (data.canceled) {
+      target.value = undefined;
+      message.error('璇ヨ川妫�瀵硅薄宸蹭綔搴燂紝涓嶈兘璐ㄦ');
+      return;
+    }
+    if (!data.inbound) {
+      target.value = undefined;
+      message.error('璇ュ璞$殑鏈夋晥琚嬬爜灏氭湭鍏ㄩ儴鍏ュ簱锛屾殏涓嶈兘璐ㄦ');
+      return;
+    }
+    if (!data.pendingBagIds?.length) {
+      target.value = undefined;
+      message.error('璇ュ璞℃病鏈夊緟璐ㄦ琚嬬爜锛岃鍦ㄨ川妫�璁板綍涓煡鐪嬪凡鏈夌粨鏋�');
+      return;
+    }
+    target.value = data;
+    resetIndicators();
+    message.success(`${SCAN_TYPE_LABEL[scanType.value]} ${data.code} 宸茶瘑鍒紝鏈寰呮 ${data.pendingBagIds.length} 琚媊);
+  } catch (error) {
+    target.value = undefined;
+    message.error(errorText(error));
+  } finally {
+    resolving.value = false;
+  }
+}
+
 async function submit() {
-  if (!code.value.trim()) {
-    message.warning('璇峰~鍐欒鐮�');
+  const current = target.value;
+  if (!current) {
+    message.warning(`璇峰厛璇嗗埆${SCAN_TYPE_LABEL[scanType.value]}`);
     return;
   }
   if (!allEntered.value) {
     message.warning('璇峰厛濉啓鍥涢」鎸囨爣鐨勫疄娴嬪��');
     return;
   }
-  if (!qualifiedWarehouseId.value) {
-    message.warning('璇烽�夋嫨鍏ュ簱鍚堟牸搴�');
-    return;
-  }
   if (indicators.value.some((item) => item.minValue === undefined || item.maxValue === undefined || item.minValue > item.maxValue)) {
     message.warning('鎸囨爣涓嬮檺涓嶈兘澶т簬涓婇檺');
+    return;
+  }
+  if (!qualifiedWarehouseId.value) {
+    message.warning('璇烽�夋嫨鍏ュ簱鍚堟牸搴�');
     return;
   }
   if (!overallPassed.value && !reason.value.trim()) {
@@ -83,43 +179,53 @@
     return;
   }
 
-  if (!overallPassed.value) {
-    const confirmed = await new Promise<boolean>((resolve) => {
-      Modal.confirm({
-        title: '纭鎻愪氦涓嶅悎鏍肩粨鏋滐紵',
-        content: '涓嶅悎鏍煎搧灏嗙敱鍚庣浠庢殏瀛樺簱瀛樹腑娓呴櫎锛屽苟淇濈暀涓嶅悎鏍煎師鍥犵敤浜庤川閲忓垎鏋愩��',
-        okText: '纭鎻愪氦',
-        cancelText: '杩斿洖淇敼',
-        okButtonProps: { danger: true },
-        onOk: () => resolve(true),
-        onCancel: () => resolve(false),
-      });
+  const count = current.pendingBagIds?.length ?? 0;
+  const scopeNote = isGroupScan.value
+    ? `鎸囨爣涓庢楠岀粨璁哄皢搴旂敤浜庢湰娆″叏閮� ${count} 琚嬶紝璇风‘璁ゆ娊鏍风粨鏋滈�傜敤浜庤鑼冨洿銆俙
+    : '';
+  const confirmed = await new Promise<boolean>((resolve) => {
+    Modal.confirm({
+      title: overallPassed.value ? '纭鎻愪氦鍚堟牸缁撴灉锛�' : '纭鎻愪氦涓嶅悎鏍肩粨鏋滐紵',
+      content: overallPassed.value
+        ? `鏈 ${count} 琚嬪皢缁熶竴鍒ゅ畾鍚堟牸锛屽苟浠庢殏瀛樺簱瀛樿浆鍏ユ墍閫夊悎鏍煎簱銆�${scopeNote}`
+        : `鏈 ${count} 琚嬪皢浠庢殏瀛樺簱瀛樹腑娓呴櫎骞舵爣璁颁笉鍚堟牸锛屽悓鏃朵繚鐣欎笉鍚堟牸鍘熷洜鐢ㄤ簬璐ㄩ噺鍒嗘瀽銆�${scopeNote}`,
+      okText: '纭鎻愪氦',
+      cancelText: '杩斿洖淇敼',
+      okButtonProps: { danger: !overallPassed.value },
+      onOk: () => resolve(true),
+      onCancel: () => resolve(false),
     });
-    if (!confirmed) return;
-  }
+  });
+  if (!confirmed) return;
 
   loading.value = true;
+  const passed = overallPassed.value;
   try {
-    submittedId.value = await submitSeedQuality({
-      code: code.value.trim(),
-      batchCode: batchCode.value.trim() || undefined,
+    const result = await submitSeedQualityGroup({
+      code: current.code,
+      batchCode: current.batchCode || undefined,
+      scanType: scanType.value,
+      bagIds: current.pendingBagIds ?? [],
+      qualifiedWarehouseId: qualifiedWarehouseId.value,
+      reason: reason.value.trim() || undefined,
       indicators: indicators.value.map(({ code: indicatorCode, value, minValue, maxValue }) => ({
         code: indicatorCode,
         value: value as number,
         minValue: minValue as number,
         maxValue: maxValue as number,
       })),
-      reason: reason.value.trim() || undefined,
-      qualifiedWarehouseId: qualifiedWarehouseId.value,
     });
-    message.success(`璐ㄦ鎻愪氦鎴愬姛锛岃川妫�缂栧彿锛�${submittedId.value}`);
+    submittedCount.value = result.processedCount;
+    message.success(`鏈 ${result.processedCount} 琚嬪凡鎻愪氦锛�${passed ? '璐ㄦ鍚堟牸锛屽凡杞叆鎵�閫夊悎鏍煎簱' : '璐ㄦ涓嶅悎鏍硷紝宸茶褰曞師鍥犲苟绉婚櫎鏆傚瓨搴撳瓨'}`);
     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' });
     }
-    resetForm(false);
+    // 妫�楠岃寖鍥村凡娑堣垂锛岄渶閲嶆柊璇嗗埆鍚庢墠鑳芥彁浜や笅涓�鎵癸紝閬垮厤璇妸涓婁竴鎵圭殑娓呭崟鍐嶆鎻愪氦
+    code.value = '';
+    target.value = undefined;
+    resetIndicators();
   } catch (error) {
-    const response = error as { message?: string; msg?: string };
-    message.error(response.message ?? response.msg ?? '璐ㄦ鎻愪氦澶辫触锛岃妫�鏌ヨ〃鍗曞悗閲嶈瘯');
+    message.error(errorText(error));
   } finally {
     loading.value = false;
   }
@@ -129,7 +235,7 @@
   try {
     const warehouses = await getWarehouseSimpleList();
     qualifiedWarehouses.value = (warehouses ?? []).filter(
-      (item) => item.type === MesWmWarehouseTypeEnum.QUALIFIED,
+      (item) => item.type === MesWmWarehouseTypeEnum.QUALIFIED && !item.frozen,
     );
   } catch {
     qualifiedWarehouses.value = [];
@@ -157,14 +263,14 @@
       <div>
         <div class="eyebrow">MES / 璐ㄩ噺绠$悊</div>
         <h1>绉嶅瓙璐ㄦ</h1>
-        <p>褰曞叆绾害銆佸噣搴︺�佸彂鑺界巼鍜屾按鍒嗭紝绯荤粺鏍规嵁鏍囧噯鑼冨洿鑷姩鍒ゅ畾妫�楠岀粨璁�</p>
+        <p>閫夋嫨琚嬬爜銆佹墭鐩樼爜鎴栨壒娆$爜璇嗗埆寰呮鑼冨洿锛屽綍鍏ョ函搴︺�佸噣搴︺�佸彂鑺界巼鍜屾按鍒嗭紝绯荤粺鑷姩鍒ゅ畾妫�楠岀粨璁�</p>
       </div>
-      <div class="header-badge"><IconifyIcon icon="ep:checked" /> 鍥涢」鎸囨爣妫�楠�</div>
+      <div class="header-badge"><IconifyIcon icon="ep:checked" /> 琚嬬爜 / 鎵樼洏鐮� / 鎵规鐮�</div>
     </header>
 
     <section class="summary-grid" aria-label="璐ㄦ鎽樿">
       <div class="summary-card blue"><IconifyIcon icon="ep:document-checked" /><div><span>妫�楠岄」鐩�</span><strong>4</strong><small>绾害銆佸噣搴︺�佸彂鑺界巼銆佹按鍒�</small></div></div>
-      <div class="summary-card green"><IconifyIcon icon="ep:success-filled" /><div><span>褰撳墠鍚堟牸椤�</span><strong>{{ passedCount }} / 4</strong><small>瀹炴椂渚濇嵁涓婁笅闄愬垽鏂�</small></div></div>
+      <div class="summary-card" :class="target ? 'green' : 'blue'"><IconifyIcon icon="ep:box" /><div><span>鏈寰呮</span><strong>{{ target ? `${pendingCount} 琚媊 : '寰呰瘑鍒�' }}</strong><small>{{ target ? `${SCAN_TYPE_LABEL[scanType]} ${target.code}` : `璇烽�夋嫨绫诲瀷鍚庤瘑鍒�${SCAN_TYPE_LABEL[scanType]}` }}</small></div></div>
       <div class="summary-card" :class="overallPassed ? 'green' : allEntered ? 'red' : 'blue'"><IconifyIcon :icon="overallPassed ? 'ep:circle-check-filled' : allEntered ? 'ep:warning-filled' : 'ep:edit-pen'" /><div><span>鎬讳綋缁撹</span><strong>{{ overallPassed ? '鍚堟牸' : allEntered ? '涓嶅悎鏍�' : '寰呭綍鍏�' }}</strong><small>{{ overallPassed ? '鍙彁浜ゅ悎鏍肩粨鏋�' : allEntered ? '璇峰~鍐欎笉鍚堟牸鍘熷洜' : `宸插綍鍏� ${enteredCount} / 4 椤筦 }}</small></div></div>
     </section>
 
@@ -172,19 +278,43 @@
       <a-card class="form-card" :bordered="false">
         <template #title><div class="card-title"><IconifyIcon icon="ep:edit-pen" />璐ㄦ淇℃伅</div></template>
         <a-form layout="vertical">
+          <a-form-item label="鎵爜绫诲瀷" required>
+            <div class="scan-row">
+              <a-select v-model:value="scanType" class="scan-type" size="large" :options="SCAN_TYPE_OPTIONS" :disabled="loading" @change="onScanTypeChange" />
+              <a-input-search v-model:value="code" class="scan-input" size="large" :disabled="loading" :loading="resolving" enter-button="璇嗗埆" :placeholder="`璇疯緭鍏ユ垨鎵弿${SCAN_TYPE_LABEL[scanType]}`" @search="identify" />
+            </div>
+            <div class="scan-hint"><IconifyIcon icon="ep:info-filled" /> 琚嬬爜鎸� 1 琚嬨�佹墭鐩樼爜鎸夊凡瑁呰鏁般�佹壒娆$爜鎸夋壒娆¤鏁扮‘瀹氭湰娆″緟妫�鑼冨洿锛屽凡妫�涓庝綔搴熻鐮佷笉閲嶅澶勭悊</div>
+          </a-form-item>
+
+          <div v-if="target" class="target-panel">
+            <div class="target-head">
+              <span class="target-type">{{ SCAN_TYPE_LABEL[scanType] }}宸茶瘑鍒�</span>
+              <span class="target-code">{{ target.code }}</span>
+              <a-tag v-if="target.businessCode" color="blue">涓氬姟鐮� {{ target.businessCode }}</a-tag>
+              <a-tag v-if="target.batchCode" color="geekblue">鎵规 {{ target.batchCode }}</a-tag>
+            </div>
+            <div class="target-stats">
+              <div><span>鏈夋晥琚嬫暟</span><strong>{{ target.totalCount ?? 0 }}</strong></div>
+              <div><span>宸叉琚嬫暟</span><strong>{{ completedCount }}</strong></div>
+              <div class="highlight"><span>鏈寰呮</span><strong>{{ pendingCount }} 琚�</strong></div>
+            </div>
+            <p v-if="isGroupScan" class="target-warning">浠ヤ笅鎸囨爣涓庢楠岀粨璁哄皢搴旂敤浜庢湰娆″叏閮� {{ pendingCount }} 琚嬶紝璇风‘璁ゆ娊鏍风粨鏋滈�傜敤浜庤鑼冨洿銆�</p>
+          </div>
+          <a-alert v-else class="target-empty" type="info" show-icon message="灏氭湭璇嗗埆璐ㄦ瀵硅薄" description="鍏堥�夋嫨鎵爜绫诲瀷骞惰瘑鍒爜鍊硷紝绯荤粺浼氬甫鍑鸿瀵硅薄鐨勬湁鏁堣鏁般�佸凡妫�琚嬫暟涓庢湰娆″緟妫�琚嬫暟銆�" />
+
           <div class="base-grid">
-            <a-form-item label="琚嬬爜" required><a-input v-model:value="code" size="large" :disabled="loading" placeholder="璇疯緭鍏ユ垨鎵弿琚嬬爜" /></a-form-item>
-            <a-form-item label="鐢熶骇鎵规"><a-input v-model:value="batchCode" size="large" :disabled="loading" placeholder="璇疯緭鍏ユ壒娆″彿锛堥�夊~锛�" /></a-form-item>
-            <a-form-item label="鍏ュ簱鍚堟牸搴�" required class="full-width">
+            <a-form-item label="鐢熶骇鎵规"><a-input :value="target?.batchCode ?? ''" size="large" disabled placeholder="璇嗗埆鍚庤嚜鍔ㄥ甫鍑�" /></a-form-item>
+            <a-form-item label="鍏ュ簱鍚堟牸搴�" required>
               <a-select
                 v-model:value="qualifiedWarehouseId"
                 size="large"
                 :disabled="loading"
+                :options="qualifiedWarehouseOptions"
                 placeholder="璇烽�夋嫨璐ㄦ鍚堟牸鍚庡叆搴撶殑鍚堟牸搴�"
-                :options="qualifiedWarehouses.map((item) => ({ label: `${item.name}锛�${item.code}锛塦, value: item.id }))"
               />
             </a-form-item>
           </div>
+          <p v-if="!qualifiedWarehouses.length" class="target-warning">鏆傛棤鍙敤鍚堟牸搴擄紝璇疯仈绯荤鐞嗗憳閰嶇疆鍚庡啀鎻愪氦璐ㄦ銆�</p>
 
           <a-divider orientation="left">鎸囨爣缁撴灉</a-divider>
           <div class="indicator-list">
@@ -200,16 +330,16 @@
           </div>
 
           <a-form-item label="涓嶅悎鏍煎師鍥�" :required="!overallPassed" class="reason-item"><a-textarea v-model:value="reason" :disabled="loading" :rows="3" :maxlength="500" show-count :placeholder="overallPassed ? '鍚堟牸缁撴灉鏃犻渶濉啓' : '璇疯鏄庝笉鍚堟牸鍘熷洜'" /></a-form-item>
-          <div class="form-actions"><a-button :disabled="loading" @click="resetForm">閲嶇疆</a-button><a-button type="primary" size="large" :loading="loading" @click="submit"><IconifyIcon icon="ep:upload-filled" />鎻愪氦璐ㄦ</a-button></div>
+          <div class="form-actions"><a-button :disabled="loading" @click="resetForm">閲嶇疆</a-button><a-button type="primary" size="large" :loading="loading" :disabled="!target" @click="submit"><IconifyIcon icon="ep:upload-filled" />鎻愪氦璐ㄦ</a-button></div>
         </a-form>
       </a-card>
 
       <a-card class="result-card" :bordered="false">
         <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>
-        <a-alert message="搴撳瓨娴佽浆鐢卞悗绔鐞�" description="鍚堟牸鍝佸皢鎸夋墍閫夈�屽叆搴撳悎鏍煎簱銆嶄粠鏆傚瓨搴撳瓨鍏ヨ鍚堟牸搴擄紱涓嶅悎鏍煎搧灏嗙洿鎺ユ竻闄ゆ殏瀛樺簱瀛樺苟淇濈暀涓嶅悎鏍煎師鍥犮�傝川妫�鍛樺拰妫�楠屾椂闂寸敱鏈嶅姟绔褰曘��" type="info" show-icon />
-        <div v-if="submittedId" class="success-note"><IconifyIcon icon="ep:success-filled" /><span>鏈�杩戞彁浜よ川妫�缂栧彿锛�<strong>{{ submittedId }}</strong></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 />
+        <div v-if="submittedCount" class="success-note"><IconifyIcon icon="ep:success-filled" /><span>鏈�杩戞彁浜よ川妫�锛�<strong>{{ submittedCount }} 琚�</strong></span></div>
       </a-card>
     </section>
   </div>
@@ -218,9 +348,11 @@
 <style scoped>
 .quality-page { min-height: 100%; padding: 24px; background: #f4f7fb; color: #172033; }
 .page-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 22px; }.eyebrow { color: #1677ff; font-size: 12px; font-weight: 700; letter-spacing: .08em; }.page-header h1 { margin: 6px 0; font-size: 28px; }.page-header p { margin: 0; color: #718096; }.header-badge { display: inline-flex; align-items: center; gap: 8px; padding: 9px 13px; border: 1px solid #cfe0fb; border-radius: 7px; color: #1677ff; background: #f0f6ff; font-size: 13px; font-weight: 600; }
-.summary-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 18px; }.summary-card { display: flex; align-items: flex-start; gap: 14px; padding: 17px 19px; border: 1px solid #e8edf5; border-radius: 10px; background: #fff; box-shadow: 0 3px 12px #243b5310; }.summary-card > .iconify { flex: 0 0 38px; width: 38px; height: 38px; padding: 9px; border-radius: 8px; font-size: 20px; }.summary-card.blue > .iconify { color: #1677ff; background: #eaf3ff; }.summary-card.green > .iconify { color: #17a673; background: #e6f8f0; }.summary-card.red > .iconify { color: #e05252; background: #fff0f0; }.summary-card span, .summary-card small { display: block; color: #7b8799; font-size: 12px; }.summary-card strong { display: block; margin: 4px 0; font-size: 23px; }.content-grid { display: grid; grid-template-columns: minmax(0, 1.5fr) minmax(310px, .75fr); gap: 18px; }.form-card, .result-card { border-radius: 10px; box-shadow: 0 3px 12px #243b5310; }.card-title { display: flex; align-items: center; gap: 9px; font-weight: 700; }.card-title :deep(.iconify) { color: #1677ff; }.base-grid { display: grid; grid-template-columns: 1.3fr 1.2fr; gap: 16px; }.full-width { width: 100%; }
+.summary-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 18px; }.summary-card { display: flex; align-items: flex-start; gap: 14px; padding: 17px 19px; border: 1px solid #e8edf5; border-radius: 10px; background: #fff; box-shadow: 0 3px 12px #243b5310; }.summary-card > .iconify { flex: 0 0 38px; width: 38px; height: 38px; padding: 9px; border-radius: 8px; font-size: 20px; }.summary-card.blue > .iconify { color: #1677ff; background: #eaf3ff; }.summary-card.green > .iconify { color: #17a673; background: #e6f8f0; }.summary-card.red > .iconify { color: #e05252; background: #fff0f0; }.summary-card span, .summary-card small { display: block; color: #7b8799; font-size: 12px; }.summary-card small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.summary-card strong { display: block; margin: 4px 0; font-size: 23px; }.content-grid { display: grid; grid-template-columns: minmax(0, 1.5fr) minmax(310px, .75fr); gap: 18px; }.form-card, .result-card { border-radius: 10px; box-shadow: 0 3px 12px #243b5310; }.card-title { display: flex; align-items: center; gap: 9px; font-weight: 700; }.card-title :deep(.iconify) { color: #1677ff; }.base-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }.full-width { width: 100%; }
+.scan-row { display: flex; align-items: stretch; gap: 10px; }.scan-type { flex: 0 0 140px; }.scan-input { flex: 1 1 auto; min-width: 0; }.scan-hint { display: flex; align-items: center; gap: 8px; margin-top: 10px; color: #8793a5; font-size: 12px; line-height: 1.5; }.scan-hint :deep(.iconify) { flex: 0 0 auto; color: #1677ff; }
+.target-panel { margin-bottom: 18px; padding: 14px 16px; border: 1px solid #d6e8ff; border-radius: 8px; background: #f7fbff; }.target-head { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; }.target-type { color: #1677ff; font-size: 13px; font-weight: 700; }.target-code { font-size: 16px; font-weight: 700; letter-spacing: .02em; }.target-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 12px; }.target-stats div { padding: 9px 12px; border: 1px solid #e8edf5; border-radius: 6px; background: #fff; }.target-stats span { display: block; color: #7b8799; font-size: 12px; }.target-stats strong { display: block; margin-top: 3px; font-size: 19px; }.target-stats .highlight { border-color: #b7ebd2; background: #f2fcf7; }.target-stats .highlight strong { color: #16835b; }.target-warning { margin: 12px 0 0; color: #c07a17; font-size: 12px; line-height: 1.6; }.target-empty { margin-bottom: 18px; }
 .indicator-list { display: flex; flex-direction: column; gap: 10px; }.indicator-row { display: flex; align-items: center; gap: 9px; padding: 11px 12px; border: 1px solid #e8edf5; border-radius: 7px; background: #fbfcfe; }.indicator-row.failed { border-color: #ffd6d6; background: #fffafa; }.indicator-name { display: flex; align-items: center; gap: 8px; min-width: 82px; }.indicator-name small { color: #8793a5; font-size: 12px; }.indicator-dot { width: 8px; height: 8px; border-radius: 50%; }.indicator-dot.pass { background: #20b878; }.indicator-dot.fail { background: #e05252; }.indicator-input { width: 105px; }.range-input { width: 88px; }.range-separator { margin-left: auto; color: #8793a5; font-size: 12px; }.reason-item { margin-top: 18px; }.form-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 5px; }.form-actions :deep(.iconify) { margin-right: 5px; }
 .result-hero { display: flex; flex-direction: column; align-items: center; gap: 8px; margin: 8px 0 22px; padding: 25px 15px; border-radius: 8px; text-align: center; }.result-hero :deep(.iconify) { font-size: 35px; }.result-hero strong { font-size: 18px; }.result-hero span { color: #718096; font-size: 12px; }.result-hero.passed { color: #16835b; background: #effaf5; }.result-hero.failed-result { color: #c53e3e; background: #fff3f3; }.result-hero.pending-result { color: #1677ff; background: #f0f6ff; }.rule-list { display: flex; flex-direction: column; gap: 14px; margin-bottom: 22px; }.rule-list div { display: flex; align-items: center; gap: 9px; color: #59677b; font-size: 13px; }.rule-list :deep(.iconify) { color: #20b878; }.success-note { display: flex; align-items: center; gap: 8px; margin-top: 16px; padding: 11px 12px; border-radius: 7px; color: #16835b; background: #effaf5; font-size: 13px; }
 @media (max-width: 1050px) { .content-grid { grid-template-columns: 1fr; }.base-grid { grid-template-columns: repeat(2, 1fr); } }
-@media (max-width: 760px) { .quality-page { padding: 14px; }.page-header { display: block; }.header-badge { margin-top: 14px; }.summary-grid, .base-grid { grid-template-columns: 1fr; }.indicator-row { flex-wrap: wrap; }.range-separator { margin-left: 0; }.indicator-input { flex: 1; min-width: 105px; }.range-input { flex: 1; min-width: 75px; } }
+@media (max-width: 760px) { .quality-page { padding: 14px; }.page-header { display: block; }.header-badge { margin-top: 14px; }.summary-grid, .base-grid, .target-stats { grid-template-columns: 1fr; }.scan-row { flex-wrap: wrap; }.scan-type { flex-basis: 100%; }.indicator-row { flex-wrap: wrap; }.range-separator { margin-left: 0; }.indicator-input { flex: 1; min-width: 105px; }.range-input { flex: 1; min-width: 75px; } }
 </style>
diff --git a/vite.config.ts b/vite.config.ts
index 8e512c3..0d6b580 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -76,7 +76,7 @@
         proxy: {
           "/admin-api": {
             changeOrigin: true,
-            target: "http://localhost:48080",
+            target: "http://127.0.0.1:48080",
             ws: true,
           },
         },

--
Gitblit v1.9.3