From f2e80b9f8dddd8da58b9840cf69139d028aaf0d4 Mon Sep 17 00:00:00 2001
From: liu <2021943741@qq.com>
Date: 星期五, 16 十月 2026 17:45:36 +0800
Subject: [PATCH] feat(mes): 二维码导出接入后台任务与可隐藏进度浮层
---
src/api/mes/pro/qrExport/index.ts | 85 +++++++
src/views/mes/pro/bagCode/index.vue | 35 ++
src/views/mes/pro/qrexport/qr-export-progress.vue | 189 +++++++++++++++
src/views/mes/pro/qrexport/use-qr-export.ts | 387 ++++++++++++++++++++++++++++++++
4 files changed, 691 insertions(+), 5 deletions(-)
diff --git a/src/api/mes/pro/qrExport/index.ts b/src/api/mes/pro/qrExport/index.ts
new file mode 100644
index 0000000..b7e83d2
--- /dev/null
+++ b/src/api/mes/pro/qrExport/index.ts
@@ -0,0 +1,85 @@
+import { requestClient } from '#/api/request';
+
+export namespace MesProQrExportApi {
+ /** 涓氬姟绫诲瀷 */
+ export type BizType =
+ | 'BAG_BUSINESS'
+ | 'BAG_LABEL'
+ | 'BATCH_BUSINESS'
+ | 'BATCH_LABEL'
+ | 'PALLET_BUSINESS'
+ | 'PALLET_LABEL';
+
+ /** 瀵煎嚭鏂瑰紡锛歋YNC 鍚屾鐩存帴涓嬭浇锛堢鍑猴級/ ASYNC 鍚庡彴浠诲姟 */
+ export type ExportMode = 'ASYNC' | 'SYNC';
+
+ /** 瀵煎嚭浠诲姟鐘舵�侊細10 鎺掗槦涓� 20 鐢熸垚涓� 30 寰呬笅杞� 40 宸插彇娑� 50 宸插け璐� 60 宸蹭笅杞� */
+ export type TaskStatus = 10 | 20 | 30 | 40 | 50 | 60;
+
+ /** 瀵煎嚭鍙傛暟 */
+ export interface ExportParams {
+ bizType: BizType;
+ batchId?: number;
+ uuid?: string;
+ }
+
+ /** 瀵煎嚭鏁伴噺涓庢柟寮� */
+ export interface ExportCount {
+ mode: ExportMode;
+ syncThreshold: number;
+ totalCount: number;
+ }
+
+ /** 瀵煎嚭浠诲姟 */
+ export interface ExportTask {
+ id: number;
+ bizType: string;
+ taskName: string;
+ status: TaskStatus;
+ statusName: string;
+ totalCount: number;
+ finishedCount: number;
+ progress: number;
+ fileSize?: number;
+ errorMsg?: string;
+ downloadable: boolean;
+ createTime?: string;
+ finishTime?: string;
+ }
+}
+
+/** 缁熻瀵煎嚭寮犳暟骞剁粰鍑哄鍑烘柟寮� */
+export function getQrExportCount(params: MesProQrExportApi.ExportParams) {
+ return requestClient.get<MesProQrExportApi.ExportCount>(
+ '/mes/pro/qr-export/count',
+ { params },
+ );
+}
+
+/** 鎻愪氦鍚庡彴瀵煎嚭浠诲姟 */
+export function submitQrExportTask(data: MesProQrExportApi.ExportParams) {
+ return requestClient.post<MesProQrExportApi.ExportTask>(
+ '/mes/pro/qr-export/task',
+ data,
+ );
+}
+
+/** 鏌ヨ瀵煎嚭浠诲姟杩涘害 */
+export function getQrExportTask(id: number) {
+ return requestClient.get<MesProQrExportApi.ExportTask>(
+ '/mes/pro/qr-export/task/get',
+ { params: { id } },
+ );
+}
+
+/** 鍙栨秷瀵煎嚭浠诲姟 */
+export function cancelQrExportTask(id: number) {
+ return requestClient.post<boolean>('/mes/pro/qr-export/task/cancel', null, {
+ params: { id },
+ });
+}
+
+/** 瀵煎嚭浠诲姟浜х墿涓嬭浇鍦板潃锛堜笉甯﹂壌鏉冨ご锛屼緵娴佸紡涓嬭浇鑷闄勫姞 token锛� */
+export function getQrExportDownloadPath(id: number) {
+ return `/mes/pro/qr-export/task/download?id=${id}`;
+}
diff --git a/src/views/mes/pro/bagCode/index.vue b/src/views/mes/pro/bagCode/index.vue
index cf22f99..6bd6b3a 100644
--- a/src/views/mes/pro/bagCode/index.vue
+++ b/src/views/mes/pro/bagCode/index.vue
@@ -30,6 +30,8 @@
import ReplaceModal from "./modules/replace-form.vue";
import QrTraceModal from "../components/qr-trace-modal.vue";
import BusinessQrModal from "../components/business-qr.vue";
+ import QrExportProgress from "../qrexport/qr-export-progress.vue";
+ import { useQrExport } from "../qrexport/use-qr-export";
defineOptions({ name: "MesProBagCode" });
@@ -91,6 +93,8 @@
gridApi.query();
}
+ const { start: startQrExport } = useQrExport();
+
/** 鐢熸垚鏁版嵁鍖� */
function handleGenerate() {
generateFormModalApi.open();
@@ -131,8 +135,24 @@
message.warning("璇峰厛鎸夌敓鎴愮粍鎴栨壒娆$瓫閫夊悗鍐嶅鍑轰簩缁寸爜");
return;
}
- const data = await (kind === 'business' ? exportBagBusinessQr : exportBagQr)(params);
- downloadFileFromBlobPart({ fileName: kind === 'business' ? '琚嬬爜涓氬姟浜岀淮鐮�.zip' : '琚嬬爜杩芥函浜岀淮鐮�.zip', source: data });
+ // 鏁伴噺灏戝垯鍚屾绉掑嚭锛涜秴杩囧垎鐣岀嚎鑷姩杞悗鍙颁换鍔★紝鐢辫繘搴︽诞灞傚睍绀虹湡瀹炶繘搴﹀苟鏀寔鍙栨秷/闅愯棌
+ const bizType = kind === "business" ? "BAG_BUSINESS" : "BAG_LABEL";
+ const fileName = kind === "business" ? "琚嬬爜涓氬姟浜岀淮鐮�" : "琚嬬爜杩芥函浜岀淮鐮�";
+ await startQrExport(
+ { bizType, ...params },
+ {
+ fileName,
+ syncDownload: async () => {
+ const data = await (kind === "business"
+ ? exportBagBusinessQr
+ : exportBagQr)(params);
+ downloadFileFromBlobPart({
+ fileName: `${fileName}.zip`,
+ source: data,
+ });
+ },
+ },
+ );
}
/** 瀵煎嚭琚嬬爜 Excel锛堜氦浠樺嵃鍒凤級锛屽鍑哄悗琚嬬爜鐘舵�佸彉涓哄凡鍗板埛锛岄渶鍒锋柊鍒楄〃 */
@@ -238,11 +258,14 @@
function handleBindPallet() {
const records = getSelectedRecords();
if (!records) return;
- if (records.some((record) => !record.batchId)) {
- message.warning("璇峰厛褰曞叆鐢熶骇鎵规锛屽啀缁戝畾鎵樼洏");
+ // 宸插綍鍏ユ壒娆$殑琚嬬爜鍙兘缁戝埌鏈壒娆$殑鎵樼洏锛涙湭褰曞叆鎵规鐨勮鐮佸彲缁戜换鎰忔墭鐩橈紙閫夊埌姝e紡鎵樼洏鏃跺悗绔細鍏堣ˉ褰曟壒娆★級銆�
+ // 涓よ�呮贩閫夋椂鎵规褰掑睘涓嶆槑纭紝瑕佹眰鍒嗗紑鎿嶄綔銆�
+ const boundRecords = records.filter((record) => record.batchId);
+ if (boundRecords.length > 0 && boundRecords.length < records.length) {
+ message.warning("鎵�閫夎鐮佷腑鏃㈡湁宸插綍鍏ユ壒娆°�佷篃鏈夋湭褰曞叆鎵规鐨勮褰曪紝璇峰垎寮�缁戝畾");
return;
}
- const batchIds = new Set(records.map((record) => record.batchId));
+ const batchIds = new Set(boundRecords.map((record) => record.batchId));
if (batchIds.size > 1) {
message.warning("璇烽�夋嫨鍚屼竴鎵规涓嬬殑琚嬬爜杩涜缁戝畾");
return;
@@ -264,6 +287,7 @@
.setData({
batchId: first.batchId,
batchCode: first.batchCode,
+ itemId: first.itemId,
ids: records.map((record) => record.id!),
})
.open();
@@ -347,6 +371,7 @@
<ReplaceFormModal @success="handleReplaceSuccess" />
<QrTraceQueryModal />
<BusinessQrViewModal />
+ <QrExportProgress />
<Grid table-title="琚嬬爜鍒楄〃锛堜竴琚嬩竴鐮侊級">
<template #table-title><span class="text-[1rem] font-bold" style="white-space:nowrap">琚嬬爜鍒楄〃锛堜竴琚嬩竴鐮侊級</span></template>
<template #toolbar-tools>
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,
+ };
+}
--
Gitblit v1.9.3