| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232 |
- <template>
- <div :class="prefixCls" class="case-clean-progress">
- <div class="page-header">
- <div class="page-title-wrap">
- <div>
- <div class="page-title">数据清洗导入</div>
- <div class="page-subtitle">实时展示文件清洗、入库与治理的执行进度,便于快速确认任务运行状态。</div>
- </div>
- </div>
- </div>
- <div class="progress-layout">
- <div class="progress-card">
- <template v-if="!isComplete">
- <div class="progress-card__header">
- <div>
- <div class="panel-title">任务执行中</div>
- <div class="panel-desc">{{ progressStatusText }}</div>
- </div>
- <div class="progress-text">{{ progress }}%</div>
- </div>
- <div class="progress-ring-section">
- <div class="progress-ring" :class="{ complete: isComplete }">
- <svg viewBox="0 0 120 120">
- <defs>
- <linearGradient id="progressGradient" x1="0" y1="0" x2="1" y2="1">
- <stop offset="0%" stop-color="#60a5fa" />
- <stop offset="100%" stop-color="#2563eb" />
- </linearGradient>
- <linearGradient id="progressComplete" x1="0" y1="0" x2="1" y2="1">
- <stop offset="0%" stop-color="#34d399" />
- <stop offset="100%" stop-color="#16a34a" />
- </linearGradient>
- </defs>
- <circle cx="60" cy="60" r="50" class="ring-bg" />
- <circle
- cx="60"
- cy="60"
- r="50"
- class="ring-value"
- :style="{ strokeDasharray: `${progressDash} 314`, strokeDashoffset: `${314 - progressDash}` }"
- />
- </svg>
- <div class="progress-value">
- <strong>{{ progress }}%</strong>
- <span>{{ currentStepLabel }}</span>
- </div>
- </div>
- <div class="progress-summary" :class="{ 'is-govern': activeStep >= 2 }">
- <div class="summary-title">当前任务概况</div>
- <div class="summary-desc">{{ summaryDesc }}</div>
- <div class="summary-file">{{ summaryMetaLabel }}:{{ summaryMetaText }}</div>
- </div>
- </div>
- <div class="step-track">
- <div class="step" :class="{ active: activeStep === 0, done: activeStep > 0 }">
- <div class="step-icon">
- <span class="step-dot"></span>
- </div>
- <div class="step-title">读取数据</div>
- </div>
- <div class="step-line"></div>
- <div class="step" :class="{ active: activeStep === 1, done: activeStep > 1 }">
- <div class="step-icon">
- <span class="step-dot"></span>
- </div>
- <div class="step-title">清洗入库</div>
- </div>
- <div class="step-line"></div>
- <div class="step" :class="{ active: activeStep === 2, done: activeStep > 2 }">
- <div class="step-icon">
- <span class="step-dot"></span>
- </div>
- <div class="step-title">数据治理</div>
- </div>
- </div>
- <div class="status-grid">
- <div class="status-card">
- <div class="status-label">当前步骤</div>
- <div class="status-value">{{ currentStepLabel }}</div>
- </div>
- <div class="status-card">
- <div class="status-label">进度状态</div>
- <div class="status-value">{{ progressStatusText }}</div>
- </div>
- <div class="status-card">
- <div class="status-label">推送连接</div>
- <div class="status-value">{{ sseConnected ? '已连接' : '未连接' }}</div>
- </div>
- </div>
- </template>
- <template v-else>
- <div class="progress-complete">
- <div class="complete-icon">✓</div>
- <div class="complete-title">{{ completeTitle }}</div>
- <div class="complete-desc">{{ completeDesc }}</div>
- <div class="complete-stats">
- <div class="stat-row">
- <span class="stat-label">导入成功</span>
- <span class="stat-value">{{ displaySuccessCount }} 条</span>
- </div>
- <div class="stat-row">
- <span class="stat-label">导入失败</span>
- <span class="stat-value">{{ displayFailCount }} 条</span>
- </div>
- </div>
- <div class="complete-actions">
- <Button v-if="showErrorLogAction" class="complete-log" @click="handleViewLog">查看错误日志</Button>
- <Button type="primary" class="complete-result" @click="handleViewResult">查看导入结果</Button>
- </div>
- </div>
- </template>
- </div>
- </div>
- <CleanLogModal v-model:open="logModalOpen" :file-infos="currentCleanFiles" :batch-id="currentCleanBatchId" />
- </div>
- </template>
- <script lang="ts" setup name="ViewsDataCleanProgress">
- import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
- import { Button } from 'ant-design-vue';
- import { useGo } from '@jeesite/core/hooks/web/usePage';
- import { PageEnum } from '@jeesite/core/enums/pageEnum';
- import { useDesign } from '@jeesite/core/hooks/web/useDesign';
- import { useMessage } from '@jeesite/core/hooks/web/useMessage';
- import { useEmitter, useUserStore } from '@jeesite/core/store/modules/case';
- import { markAnalysisDataChanged } from '@jeesite/core/logics/analysisRefreshPrompt';
- import {
- calcTask,
- doClean,
- getGovernConfList,
- getGovernTaskSseUrl,
- testSse,
- } from '@jeesite/case/api/govern/governApi';
- import CleanLogModal from './components/CleanLogModal.vue';
- import {
- clearImportProgress,
- getActiveImportProgress,
- getImportProgressEventSource,
- resetImportProgressRuntimeState,
- saveImportProgress,
- setImportProgressEventSource,
- useImportProgressRuntimeState,
- } from './importProgressSession';
- import type { FileInfo } from '@jeesite/types/api';
- const { prefixCls } = useDesign('case-clean-progress');
- const go = useGo();
- const emitter = useEmitter();
- const userStore = useUserStore();
- const { createMessage, createWarningModal } = useMessage();
- const CASE_DATA_REFRESH_EVENT = 'case-data:refresh';
- const CASE_IMPORT_FILE_REFRESH_EVENT = 'case-import-file:refresh';
- const FILE_INFO_STORAGE_KEY = 'case_clean_file_infos';
- const IMPORT_FILE_INFO_STORAGE_KEY = 'case_clean_import_file_infos';
- const RECLEAN_CONTEXT_STORAGE_KEY = 'case_clean_reclean_context';
- const GOVERN_START_TASK_PROCESS = 1;
- const {
- progress,
- activeStep,
- currentFileName,
- currentCleanFiles,
- currentCleanBatchId,
- sseConnected,
- sseError,
- cleanTaskStarted,
- calcTaskTriggered,
- cleanTaskFinished,
- governTaskStarted,
- governTaskFinished,
- shouldRunGovernAfterClean,
- governCompleteNotified,
- successCount,
- failCount,
- cleanStatsSnapshot,
- } = useImportProgressRuntimeState();
- const sseMessages = ref<string[]>([]);
- const logModalOpen = ref(false);
- let eventSource = getImportProgressEventSource();
- const closeSse = () => {
- const activeEventSource = eventSource || getImportProgressEventSource();
- activeEventSource?.close();
- if (getImportProgressEventSource() === activeEventSource) {
- setImportProgressEventSource(null);
- }
- eventSource = null;
- sseConnected.value = false;
- };
- const progressDash = computed(() => Math.round((progress.value / 100) * 314));
- const isComplete = computed(
- () => progress.value >= 100 && (!shouldRunGovernAfterClean.value || governTaskFinished.value),
- );
- const currentStepLabel = computed(() => {
- if (activeStep.value >= 2) return '数据治理';
- if (activeStep.value === 1) return '清洗入库';
- return '读取数据';
- });
- const progressStatusText = computed(() => {
- if (isComplete.value) return governTaskFinished.value ? '数据治理完成' : '清洗入库流程已完成';
- return `当前正在执行${currentStepLabel.value}`;
- });
- const summaryDesc = computed(() => {
- if (activeStep.value >= 2) {
- return '清洗入库已完成,系统正在执行关系去重、数据识别、关系合并与图谱治理计算。';
- }
- if (activeStep.value === 1) {
- return '系统正在将清洗后的文件写入案件数据,并同步统计导入成功与失败数量。';
- }
- return '系统正在读取上传文件并准备清洗任务,完成后将进入清洗入库流程。';
- });
- const summaryMetaLabel = computed(() => (activeStep.value >= 2 ? '治理对象' : '当前处理文件'));
- const summaryMetaText = computed(
- () => currentFileName.value || (activeStep.value >= 2 ? '本次导入文件' : '暂无文件'),
- );
- const completeTitle = computed(() => (governTaskFinished.value ? '数据治理完成' : '清洗导入完成'));
- const showErrorLogAction = computed(() => Number(displayFailCount.value ?? 0) > 0);
- const completeDesc = computed(() =>
- governTaskFinished.value
- ? '数据治理已完成,治理结果已写入案件数据,您可以查看导入结果。'
- : showErrorLogAction.value
- ? '任务已完成,您可以查看错误日志或查看导入结果。'
- : '任务已完成,您可以直接查看导入结果。',
- );
- const displaySuccessCount = computed(() => cleanStatsSnapshot.value.success ?? successCount.value);
- const displayFailCount = computed(() => cleanStatsSnapshot.value.fail ?? failCount.value);
- const getCurrentCaseId = () => userStore.getCaseInfo?.id;
- const persistImportProgress = () => {
- if (isComplete.value) {
- clearImportProgress(getCurrentCaseId());
- return;
- }
- if (!cleanTaskStarted.value) return;
- saveImportProgress({
- active: true,
- caseId: getCurrentCaseId(),
- progress: progress.value,
- activeStep: activeStep.value,
- currentFileName: currentFileName.value,
- currentCleanFiles: currentCleanFiles.value,
- currentCleanBatchId: currentCleanBatchId.value,
- successCount: successCount.value,
- failCount: failCount.value,
- cleanStatsSnapshot: cleanStatsSnapshot.value,
- cleanTaskStarted: cleanTaskStarted.value,
- cleanTaskFinished: cleanTaskFinished.value,
- calcTaskTriggered: calcTaskTriggered.value,
- governTaskStarted: governTaskStarted.value,
- governTaskFinished: governTaskFinished.value,
- shouldRunGovernAfterClean: shouldRunGovernAfterClean.value,
- updatedAt: Date.now(),
- });
- };
- const restoreImportProgress = () => {
- const snapshot = getActiveImportProgress(getCurrentCaseId());
- if (!snapshot) return false;
- progress.value = snapshot.progress;
- activeStep.value = snapshot.activeStep;
- currentFileName.value = snapshot.currentFileName;
- currentCleanFiles.value = snapshot.currentCleanFiles || [];
- currentCleanBatchId.value = snapshot.currentCleanBatchId;
- successCount.value = snapshot.successCount;
- failCount.value = snapshot.failCount;
- cleanStatsSnapshot.value = snapshot.cleanStatsSnapshot || {};
- cleanTaskStarted.value = snapshot.cleanTaskStarted;
- cleanTaskFinished.value = snapshot.cleanTaskFinished;
- calcTaskTriggered.value = snapshot.calcTaskTriggered;
- governTaskStarted.value = snapshot.governTaskStarted;
- governTaskFinished.value = snapshot.governTaskFinished;
- shouldRunGovernAfterClean.value = snapshot.shouldRunGovernAfterClean;
- return true;
- };
- watch(
- [
- progress,
- activeStep,
- currentFileName,
- currentCleanFiles,
- currentCleanBatchId,
- successCount,
- failCount,
- cleanStatsSnapshot,
- cleanTaskStarted,
- cleanTaskFinished,
- calcTaskTriggered,
- governTaskStarted,
- governTaskFinished,
- shouldRunGovernAfterClean,
- ],
- persistImportProgress,
- { deep: true },
- );
- const normalizeText = (value: unknown) => String(value ?? '').trim();
- const normalizeBatchId = (value: unknown) => normalizeText(value);
- const findBatchId = (items?: FileInfo[] | null): string => {
- if (!Array.isArray(items) || !items.length) return '';
- for (const item of items) {
- const batchId = normalizeBatchId(item?.batchId);
- if (batchId) return batchId;
- const childBatchId = findBatchId(Array.isArray(item?.children) ? item.children : []);
- if (childBatchId) return childBatchId;
- }
- return '';
- };
- const refreshCurrentCleanBatchId = () => {
- const preFileRaw = sessionStorage.getItem(FILE_INFO_STORAGE_KEY);
- if (preFileRaw) {
- try {
- const preFileInfos = JSON.parse(preFileRaw) as FileInfo[];
- const preFileBatchId = findBatchId(preFileInfos);
- if (preFileBatchId) {
- currentCleanBatchId.value = preFileBatchId;
- return currentCleanBatchId.value;
- }
- } catch (error) {
- // Ignore invalid preFile cache and fallback to current task files.
- }
- }
- currentCleanBatchId.value = findBatchId(currentCleanFiles.value);
- return currentCleanBatchId.value;
- };
- const handleViewLog = () => {
- if (!showErrorLogAction.value) return;
- refreshCurrentCleanBatchId();
- logModalOpen.value = true;
- };
- const navigateToImportResult = async () => {
- sessionStorage.removeItem(IMPORT_FILE_INFO_STORAGE_KEY);
- sessionStorage.removeItem(RECLEAN_CONTEXT_STORAGE_KEY);
- markAnalysisDataChanged();
- await go(PageEnum.BASE_HOME, true);
- await nextTick();
- emitter.emit(CASE_DATA_REFRESH_EVENT);
- };
- const handleViewResult = async () => {
- if (!shouldRunGovernAfterClean.value) {
- createWarningModal({
- title: '提示',
- content: '数据治理关闭,数据未更新,请手动治理。',
- async onOk() {
- await navigateToImportResult();
- },
- });
- return;
- }
- await navigateToImportResult();
- };
- type SseDTO = {
- cmd: number;
- code: number;
- process: number;
- message: string;
- seeData?: Recordable;
- data?: Recordable;
- result?: Recordable;
- successCount?: number;
- failCount?: number;
- successNum?: number;
- failNum?: number;
- };
- const readNumber = (source: Recordable | null | undefined, keys: string[]) => {
- if (!source) return undefined;
- for (const key of keys) {
- const value = source[key];
- if (value !== undefined && value !== null && value !== '') {
- const numberValue = Number(value);
- if (Number.isFinite(numberValue)) return numberValue;
- }
- }
- return undefined;
- };
- const normalizeCleanResult = (result: Recordable | null | undefined) => {
- const nested =
- result?.data && typeof result.data === 'object'
- ? (result.data as Recordable)
- : result?.seeData && typeof result.seeData === 'object'
- ? (result.seeData as Recordable)
- : result?.result && typeof result.result === 'object'
- ? (result.result as Recordable)
- : result;
- const data = nested || {};
- const completedCount = readNumber(data, ['completedCount', 'successCount', 'successNum', 'success']);
- const failCount = readNumber(data, ['failCount', 'failNum', 'failedCount', 'fail']);
- return {
- success: completedCount,
- fail: failCount,
- process: readNumber(data, ['process', 'progress', 'taskProcess']),
- };
- };
- const normalizeSseProcess = (value: unknown) => {
- const numberValue = Number(value);
- if (!Number.isFinite(numberValue)) return progress.value;
- return Math.min(100, Math.max(0, Math.round(numberValue)));
- };
- const extractCurrentFileName = (payload: SseDTO) => {
- return payload.message;
- };
- const applyCleanResult = (result: Recordable | null | undefined, applyProcess = true) => {
- const normalized = normalizeCleanResult(result);
- if (normalized.success !== undefined) {
- successCount.value = normalized.success;
- cleanStatsSnapshot.value = { ...cleanStatsSnapshot.value, success: normalized.success };
- }
- if (normalized.fail !== undefined) {
- failCount.value = normalized.fail;
- cleanStatsSnapshot.value = { ...cleanStatsSnapshot.value, fail: normalized.fail };
- }
- if (applyProcess && normalized.process !== undefined) {
- progress.value = Math.min(100, Math.max(0, normalized.process));
- }
- };
- // const isCleanFinishedResult = (result: Recordable | null | undefined) => {
- // const normalized = normalizeCleanResult(result);
- // if (normalized.process !== undefined && normalized.process >= 100) return true;
- // const cmd = Number(result?.cmd ?? result?.data?.cmd ?? result?.seeData?.cmd ?? result?.result?.cmd);
- // if (cmd === 102) return false;
- // const data = (result?.data || result?.seeData || result?.result || result || {}) as Recordable;
- // const message = String(data?.message || data?.msg || result?.message || result?.msg || '').trim();
- // return (
- // message.includes('清洗导入完成') ||
- // message.includes('清洗完成') ||
- // message.includes('清洗结束') ||
- // message.includes('入库完成') ||
- // message.includes('全部清洗完成') ||
- // message.includes('全部入库完成')
- // );
- // };
- const startGovernAfterClean = async () => {
- // if (!shouldRunGovernAfterClean.value) {
- // await triggerCalcTask();
- // return;
- // }
- activeStep.value = 2;
- await triggerCalcTask();
- };
- const safeEmitImportRefresh = () => {
- try {
- emitter.emit(CASE_IMPORT_FILE_REFRESH_EVENT);
- } catch (error) {
- console.error('[clean-progress] import refresh emit failed', error);
- }
- };
- // const handleCleanFinished = async () => {
- // if (calcTaskTriggered.value || governTaskStarted.value || governTaskFinished.value) return;
- // if (!cleanTaskFinished.value) {
- // cleanTaskFinished.value = true;
- // }
- // await startGovernAfterClean();
- // safeEmitImportRefresh();
- // };
- const normalizeGovernConfFlag = (value?: unknown) =>
- String(value ?? '')
- .trim()
- .toUpperCase();
- const getRequestErrorMessage = (error: any, fallback = '操作失败') =>
- error?.response?.data?.message || error?.message || fallback;
- const loadGovernSwitch = async () => {
- try {
- const list = await getGovernConfList();
- const hasGovernConf = (Array.isArray(list) ? list : []).find(
- (item) =>
- String(item?.confType ?? '')
- .trim()
- .toLowerCase() === 'hasgovern',
- );
- // `hasGovern` 的实际语义以接口返回为准:N 表示导入不立即治理,Y 表示立即治理。
- shouldRunGovernAfterClean.value = normalizeGovernConfFlag(hasGovernConf?.confStr) === 'Y';
- } catch (error) {
- shouldRunGovernAfterClean.value = true;
- createMessage.warning('治理配置读取失败,将按默认流程继续执行数据治理');
- }
- };
- const isGovernCompleteText = (text: string) =>
- ['数据治理完成', '治理完成', '治理结束', '治理任务完成'].some((keyword) => text.includes(keyword));
- const isGovernCompletePayload = (payload: SseDTO, raw = '') => {
- const value = Number(payload.process ?? 0);
- const message = `${payload.message || ''}${raw}`;
- if (isGovernCompleteText(message)) return true;
- if (payload.cmd === 101) return false;
- return (governTaskStarted.value || calcTaskTriggered.value || activeStep.value >= 2) && value >= 100;
- };
- const markGovernComplete = () => {
- if (governTaskFinished.value) return;
- activeStep.value = 2;
- governTaskFinished.value = true;
- closeSse();
- clearImportProgress(getCurrentCaseId());
- safeEmitImportRefresh();
- if (!governCompleteNotified.value) {
- governCompleteNotified.value = true;
- createMessage.success('数据治理完成');
- }
- };
- const triggerCalcTask = async () => {
- if (!shouldRunGovernAfterClean.value) {
- activeStep.value = 1;
- progress.value = 100;
- closeSse();
- persistImportProgress();
- return;
- }
- if (calcTaskTriggered.value) return;
- calcTaskTriggered.value = true;
- persistImportProgress();
- try {
- if (!eventSource) {
- connectSse();
- }
- await calcTask({
- // 治理启动接口固定传起始进度值,不能复用清洗完成时返回的 100。
- taskProcess: progress.value,
- });
- governTaskStarted.value = true;
- persistImportProgress();
- if (!eventSource || !sseConnected.value) {
- connectSse();
- }
- createMessage.success('治理任务已启动');
- } catch (error: any) {
- calcTaskTriggered.value = false;
- governTaskStarted.value = false;
- persistImportProgress();
- createMessage.error(getRequestErrorMessage(error, '治理任务启动失败'));
- }
- };
- const updateProgressFromMessage = (rawMessage: string) => {
- const normalized = rawMessage.trim();
- if (!normalized) return;
- let payload: SseDTO | null = null;
- try {
- payload = JSON.parse(normalized) as SseDTO;
- } catch (error) {
- payload = null;
- }
- if (payload) {
- const value = normalizeSseProcess(payload.process);
- if (payload.cmd === 101) {
- const shouldApplyCleanProgress = !calcTaskTriggered.value && activeStep.value < 2;
- const fileName = extractCurrentFileName(payload);
- if (fileName) {
- currentFileName.value = fileName;
- }
- applyCleanResult(payload as unknown as Recordable, shouldApplyCleanProgress);
- if (shouldApplyCleanProgress) {
- activeStep.value = 1;
- progress.value = value;
- }
- // if (isCleanFinishedResult(payload as unknown as Recordable)) {
- // void handleCleanFinished();
- // }
- if (value >= 100) {
- cleanTaskFinished.value = true;
- }
- } else if (payload.cmd === 102) {
- activeStep.value = 2;
- governTaskStarted.value = true;
- progress.value = value;
- // shouldRunGovernAfterClean.value = true;
- startGovernAfterClean();
- if (value >= 100) {
- markGovernComplete();
- }
- } else {
- // activeStep.value = 0;
- // progress.value = value;
- }
- if (payload.message) {
- sseMessages.value.unshift(payload.message);
- sseMessages.value = sseMessages.value.slice(0, 20);
- }
- persistImportProgress();
- return;
- }
- // if (isGovernCompleteText(normalized)) {
- // sseMessages.value.unshift(normalized);
- // sseMessages.value = sseMessages.value.slice(0, 20);
- // markGovernComplete();
- // return;
- // }
- // const percentMatch = normalized.match(/(\d{1,3})%/);
- // if (percentMatch) {
- // const value = Math.min(100, Math.max(0, Number(percentMatch[1])));
- // progress.value = value;
- // if (value >= 90) {
- // activeStep.value = 2;
- // } else if (value >= 50) {
- // activeStep.value = 1;
- // } else {
- // activeStep.value = 0;
- // }
- // return;
- // }
- // if (normalized.includes('读取')) {
- // activeStep.value = 0;
- // } else if (normalized.includes('清洗')) {
- // activeStep.value = 1;
- // } else if (normalized.includes('治理')) {
- // activeStep.value = 2;
- // }
- };
- const connectSse = () => {
- const activeEventSource = getImportProgressEventSource();
- if (activeEventSource) {
- eventSource = activeEventSource;
- return;
- }
- sseConnected.value = false;
- sseError.value = null;
- eventSource = new EventSource(getGovernTaskSseUrl());
- setImportProgressEventSource(eventSource);
- eventSource.onopen = () => {
- sseConnected.value = true;
- sseError.value = null;
- };
- eventSource.onmessage = (event) => {
- updateProgressFromMessage(event.data);
- };
- eventSource.onerror = () => {
- sseConnected.value = false;
- if (!governTaskFinished.value) {
- sseError.value = 'SSE 连接中断';
- }
- eventSource?.close();
- if (getImportProgressEventSource() === eventSource) {
- setImportProgressEventSource(null);
- }
- eventSource = null;
- };
- };
- const loadCleanFiles = () => {
- const storageKeys = [IMPORT_FILE_INFO_STORAGE_KEY, FILE_INFO_STORAGE_KEY];
- for (const key of storageKeys) {
- const raw = sessionStorage.getItem(key);
- if (!raw) continue;
- try {
- const parsed = JSON.parse(raw) as FileInfo[];
- if (Array.isArray(parsed) && parsed.length) {
- return parsed;
- }
- } catch (error) {
- // Ignore invalid session payload and continue reading fallback storage.
- }
- }
- return [];
- };
- const loadReCleanContext = (): { reClean?: 0 | 1; fileId?: string } | null => {
- const raw = sessionStorage.getItem(RECLEAN_CONTEXT_STORAGE_KEY);
- if (!raw) return null;
- try {
- const parsed = JSON.parse(raw) as { reClean?: number; fileId?: string | number };
- const reClean = Number(parsed?.reClean) === 1 ? 1 : 0;
- const fileId = normalizeText(parsed?.fileId);
- if (reClean !== 1 || !fileId) return null;
- return { reClean: 1, fileId };
- } catch {
- return null;
- }
- };
- const pickFirstFileName = (files: FileInfo[]) => {
- const first = files[0];
- if (!first) return '暂无文件';
- return first.fileName || first.sheetName || first.filePath || '暂无文件';
- };
- const startClean = async () => {
- if (cleanTaskStarted.value) return;
- const fileInfos = loadCleanFiles();
- if (!fileInfos.length) {
- createMessage.warning('未找到可清洗文件,请先上传并匹配文件');
- return;
- }
- currentCleanFiles.value = fileInfos;
- currentCleanBatchId.value = findBatchId(fileInfos);
- currentFileName.value = pickFirstFileName(fileInfos);
- cleanTaskStarted.value = true;
- persistImportProgress();
- try {
- const reCleanContext = loadReCleanContext();
- await doClean(
- reCleanContext
- ? {
- fileInfos,
- reClean: 1,
- fileId: reCleanContext.fileId,
- }
- : { fileInfos },
- );
- sessionStorage.removeItem(RECLEAN_CONTEXT_STORAGE_KEY);
- // applyCleanResult(result);
- // if (isCleanFinishedResult(result)) {
- // await handleCleanFinished();
- // }
- createMessage.success('清洗任务已启动');
- } catch (error) {
- cleanTaskStarted.value = false;
- clearImportProgress(getCurrentCaseId());
- createMessage.error('清洗任务启动失败');
- }
- };
- const initializeProgress = async () => {
- if (restoreImportProgress()) {
- eventSource = getImportProgressEventSource();
- if (!eventSource) {
- connectSse();
- await testSse();
- }
- return;
- }
- resetImportProgressRuntimeState();
- await loadGovernSwitch();
- connectSse();
- await testSse();
- await startClean();
- };
- onMounted(() => {
- void initializeProgress();
- });
- onBeforeUnmount(() => {
- if (isComplete.value) {
- closeSse();
- } else {
- persistImportProgress();
- }
- });
- </script>
- <style lang="less">
- @prefix-cls: ~'jeesite-case-clean-progress';
- .@{prefix-cls}.case-clean-progress {
- min-height: 100%;
- height: 100%;
- padding: 12px 16px 24px;
- background: #fff;
- display: flex;
- flex-direction: column;
- gap: 12px;
- box-sizing: border-box;
- overflow: auto;
- .page-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 16px;
- }
- .page-title-wrap {
- display: flex;
- align-items: flex-start;
- min-width: 0;
- }
- .page-title {
- font-size: 18px;
- line-height: 1.4;
- font-weight: 600;
- color: #111827;
- }
- .page-subtitle {
- margin-top: 4px;
- font-size: 13px;
- line-height: 1.6;
- color: #6b7280;
- }
- .progress-layout {
- flex: 1 1 auto;
- min-height: 0;
- display: flex;
- }
- .progress-card {
- border: 1px solid #e5e7eb;
- border-radius: 14px;
- background: #fff;
- box-shadow: 0 10px 28px rgba(15, 23, 42, 0.04);
- min-height: 0;
- width: 100%;
- }
- .progress-card {
- display: flex;
- flex-direction: column;
- gap: 20px;
- padding: 20px;
- }
- .progress-card__header {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 16px;
- }
- .panel-title {
- font-size: 16px;
- font-weight: 600;
- color: #111827;
- }
- .panel-desc {
- margin-top: 4px;
- font-size: 12px;
- line-height: 1.6;
- color: #6b7280;
- }
- .progress-text {
- font-size: 28px;
- line-height: 1;
- font-weight: 600;
- color: #2563eb;
- }
- .progress-ring-section {
- display: grid;
- grid-template-columns: 220px minmax(0, 1fr);
- gap: 24px;
- align-items: center;
- }
- .progress-ring {
- position: relative;
- width: 180px;
- height: 180px;
- margin: 0 auto;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .progress-ring svg {
- width: 180px;
- height: 180px;
- transform: rotate(-90deg);
- }
- .ring-bg {
- fill: none;
- stroke: #e5e7eb;
- stroke-width: 12;
- }
- .ring-value {
- fill: none;
- stroke: url(#progressGradient);
- stroke-width: 12;
- stroke-linecap: round;
- transition:
- stroke-dasharray 0.4s ease,
- stroke-dashoffset 0.4s ease;
- filter: drop-shadow(0 10px 18px rgba(37, 99, 235, 0.16));
- }
- .progress-ring.complete .ring-value {
- stroke: url(#progressComplete);
- filter: drop-shadow(0 10px 18px rgba(22, 163, 74, 0.18));
- }
- .progress-value {
- position: absolute;
- inset: 28px;
- border-radius: 50%;
- background: #f8fafc;
- border: 1px solid #eef2f7;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 4px;
- color: #111827;
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8);
- strong {
- font-size: 28px;
- line-height: 1;
- }
- span {
- font-size: 12px;
- color: #6b7280;
- }
- }
- .progress-summary {
- display: flex;
- flex-direction: column;
- gap: 8px;
- padding: 18px;
- border-radius: 12px;
- background: #fafafa;
- border: 1px solid #eef2f7;
- }
- .progress-summary.is-govern {
- background: #f8fbff;
- border-color: #dbeafe;
- }
- .summary-title {
- font-size: 15px;
- font-weight: 600;
- color: #111827;
- }
- .summary-desc,
- .summary-file {
- font-size: 13px;
- line-height: 1.7;
- color: #6b7280;
- }
- .step-track {
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 16px 18px;
- border-radius: 12px;
- background: #fafafa;
- border: 1px solid #eef2f7;
- }
- .step {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 8px;
- color: #9ca3af;
- min-width: 88px;
- }
- .step-icon {
- position: relative;
- width: 44px;
- height: 44px;
- border-radius: 50%;
- border: 1px solid #d1d5db;
- background: #fff;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .step-dot {
- width: 10px;
- height: 10px;
- border-radius: 50%;
- background: #cbd5e1;
- }
- .step-title {
- font-size: 12px;
- font-weight: 500;
- }
- .step.active {
- color: #2563eb;
- }
- .step.active .step-icon {
- border-color: #60a5fa;
- box-shadow: 0 8px 20px rgba(37, 99, 235, 0.14);
- animation: step-active-float 1.8s ease-in-out infinite;
- }
- .step.active .step-icon::before {
- position: absolute;
- inset: -6px;
- border: 1px solid rgba(37, 99, 235, 0.26);
- border-radius: 50%;
- content: '';
- animation: step-active-pulse 1.6s ease-out infinite;
- }
- .step.active .step-dot {
- background: #2563eb;
- box-shadow: 0 0 0 5px rgba(37, 99, 235, 0.12);
- animation: step-dot-pulse 1.05s ease-in-out infinite;
- }
- .step.active .step-title {
- font-weight: 700;
- }
- .step.done {
- color: #15803d;
- }
- .step.done .step-icon {
- border-color: #34d399;
- }
- .step.done .step-dot {
- background: #16a34a;
- }
- .step-line {
- flex: 1 1 auto;
- height: 1px;
- background: #dbe3ef;
- }
- @keyframes step-active-float {
- 0%,
- 100% {
- transform: translateY(0);
- }
- 50% {
- transform: translateY(-3px);
- }
- }
- @keyframes step-active-pulse {
- 0% {
- opacity: 0.72;
- transform: scale(0.88);
- }
- 100% {
- opacity: 0;
- transform: scale(1.22);
- }
- }
- @keyframes step-dot-pulse {
- 0%,
- 100% {
- transform: scale(1);
- }
- 50% {
- transform: scale(1.18);
- }
- }
- .status-grid {
- display: grid;
- grid-template-columns: repeat(3, minmax(0, 1fr));
- gap: 12px;
- }
- .status-card {
- padding: 14px 16px;
- border-radius: 12px;
- background: #fafafa;
- border: 1px solid #eef2f7;
- }
- .status-label {
- font-size: 12px;
- color: #6b7280;
- }
- .status-value {
- margin-top: 6px;
- font-size: 14px;
- font-weight: 600;
- line-height: 1.6;
- color: #111827;
- word-break: break-all;
- }
- .progress-complete {
- min-height: 100%;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 16px;
- text-align: center;
- padding: 28px 20px;
- }
- .complete-icon {
- width: 72px;
- height: 72px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- background: #ecfdf3;
- border: 1px solid #bbf7d0;
- color: #16a34a;
- font-size: 34px;
- font-weight: 700;
- }
- .complete-title {
- font-size: 22px;
- font-weight: 600;
- color: #111827;
- }
- .complete-desc {
- max-width: 360px;
- font-size: 13px;
- line-height: 1.7;
- color: #6b7280;
- }
- .complete-stats {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 12px;
- width: 100%;
- max-width: 360px;
- }
- .stat-row {
- padding: 14px 16px;
- border-radius: 12px;
- background: #fafafa;
- border: 1px solid #eef2f7;
- display: flex;
- flex-direction: column;
- gap: 6px;
- text-align: left;
- }
- .stat-label {
- font-size: 12px;
- color: #6b7280;
- }
- .stat-value {
- font-size: 18px;
- font-weight: 600;
- color: #111827;
- }
- .complete-actions {
- display: flex;
- align-items: center;
- gap: 12px;
- margin-top: 8px;
- }
- .complete-log,
- .complete-result {
- min-width: 110px;
- height: 36px;
- border-radius: 8px;
- }
- @media (max-width: 768px) {
- .page-header {
- flex-direction: column;
- align-items: flex-start;
- }
- .progress-ring-section,
- .status-grid,
- .complete-stats {
- grid-template-columns: 1fr;
- }
- .step-track {
- flex-direction: column;
- }
- .step-line {
- width: 1px;
- min-height: 24px;
- }
- .complete-actions {
- width: 100%;
- flex-direction: column;
- }
- .complete-log,
- .complete-result {
- width: 100%;
- }
- }
- }
- </style>
|