Bladeren bron

feat(case): 清洗任务日志只保留页面内存,移除历史入口

- 导入文件页移除「任务日志」按钮与抽屉组件
- 进度页不再落 localStorage,页面退出即清空日志
- 进入进度页时清理旧版本遗留的 case_clean_task_log
- 刷新页面不再恢复历史日志;进度与统计快照保留
cc 4 dagen geleden
bovenliggende
commit
5688dbde8e

+ 9 - 94
ai-electron/frontend/src/case/views/data/cleanProgress.vue

@@ -216,17 +216,15 @@
   import type { FileInfo } from '@/types/api';
   import {
     clearImportProgress,
+    clearLegacyTaskLogStorage,
     getActiveImportProgress,
     getCleanFlowSource,
     getImportProgressEventSource,
-    getTaskLogSnapshot,
     resetImportProgressRuntimeState,
     saveImportProgress,
-    saveTaskLogSnapshot,
     setImportProgressEventSource,
     useImportProgressRuntimeState,
     type TaskLogItem,
-    type TaskLogStatus,
   } from './importProgressSession';
 
   const { prefixCls } = useDesign('case-clean-progress');
@@ -264,12 +262,10 @@
   } = useImportProgressRuntimeState();
   const sseMessages = ref<string[]>([]);
   /**
-   * 处理日志:清洗 + 治理统一成一条时间线。
-   * 实时写模块状态供页面渲染,同时按案件/批次落一份 localStorage 快照,
-   * 完成后结果页仍可回看(见 importProgressSession 的 TaskLogSnapshot)。
+   * 处理日志:清洗 + 治理统一成一条时间线,只存在本页内存里。
+   * 页面退出即清空;不落 localStorage,也不在导入文件页提供历史日志入口。
    */
   const taskLog = ref<TaskLogItem[]>([]);
-  const taskLogStatus = ref<TaskLogStatus>('running');
   const taskLogBatchId = ref('');
   const governFailed = ref(false);
   let taskLogSeq = 0;
@@ -458,7 +454,6 @@
     const batchId = currentCleanBatchId.value || refreshCurrentCleanBatchId();
     if (taskLogBatchId.value && batchId && batchId !== taskLogBatchId.value) {
       taskLog.value = [];
-      taskLogStatus.value = 'running';
       governFailed.value = false;
     }
     taskLogBatchId.value = batchId || taskLogBatchId.value;
@@ -472,7 +467,6 @@
     if (taskLog.value.length > 200) {
       taskLog.value.length = 200;
     }
-    persistTaskLog();
   };
 
   const findBatchId = (items?: FileInfo[] | null): string => {
@@ -507,76 +501,6 @@
     return currentCleanBatchId.value;
   };
 
-  /** 把当前日志序列落一份快照:完成后进度快照会清掉,但任务日志仍可在结果页回看 */
-  const persistTaskLog = (status: TaskLogStatus = taskLogStatus.value) => {
-    const caseId = getCurrentCaseId();
-    if (!caseId) return;
-
-    const batchId = taskLogBatchId.value || currentCleanBatchId.value || refreshCurrentCleanBatchId() || '';
-    const updatedAt = Date.now();
-    taskLogStatus.value = status;
-    saveTaskLogSnapshot({
-      caseId: String(caseId),
-      batchId,
-      status,
-      logs: taskLog.value,
-      successCount: successCount.value,
-      failCount: failCount.value,
-      updatedAt,
-    });
-  };
-
-  /** 刷新恢复执行中的页面时,把同一批次已产生的日志接回来 */
-  const restoreTaskLog = () => {
-    const snapshot = getTaskLogSnapshot(getCurrentCaseId());
-    if (!snapshot) return;
-
-    const currentBatch = currentCleanBatchId.value || refreshCurrentCleanBatchId();
-    if (currentBatch && snapshot.batchId && snapshot.batchId !== currentBatch) return;
-
-    taskLog.value = snapshot.logs.slice();
-    taskLogBatchId.value = snapshot.batchId;
-    taskLogStatus.value = snapshot.status;
-    if (snapshot.status === 'failed') {
-      governFailed.value = true;
-    }
-  };
-
-  /**
-   * 完成 / 失败后刷新进度页:进度快照已在完成时清掉,但任务日志快照还在。
-   * 同一批次的终态直接恢复成结果视图,避免刷新后又重新跑一遍清洗。
-   */
-  const restoreCompletedTaskLog = () => {
-    const snapshot = getTaskLogSnapshot(getCurrentCaseId());
-    if (!snapshot || (snapshot.status !== 'completed' && snapshot.status !== 'failed')) return false;
-
-    const currentBatch = currentCleanBatchId.value || refreshCurrentCleanBatchId();
-    if (currentBatch && snapshot.batchId && snapshot.batchId !== currentBatch) return false;
-
-    taskLog.value = snapshot.logs.slice();
-    taskLogBatchId.value = snapshot.batchId;
-    taskLogStatus.value = snapshot.status;
-    currentCleanBatchId.value = snapshot.batchId || currentCleanBatchId.value;
-    successCount.value = snapshot.successCount ?? successCount.value;
-    failCount.value = snapshot.failCount ?? failCount.value;
-    cleanStatsSnapshot.value = {
-      ...cleanStatsSnapshot.value,
-      success: snapshot.successCount,
-      fail: snapshot.failCount,
-    };
-
-    const hasGovernLogs = snapshot.logs.some((item) => item.text.includes('[数据治理]'));
-    shouldRunGovernAfterClean.value = hasGovernLogs;
-    governTaskStarted.value = hasGovernLogs;
-    governTaskFinished.value = hasGovernLogs && snapshot.status === 'completed';
-    governFailed.value = snapshot.status === 'failed';
-    cleanTaskStarted.value = true;
-    cleanTaskFinished.value = true;
-    activeStep.value = 2;
-    progress.value = 100;
-    return true;
-  };
-
   const handleViewLog = () => {
     refreshCurrentCleanBatchId();
     logModalOpen.value = true;
@@ -781,7 +705,6 @@
     activeStep.value = 2;
     governTaskFinished.value = true;
     closeSse();
-    persistTaskLog('completed');
     clearImportProgress(getCurrentCaseId());
     safeEmitImportRefresh();
     if (!governCompleteNotified.value) {
@@ -823,7 +746,6 @@
       governFailed.value = true;
       progress.value = 100;
       addTaskLog(`[数据治理]${message}`);
-      persistTaskLog('failed');
       closeSse();
       createMessage.error(message);
     }
@@ -886,9 +808,7 @@
         const message = String(payload.message || '').trim();
         if (/治理失败|治理异常/.test(message)) {
           governFailed.value = true;
-          taskLogStatus.value = 'failed';
           progress.value = 100;
-          persistTaskLog('failed');
           closeSse();
           createMessage.error(message || '数据治理失败,请查看任务日志');
           return;
@@ -1220,7 +1140,6 @@
    */
   watch(isComplete, (done) => {
     if (!done) return;
-    persistTaskLog(governFailed.value ? 'failed' : 'completed');
     const unable = isFused.value ? (aiStage.value?.needReviewCount ?? 0) : 0;
     if (unable > 0) {
       createMessage.warning(`另有 ${unable} 个表 AI 无法清洗,详情见处理日志;可用「手动清洗」处理。`);
@@ -1406,15 +1325,13 @@
   };
 
   const initializeProgress = async () => {
-    // 每次进入都先清掉上一次留在组件里的任务日志,再按快照恢复
+    // 任务日志只存在本页内存:进入时清空,并清掉旧版本遗留的 localStorage 快照
     taskLog.value = [];
     taskLogBatchId.value = '';
-    taskLogStatus.value = 'running';
     governFailed.value = false;
+    clearLegacyTaskLogStorage();
 
     if (restoreImportProgress()) {
-      // 刷新恢复:把同一批次已产生的清洗/治理日志接回来,避免页面空白
-      restoreTaskLog();
       // 刷新恢复:智能入口仍强制自动治理,避免旧快照把治理开关带回配置值
       if (isSmartFlowEntry()) {
         shouldRunGovernAfterClean.value = true;
@@ -1430,12 +1347,6 @@
       return;
     }
 
-    // 完成/失败后刷新:进度快照已清,但任务日志快照还在,直接恢复结果视图
-    if (restoreCompletedTaskLog()) {
-      closeSse();
-      return;
-    }
-
     resetImportProgressRuntimeState();
 
     await loadGovernSwitch();
@@ -1456,6 +1367,10 @@
 
   onBeforeUnmount(() => {
     clearCompletionFallbackTimer();
+    // 清洗/治理日志不跨页面保留:退出即清空内存,并清掉旧版本遗留的 localStorage
+    taskLog.value = [];
+    taskLogBatchId.value = '';
+    clearLegacyTaskLogStorage();
     if (isComplete.value) {
       closeSse();
     } else {

+ 0 - 194
ai-electron/frontend/src/case/views/data/components/TaskLogDrawer.vue

@@ -1,194 +0,0 @@
-<template>
-  <Drawer
-    v-model:open="open"
-    placement="right"
-    :width="680"
-    :body-style="{ padding: '16px 20px 20px' }"
-    wrap-class-name="case-task-log-drawer"
-  >
-    <template #title>
-      <div class="task-log-drawer__title">
-        <span class="task-log-drawer__title-text">任务日志</span>
-        <span class="task-log-drawer__status" :class="`is-${displayStatus}`">{{ statusText }}</span>
-      </div>
-    </template>
-
-    <div class="task-log-drawer">
-      <div class="task-log-drawer__meta">
-        <span class="task-log-drawer__meta-item">
-          <span class="label">批次</span>
-          <span class="value">{{ batchId || '-' }}</span>
-        </span>
-        <span class="task-log-drawer__meta-item">
-          <span class="label">更新于</span>
-          <span class="value">{{ updatedText }}</span>
-        </span>
-      </div>
-
-      <TaskLogList
-        :logs="logs"
-        title="最近一次任务"
-        :max-height="'calc(100vh - 224px)'"
-        empty-text="还没有任务日志,完成一次清洗后会在这里显示"
-      />
-    </div>
-  </Drawer>
-</template>
-
-<script lang="ts" setup name="TaskLogDrawer">
-  /**
-   * 结果页的任务日志抽屉。
-   *
-   * <p>数据来自 importProgressSession 落盘的任务日志快照(清洗 + 治理同一条时间线),
-   * 即使已经离开进度页,仍可回看最近一次任务的完整执行记录。
-   */
-  import { computed } from 'vue';
-  import { Drawer } from 'ant-design-vue';
-  import TaskLogList from './TaskLogList.vue';
-  import type { TaskLogItem, TaskLogStatus } from '../importProgressSession';
-
-  const open = defineModel<boolean>('open', { default: false });
-
-  const props = withDefaults(
-    defineProps<{
-      logs?: TaskLogItem[];
-      status?: TaskLogStatus | '';
-      batchId?: string | number;
-      updatedAt?: number | null;
-    }>(),
-    {
-      logs: () => [],
-      status: '',
-      batchId: '',
-      updatedAt: null,
-    },
-  );
-
-  const displayStatus = computed(() => {
-    if (props.status) return props.status;
-    return props.logs.length ? 'completed' : 'empty';
-  });
-
-  const statusText = computed(() => {
-    switch (displayStatus.value) {
-      case 'running':
-        return '进行中';
-      case 'completed':
-        return '已完成';
-      case 'failed':
-        return '存在失败';
-      default:
-        return '暂无记录';
-    }
-  });
-
-  const updatedText = computed(() => {
-    if (!props.updatedAt) return '-';
-    return new Date(props.updatedAt).toLocaleString('zh-CN', { hour12: false });
-  });
-</script>
-
-<style lang="less">
-  .task-log-drawer {
-    display: flex;
-    flex-direction: column;
-    gap: 14px;
-    min-height: 0;
-    height: 100%;
-
-    &__title {
-      display: flex;
-      align-items: center;
-      gap: 10px;
-    }
-
-    &__title-text {
-      font-size: 16px;
-      font-weight: 600;
-      color: #0f172a;
-    }
-
-    &__status {
-      display: inline-flex;
-      align-items: center;
-      height: 22px;
-      padding: 0 9px;
-      border-radius: 999px;
-      font-size: 11.5px;
-      font-weight: 600;
-      line-height: 22px;
-
-      &.is-running {
-        color: #1d4ed8;
-        background: #eff6ff;
-      }
-
-      &.is-completed {
-        color: #15803d;
-        background: #ecfdf5;
-      }
-
-      &.is-failed {
-        color: #b91c1c;
-        background: #fef2f2;
-      }
-
-      &.is-empty {
-        color: #64748b;
-        background: #f1f5f9;
-      }
-    }
-
-    &__meta {
-      display: flex;
-      align-items: center;
-      gap: 22px;
-      flex-wrap: wrap;
-      padding: 10px 12px;
-      border: 1px solid #eef2f7;
-      border-radius: 10px;
-      background: #f8fafc;
-    }
-
-    &__meta-item {
-      display: inline-flex;
-      align-items: center;
-      gap: 7px;
-      min-width: 0;
-      font-size: 12px;
-
-      .label {
-        flex: none;
-        color: #94a3b8;
-      }
-
-      .value {
-        max-width: 320px;
-        overflow: hidden;
-        color: #334155;
-        font-weight: 500;
-        text-overflow: ellipsis;
-        white-space: nowrap;
-      }
-    }
-  }
-
-  html[data-theme='dark'] {
-    .task-log-drawer__title-text {
-      color: #e5e7eb;
-    }
-
-    .task-log-drawer__meta {
-      border-color: #1f2937;
-      background: #111827;
-    }
-
-    .task-log-drawer__meta-item .label {
-      color: #94a3b8;
-    }
-
-    .task-log-drawer__meta-item .value {
-      color: #cbd5e1;
-    }
-  }
-</style>

+ 3 - 2
ai-electron/frontend/src/case/views/data/components/TaskLogList.vue

@@ -24,8 +24,9 @@
   /**
    * 清洗 / 治理任务日志时间线。
    *
-   * <p>三处复用:进度页执行中面板、完成态统计页右侧、结果页日志抽屉。
-   * 日志直接显示后端原文(含 [数据清洗] / [数据治理] 前缀),不做级别判断或阶段筛选。
+   * <p>两处复用:进度页执行中面板、完成态统计页右侧。日志只存在于当前进度页内存,
+   * 退出即清空,不落 localStorage。日志直接显示后端原文(含 [数据清洗] / [数据治理] 前缀),
+   * 不做级别判断或阶段筛选。
    */
   import { computed } from 'vue';
   import type { TaskLogItem } from '../importProgressSession';

+ 3 - 37
ai-electron/frontend/src/case/views/data/importList.vue

@@ -8,11 +8,6 @@
             <span class="table-meta-chip is-selected">已选 {{ selectedRowKeysRef.length }} 条</span>
           </div>
           <div class="table-toolbar__actions">
-            <a-button class="task-log-btn" type="default" @click="openTaskLog">
-              <Icon icon="i-ant-design:file-text-outlined" />
-              任务日志
-              <span v-if="taskLogCount" class="task-log-btn__count">{{ taskLogCount }}</span>
-            </a-button>
             <Popconfirm title="是否确认删除选中的导入记录?" @confirm="handleDeleteSelected()">
               <a-button v-if="selectedRowKeysRef.length > 0" danger class="batch-delete-btn" type="default">
                 <Icon icon="i-ant-design:delete-outlined" color="error" /> 删除 ({{ selectedRowKeysRef.length }})
@@ -49,18 +44,10 @@
       :success-num="Number(aiRejectRecord?.successNum ?? 0)"
       :fail-num="Number(aiRejectRecord?.failNum ?? 0)"
     />
-
-    <TaskLogDrawer
-      v-model:open="taskLogDrawerOpen"
-      :logs="taskLogSnapshot?.logs || []"
-      :status="taskLogSnapshot?.status || ''"
-      :batch-id="taskLogSnapshot?.batchId || ''"
-      :updated-at="taskLogSnapshot?.updatedAt || null"
-    />
   </div>
 </template>
 <script lang="ts" setup name="ViewsSysEmpUserList">
-  import { computed, h, onBeforeUnmount, onMounted, ref, watch } from 'vue';
+  import { h, onBeforeUnmount, onMounted, ref, watch } from 'vue';
   import type { PropType } from 'vue';
   import { Popconfirm } from 'ant-design-vue';
   import AnalysisPagination from '@/core/components/AnalysisPagination/src/AnalysisPagination.vue';
@@ -69,7 +56,7 @@
   import { useDesign } from '@/core/hooks/web/useDesign';
   import { useGo } from '@/core/hooks/web/usePage';
   import { useMessage } from '@/core/hooks/web/useMessage';
-  import { useEmitter, useUserStore } from '@/core/store/modules/case';
+  import { useEmitter } from '@/core/store/modules/case';
   import { PageEnum } from '@/core/enums/pageEnum';
   import type { FormProps } from '@/core/components/Form';
   import type { FileInfo } from '@/types';
@@ -79,13 +66,7 @@
   import componentSetting from '@/core/settings/componentSetting';
   import CleanLogModal from './components/CleanLogModal.vue';
   import AiRejectModal from './components/AiRejectModal.vue';
-  import TaskLogDrawer from './components/TaskLogDrawer.vue';
-  import {
-    getTaskLogSnapshot,
-    resetImportProgressSession,
-    setCleanFlowSource,
-    type TaskLogSnapshot,
-  } from './importProgressSession';
+  import { resetImportProgressSession, setCleanFlowSource } from './importProgressSession';
 
   const props = defineProps({
     treeCodes: Array as PropType<string[]>,
@@ -99,7 +80,6 @@
   const { prefixCls } = useDesign('case-import-list');
   const go = useGo();
   const emitter = useEmitter();
-  const userStore = useUserStore();
   const { createMessage } = useMessage();
   const CASE_DATA_REFRESH_EVENT = 'case-data:refresh';
   const CASE_IMPORT_FILE_REFRESH_EVENT = 'case-import-file:refresh';
@@ -115,17 +95,6 @@
   /** AI 清洗失败明细:按 sheet 最新 job 查 ai_clean_reject */
   const aiRejectOpen = ref(false);
   const aiRejectRecord = ref<Partial<FileInfo> | null>(null);
-  /** 最近一次清洗任务日志(进度页在任务结束时落盘,结果页只读) */
-  const taskLogDrawerOpen = ref(false);
-  const taskLogSnapshot = ref<TaskLogSnapshot | null>(null);
-  const taskLogCount = computed(() => taskLogSnapshot.value?.logs?.length ?? 0);
-  const refreshTaskLogSnapshot = () => {
-    taskLogSnapshot.value = getTaskLogSnapshot(userStore.getCaseInfo?.id);
-  };
-  const openTaskLog = () => {
-    refreshTaskLogSnapshot();
-    taskLogDrawerOpen.value = true;
-  };
   const hasLoadedOnActivate = ref(false);
   const recleaningFileIds = ref<string[]>([]);
   const paginationLoading = ref(false);
@@ -415,7 +384,6 @@
 
   const handleImportListRefresh = () => {
     hasLoadedOnActivate.value = true;
-    refreshTaskLogSnapshot();
     syncTableLayout();
     void reload();
   };
@@ -424,7 +392,6 @@
     () => props.active,
     (active) => {
       if (!active) return;
-      refreshTaskLogSnapshot();
       if (!hasLoadedOnActivate.value) {
         handleImportListRefresh();
         return;
@@ -436,7 +403,6 @@
   );
 
   onMounted(() => {
-    refreshTaskLogSnapshot();
     emitter.on(CASE_DATA_REFRESH_EVENT, handleImportListRefresh);
     emitter.on(CASE_IMPORT_FILE_REFRESH_EVENT, handleImportListRefresh);
     // 整页刷新直达 ?tab=files 时组件创建即为 active:watch 捕获不到「变化」,

+ 7 - 68
ai-electron/frontend/src/case/views/data/importProgressSession.ts

@@ -150,8 +150,8 @@ export const getCleanFlowSource = (): CleanFlowSource | null => {
  * 清洗/治理任务日志条目。
  *
  * <p>直接保存后端 SSE 的原始文案(保留 {@code [数据清洗]} / {@code [数据治理]} 前缀),
- * 不做级别判断或阶段归类。进度页实时展示,同时落一份 localStorage 快照;
- * 任务结束后结果页从快照里读出来,仍能查看完整日志。
+ * 不做级别判断或阶段归类。日志只存在于进度页内存,<b>不落任何存储</b>:
+ * 页面退出即清空,导入文件页也不再提供历史任务日志入口。
  */
 export interface TaskLogItem {
   id: string;
@@ -161,71 +161,10 @@ export interface TaskLogItem {
   text: string;
 }
 
-export type TaskLogStatus = 'running' | 'completed' | 'failed';
-
-export interface TaskLogSnapshot {
-  caseId: string;
-  batchId: string;
-  status: TaskLogStatus;
-  logs: TaskLogItem[];
-  /** 完成态统计:刷新完成页时不必依赖已清掉的进度快照 */
-  successCount?: number;
-  failCount?: number;
-  updatedAt: number;
-}
-
-const TASK_LOG_STORAGE_KEY = 'case_clean_task_log';
-const TASK_LOG_MAX_ITEMS = 200;
-/** 只保留最近几个案件的快照,避免 localStorage 里的历史案件无限增长 */
-const TASK_LOG_MAX_CASES = 5;
-
-const getTaskLogStorage = () => (typeof window === 'undefined' ? null : window.localStorage);
-
-const readTaskLogMap = (): Record<string, TaskLogSnapshot> => {
-  const storage = getTaskLogStorage();
-  if (!storage) return {};
-  try {
-    const parsed = JSON.parse(storage.getItem(TASK_LOG_STORAGE_KEY) || '{}');
-    return parsed && typeof parsed === 'object' ? (parsed as Record<string, TaskLogSnapshot>) : {};
-  } catch {
-    return {};
+/** 旧版本把任务日志落在 localStorage;进入进度页时清掉,避免历史日志残留 */
+export const clearLegacyTaskLogStorage = () => {
+  if (typeof window === 'undefined') {
+    return;
   }
-};
-
-export const saveTaskLogSnapshot = (snapshot: TaskLogSnapshot) => {
-  const storage = getTaskLogStorage();
-  const caseId = normalizeCaseId(snapshot.caseId);
-  if (!storage || !caseId) return;
-
-  const map = readTaskLogMap();
-  map[caseId] = {
-    ...snapshot,
-    caseId,
-    logs: (Array.isArray(snapshot.logs) ? snapshot.logs : []).slice(0, TASK_LOG_MAX_ITEMS),
-    updatedAt: snapshot.updatedAt || Date.now(),
-  };
-
-  const entries = Object.entries(map)
-    .sort(([, left], [, right]) => (right.updatedAt || 0) - (left.updatedAt || 0))
-    .slice(0, TASK_LOG_MAX_CASES);
-  storage.setItem(TASK_LOG_STORAGE_KEY, JSON.stringify(Object.fromEntries(entries)));
-};
-
-export const getTaskLogSnapshot = (caseId?: string | number): TaskLogSnapshot | null => {
-  const normalizedCaseId = normalizeCaseId(caseId);
-  if (!normalizedCaseId) return null;
-  const snapshot = readTaskLogMap()[normalizedCaseId];
-  if (!snapshot || !Array.isArray(snapshot.logs)) return null;
-  return snapshot;
-};
-
-export const clearTaskLogSnapshot = (caseId?: string | number) => {
-  const storage = getTaskLogStorage();
-  const normalizedCaseId = normalizeCaseId(caseId);
-  if (!storage || !normalizedCaseId) return;
-
-  const map = readTaskLogMap();
-  if (!(normalizedCaseId in map)) return;
-  delete map[normalizedCaseId];
-  storage.setItem(TASK_LOG_STORAGE_KEY, JSON.stringify(map));
+  window.localStorage.removeItem('case_clean_task_log');
 };