Explorar o código

feat(case): 清洗流程全屏页补齐公共顶栏并优化上传页头

- 新增 CaseFlowTopBar:上传 / 清洗 / 清洗进度共用顶栏,含品牌、案件、模式切换、模板库、下载管理、账号与窗口控制
- 三个全屏路由接入顶栏,内容区改为 page-body 撑满剩余高度
- 上传页页头改为白色卡片,返回收为图标按钮,标题与说明精简,操作按钮统一 32px
- 模板库弹窗查询区去掉渐变底,查询 / 重置按钮高度调整为 32px
cc hai 3 días
pai
achega
b0267680c8

+ 139 - 123
ai-electron/frontend/src/case/views/data/cleanProgress.vue

@@ -1,142 +1,147 @@
 <template>
   <div :class="prefixCls" class="case-clean-progress">
-    <div class="page-header">
-      <div class="page-title-wrap">
-        <div class="page-title">数据清洗导入</div>
-        <div class="page-subtitle">实时展示文件清洗、入库与治理的执行进度,便于快速确认任务运行状态。</div>
+    <!-- 清洗流程公共顶栏:品牌 / 案件 / 模板库 / 下载管理 / 窗口控制 -->
+    <CaseFlowTopBar />
+
+    <div class="page-body">
+      <div class="page-header">
+        <div class="page-title-wrap">
+          <div class="page-title">数据清洗导入</div>
+          <div class="page-subtitle">实时展示文件清洗、入库与治理的执行进度,便于快速确认任务运行状态。</div>
+        </div>
+        <div v-if="!isComplete" class="sse-pill" :class="{ 'is-on': sseConnected }">
+          <span class="sse-dot"></span>
+          {{ sseConnected ? '实时推送已连接' : '实时推送未连接' }}
+        </div>
       </div>
-      <div v-if="!isComplete" class="sse-pill" :class="{ 'is-on': sseConnected }">
-        <span class="sse-dot"></span>
-        {{ sseConnected ? '实时推送已连接' : '实时推送未连接' }}
-      </div>
-    </div>
 
-    <div class="progress-layout" :class="{ 'is-fused': isFused }">
-      <div class="progress-card">
-        <template v-if="!isComplete">
-          <div class="progress-card__header">
-            <div class="panel-heading">
-              <span class="live-dot"></span>
-              <div class="panel-heading-text">
-                <div class="panel-title">任务执行中</div>
-                <div class="panel-desc">{{ progressStatusText }}</div>
-              </div>
-            </div>
-            <div class="progress-text">{{ overallProgress }}<small>%</small></div>
-          </div>
-
-          <div class="step-track">
-            <div class="step" :class="{ active: activeStep === 0, done: activeStep > 0 }">
-              <div class="step-icon">
-                <span v-if="activeStep > 0" class="step-check">✓</span>
-                <span v-else class="step-num">1</span>
+      <div class="progress-layout" :class="{ 'is-fused': isFused }">
+        <div class="progress-card">
+          <template v-if="!isComplete">
+            <div class="progress-card__header">
+              <div class="panel-heading">
+                <span class="live-dot"></span>
+                <div class="panel-heading-text">
+                  <div class="panel-title">任务执行中</div>
+                  <div class="panel-desc">{{ progressStatusText }}</div>
+                </div>
               </div>
-              <div class="step-title">读取数据</div>
+              <div class="progress-text">{{ overallProgress }}<small>%</small></div>
             </div>
-            <div class="step-line" :class="{ done: activeStep > 0 }"></div>
-            <div class="step" :class="{ active: activeStep === 1, done: activeStep > 1 }">
-              <div class="step-icon">
-                <span v-if="activeStep > 1" class="step-check">✓</span>
-                <span v-else class="step-num">2</span>
+
+            <div class="step-track">
+              <div class="step" :class="{ active: activeStep === 0, done: activeStep > 0 }">
+                <div class="step-icon">
+                  <span v-if="activeStep > 0" class="step-check">✓</span>
+                  <span v-else class="step-num">1</span>
+                </div>
+                <div class="step-title">读取数据</div>
               </div>
-              <div class="step-title">清洗入库</div>
-            </div>
-            <div class="step-line" :class="{ done: activeStep > 1 }"></div>
-            <div class="step" :class="{ active: activeStep === 2 }">
-              <div class="step-icon">
-                <span class="step-num">3</span>
+              <div class="step-line" :class="{ done: activeStep > 0 }"></div>
+              <div class="step" :class="{ active: activeStep === 1, done: activeStep > 1 }">
+                <div class="step-icon">
+                  <span v-if="activeStep > 1" class="step-check">✓</span>
+                  <span v-else class="step-num">2</span>
+                </div>
+                <div class="step-title">清洗入库</div>
               </div>
-              <div class="step-title">数据治理</div>
-            </div>
-          </div>
-
-          <div class="progress-hero">
-            <div class="progress-ring">
-              <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>
-                </defs>
-                <circle cx="60" cy="60" r="50" class="ring-bg" />
-                <circle
-                  cx="60"
-                  cy="60"
-                  r="50"
-                  class="ring-value"
-                  :style="{
-                    strokeDasharray: `${RING_CIRCUMFERENCE}`,
-                    strokeDashoffset: `${RING_CIRCUMFERENCE - progressDash}`,
-                  }"
-                />
-              </svg>
-              <div class="progress-value">
-                <strong>{{ overallProgress }}<small>%</small></strong>
-                <span>{{ currentStepLabel }}</span>
+              <div class="step-line" :class="{ done: activeStep > 1 }"></div>
+              <div class="step" :class="{ active: activeStep === 2 }">
+                <div class="step-icon">
+                  <span class="step-num">3</span>
+                </div>
+                <div class="step-title">数据治理</div>
               </div>
             </div>
 
-            <div class="progress-log" :class="{ 'is-govern': activeStep >= 2 }">
-              <div class="progress-log__header">
-                <span class="progress-log__title">处理日志</span>
-                <span class="progress-log__count">共 {{ taskLog.length }} 条</span>
+            <div class="progress-hero">
+              <div class="progress-ring">
+                <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>
+                  </defs>
+                  <circle cx="60" cy="60" r="50" class="ring-bg" />
+                  <circle
+                    cx="60"
+                    cy="60"
+                    r="50"
+                    class="ring-value"
+                    :style="{
+                      strokeDasharray: `${RING_CIRCUMFERENCE}`,
+                      strokeDashoffset: `${RING_CIRCUMFERENCE - progressDash}`,
+                    }"
+                  />
+                </svg>
+                <div class="progress-value">
+                  <strong>{{ overallProgress }}<small>%</small></strong>
+                  <span>{{ currentStepLabel }}</span>
+                </div>
               </div>
-              <div class="progress-log__list">
-                <div v-if="!taskLog.length" class="progress-log__empty">等待任务推送,处理记录将实时写入这里</div>
-                <div v-for="(item, index) in taskLog" :key="`${item.time}-${index}`" class="progress-log__row">
-                  <span class="progress-log__dot" :class="{ 'is-current': index === 0 }"></span>
-                  <span class="progress-log__text">{{ item.text }}</span>
-                  <span class="progress-log__time">{{ item.time }}</span>
+
+              <div class="progress-log" :class="{ 'is-govern': activeStep >= 2 }">
+                <div class="progress-log__header">
+                  <span class="progress-log__title">处理日志</span>
+                  <span class="progress-log__count">共 {{ taskLog.length }} 条</span>
+                </div>
+                <div class="progress-log__list">
+                  <div v-if="!taskLog.length" class="progress-log__empty">等待任务推送,处理记录将实时写入这里</div>
+                  <div v-for="(item, index) in taskLog" :key="`${item.time}-${index}`" class="progress-log__row">
+                    <span class="progress-log__dot" :class="{ 'is-current': index === 0 }"></span>
+                    <span class="progress-log__text">{{ item.text }}</span>
+                    <span class="progress-log__time">{{ item.time }}</span>
+                  </div>
                 </div>
               </div>
             </div>
-          </div>
-
-          <!-- 融合模式的出口:SSE 推送中断(后端重启/网络断开),进度无法继续更新 -->
-          <div v-if="aiStageTimeout && !isComplete" class="ai-timeout-banner">
-            <span class="ai-timeout-text">
-              AI
-              清洗进度推送已中断(连接断开或后端已重启)。稍后可在「数据治理」查看已入库结果,或改用手动清洗处理这批文件。
-            </span>
-            <Button type="link" size="small" @click="goManualClean">改为手动清洗</Button>
-          </div>
-        </template>
-
-        <template v-else>
-          <div class="progress-complete">
-            <div class="complete-icon">✓</div>
-            <div class="complete-title">清洗导入完成</div>
-            <div class="complete-desc">{{ completeDesc }}</div>
-            <div class="complete-stats">
-              <div class="stat-item">
-                <span class="stat-value is-success">{{ displaySuccessCount }}</span>
-                <span class="stat-label">导入成功(条)</span>
-              </div>
-              <div class="stat-divider"></div>
-              <div class="stat-item">
-                <!-- 失败数固定用红色(与成功数的绿色对应),0 也保持红色,避免完成态一眼看不出哪一项是失败口径 -->
-                <span class="stat-value is-fail">{{ displayFailCount }}</span>
-                <span class="stat-label">导入失败(条)</span>
-              </div>
-              <!-- AI 无法清洗的表:同属失败语义,单独明示(此前只藏在处理日志里) -->
-              <template v-if="unableCount > 0">
+
+            <!-- 融合模式的出口:SSE 推送中断(后端重启/网络断开),进度无法继续更新 -->
+            <div v-if="aiStageTimeout && !isComplete" class="ai-timeout-banner">
+              <span class="ai-timeout-text">
+                AI
+                清洗进度推送已中断(连接断开或后端已重启)。稍后可在「数据治理」查看已入库结果,或改用手动清洗处理这批文件。
+              </span>
+              <Button type="link" size="small" @click="goManualClean">改为手动清洗</Button>
+            </div>
+          </template>
+
+          <template v-else>
+            <div class="progress-complete">
+              <div class="complete-icon">✓</div>
+              <div class="complete-title">清洗导入完成</div>
+              <div class="complete-desc">{{ completeDesc }}</div>
+              <div class="complete-stats">
+                <div class="stat-item">
+                  <span class="stat-value is-success">{{ displaySuccessCount }}</span>
+                  <span class="stat-label">导入成功(条)</span>
+                </div>
                 <div class="stat-divider"></div>
                 <div class="stat-item">
-                  <span class="stat-value is-fail">{{ unableCount }}</span>
-                  <span class="stat-label">未清洗(表)</span>
+                  <!-- 失败数固定用红色(与成功数的绿色对应),0 也保持红色,避免完成态一眼看不出哪一项是失败口径 -->
+                  <span class="stat-value is-fail">{{ displayFailCount }}</span>
+                  <span class="stat-label">导入失败(条)</span>
                 </div>
-              </template>
-            </div>
-            <div class="complete-actions">
-              <Button class="complete-log" @click="handleViewLog">查看清洗日志</Button>
-              <Button v-if="isSmartCleanEntry" type="primary" class="complete-result" @click="handleStartAnalysis">
-                开始分析
-              </Button>
-              <Button v-else type="primary" class="complete-result" @click="handleViewResult">查看结果</Button>
+                <!-- AI 无法清洗的表:同属失败语义,单独明示(此前只藏在处理日志里) -->
+                <template v-if="unableCount > 0">
+                  <div class="stat-divider"></div>
+                  <div class="stat-item">
+                    <span class="stat-value is-fail">{{ unableCount }}</span>
+                    <span class="stat-label">未清洗(表)</span>
+                  </div>
+                </template>
+              </div>
+              <div class="complete-actions">
+                <Button class="complete-log" @click="handleViewLog">查看清洗日志</Button>
+                <Button v-if="isSmartCleanEntry" type="primary" class="complete-result" @click="handleStartAnalysis">
+                  开始分析
+                </Button>
+                <Button v-else type="primary" class="complete-result" @click="handleViewResult">查看结果</Button>
+              </div>
             </div>
-          </div>
-        </template>
+          </template>
+        </div>
       </div>
     </div>
 
@@ -164,6 +169,7 @@
     testSse,
   } from '@/case/api/govern/governApi';
   import CleanLogModal from './components/CleanLogModal.vue';
+  import CaseFlowTopBar from './components/CaseFlowTopBar.vue';
   import type { SmartCleanReviewItem, SmartCleanStatusVO } from '@/case/api/govern/types/dto';
   import type { FileInfo } from '@/types/api';
   import {
@@ -1235,13 +1241,23 @@
     // height: 100% 会退化成 auto(页面塌成内容高度),必须用 100vh 撑满视口。
     min-height: 100vh;
     height: 100vh;
-    padding: 16px 24px 20px;
     background: #f0f2f5;
     display: flex;
     flex-direction: column;
-    gap: 14px;
     box-sizing: border-box;
-    overflow: auto;
+    overflow: hidden;
+
+    // 公共顶栏由 AppTopBar 自带高度,内容区在剩余高度里滚动
+    .page-body {
+      flex: 1 1 auto;
+      min-height: 0;
+      padding: 16px 24px 20px;
+      display: flex;
+      flex-direction: column;
+      gap: 14px;
+      box-sizing: border-box;
+      overflow: auto;
+    }
 
     .page-header {
       display: flex;

+ 82 - 76
ai-electron/frontend/src/case/views/data/cleaning.vue

@@ -1,99 +1,104 @@
 <template>
   <div :class="prefixCls" class="case-data-cleaning-page">
-    <div class="page-header">
-      <div class="page-header-main">
-        <div class="page-title-area">
-          <Button type="text" class="back-btn" @click="handleBack">←</Button>
-          <div class="page-title-block">
-            <div class="page-title">案件数据清洗</div>
+    <!-- 清洗流程公共顶栏:品牌 / 案件 / 模板库 / 下载管理 / 窗口控制 -->
+    <CaseFlowTopBar />
+
+    <div class="page-body">
+      <div class="page-header">
+        <div class="page-header-main">
+          <div class="page-title-area">
+            <Button type="text" class="back-btn" @click="handleBack">←</Button>
+            <div class="page-title-block">
+              <div class="page-title">案件数据清洗</div>
+            </div>
+          </div>
+          <div class="page-header-actions">
+            <Button @click="toggleLeft">{{ leftCollapsed ? '展开左侧' : '收起左侧' }}</Button>
+            <Button type="primary" :loading="importingData" @click="handleImportData">导入数据</Button>
           </div>
-        </div>
-        <div class="page-header-actions">
-          <Button @click="toggleLeft">{{ leftCollapsed ? '展开左侧' : '收起左侧' }}</Button>
-          <Button type="primary" :loading="importingData" @click="handleImportData">导入数据</Button>
         </div>
       </div>
-    </div>
 
-    <div class="main-layout" :style="{ gridTemplateColumns: layoutTemplateColumns }">
-      <FileTreePanel />
-
-      <div v-show="!leftCollapsed" class="panel-resizer" @mousedown="startResize('left', $event)"></div>
-
-      <section class="middle-panel">
-        <div class="template-match-bar" :class="{ 'is-empty': !hasTemplateSelected }">
-          <div class="template-match-main">
-            <span class="template-match-icon"></span>
-            <div class="template-match-copy">
-              <div class="template-match-label">匹配模板</div>
-              <div class="template-match-name">
-                {{ currentTemplateDisplayName }}
-                <span v-if="!hasTemplateSelected" class="template-match-badge is-pending">待配置</span>
-                <span v-else-if="templateDetailFromMock" class="template-match-badge">示例</span>
+      <div class="main-layout" :style="{ gridTemplateColumns: layoutTemplateColumns }">
+        <FileTreePanel />
+
+        <div v-show="!leftCollapsed" class="panel-resizer" @mousedown="startResize('left', $event)"></div>
+
+        <section class="middle-panel">
+          <div class="template-match-bar" :class="{ 'is-empty': !hasTemplateSelected }">
+            <div class="template-match-main">
+              <span class="template-match-icon"></span>
+              <div class="template-match-copy">
+                <div class="template-match-label">匹配模板</div>
+                <div class="template-match-name">
+                  {{ currentTemplateDisplayName }}
+                  <span v-if="!hasTemplateSelected" class="template-match-badge is-pending">待配置</span>
+                  <span v-else-if="templateDetailFromMock" class="template-match-badge">示例</span>
+                </div>
               </div>
             </div>
+            <Button type="link" class="template-select-trigger" @click="openTemplateModal">选择模板</Button>
           </div>
-          <Button type="link" class="template-select-trigger" @click="openTemplateModal">选择模板</Button>
-        </div>
 
-        <template v-if="hasTemplateSelected">
-          <div class="middle-panel-body">
-            <FieldMappingCard />
+          <template v-if="hasTemplateSelected">
+            <div class="middle-panel-body">
+              <FieldMappingCard />
 
-            <div
-              class="template-foot-panel"
-              :class="{
-                'is-collapsed': showRecognitionPanel && !recognitionPanelExpanded,
-                'is-compact': !showRecognitionPanel,
-              }"
-            >
-              <RecognitionRulePanel />
+              <div
+                class="template-foot-panel"
+                :class="{
+                  'is-collapsed': showRecognitionPanel && !recognitionPanelExpanded,
+                  'is-compact': !showRecognitionPanel,
+                }"
+              >
+                <RecognitionRulePanel />
 
-              <ChildTemplatePanel />
+                <ChildTemplatePanel />
+              </div>
             </div>
-          </div>
-        </template>
+          </template>
 
-        <MiddleEmptyState v-else />
-      </section>
+          <MiddleEmptyState v-else />
+        </section>
 
-      <div class="panel-resizer" @mousedown="startResize('middle', $event)"></div>
+        <div class="panel-resizer" @mousedown="startResize('middle', $event)"></div>
 
-      <section class="right-panel">
-        <div class="panel-header">
-          <div class="panel-title-wrap">
-            <div class="panel-title">数据清洗区</div>
-          </div>
-        </div>
-        <div class="toolbar toolbar--column-actions">
-          <div class="toolbar-head">
-            <span class="toolbar-label">列操作</span>
-          </div>
-          <div class="toolbar-actions">
-            <Button size="small" type="primary" @click="openAddColumn">新增列</Button>
-            <Button size="small" @click="openSplitColumn">拆分列</Button>
-            <Button size="small" @click="openEditColumn">编辑列</Button>
-            <Button size="small" @click="openDeleteColumn">删除列</Button>
+        <section class="right-panel">
+          <div class="panel-header">
+            <div class="panel-title-wrap">
+              <div class="panel-title">数据清洗区</div>
+            </div>
           </div>
-          <div class="toolbar-meta toolbar-meta--header-row">
-            <span class="meta-label">表头行</span>
-            <Input
-              v-model:value="headerRowInput"
-              size="small"
-              class="meta-input"
-              placeholder="输入行号"
-              @pressEnter="applyHeaderRowInput"
-              @blur="applyHeaderRowInput"
-            />
+          <div class="toolbar toolbar--column-actions">
+            <div class="toolbar-head">
+              <span class="toolbar-label">列操作</span>
+            </div>
+            <div class="toolbar-actions">
+              <Button size="small" type="primary" @click="openAddColumn">新增列</Button>
+              <Button size="small" @click="openSplitColumn">拆分列</Button>
+              <Button size="small" @click="openEditColumn">编辑列</Button>
+              <Button size="small" @click="openDeleteColumn">删除列</Button>
+            </div>
+            <div class="toolbar-meta toolbar-meta--header-row">
+              <span class="meta-label">表头行</span>
+              <Input
+                v-model:value="headerRowInput"
+                size="small"
+                class="meta-input"
+                placeholder="输入行号"
+                @pressEnter="applyHeaderRowInput"
+                @blur="applyHeaderRowInput"
+              />
+            </div>
           </div>
-        </div>
 
-        <div class="right-panel-body">
-          <DataPreviewTable />
+          <div class="right-panel-body">
+            <DataPreviewTable />
 
-          <CleaningRulePanel />
-        </div>
-      </section>
+            <CleaningRulePanel />
+          </div>
+        </section>
+      </div>
     </div>
 
     <AddColumnModal />
@@ -170,6 +175,7 @@
   import FileTreePanel from './cleaning/components/FileTreePanel.vue';
   import DataPreviewTable from './cleaning/components/DataPreviewTable.vue';
   import { useCaseDataCleaningActions } from './cleaning/composables/useCaseDataCleaningActions';
+  import CaseFlowTopBar from './components/CaseFlowTopBar.vue';
 
   const { prefixCls } = useDesign('case-data-cleaning');
   const go = useGo();

+ 14 - 4
ai-electron/frontend/src/case/views/data/cleaning/styles/page.less

@@ -10,18 +10,28 @@
 .@{prefix-cls}.case-data-cleaning-page {
   min-height: 0;
   // 独立全屏路由(不套 LAYOUT):直接占满视口。
-  // 这里不能再用 calc(100vh - 100px),那个 100px 是布局 header + tabs 的高度,
-  // 脱离 LAYOUT 后页面顶部没有这两条,扣掉高度会平白少一截。
+  // 顶栏由页面自己渲染(公共 AppTopBar),内容区在剩余高度里排布,
+  // 不再用 calc(100vh - 100px) 去扣布局 header + tabs。
   height: 100vh;
   max-height: 100vh;
   background: #f0f2f5;
-  padding: 12px 16px 16px;
   box-sizing: border-box;
   display: flex;
   flex-direction: column;
-  gap: 12px;
   overflow: hidden;
 
+  // 公共顶栏由 AppTopBar 自带高度,内容区在剩余高度里排布
+  .page-body {
+    flex: 1 1 auto;
+    min-height: 0;
+    padding: 12px 16px 16px;
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+    box-sizing: border-box;
+    overflow: hidden;
+  }
+
   .page-header {
     display: flex;
     flex-direction: column;

+ 95 - 0
ai-electron/frontend/src/case/views/data/components/CaseFlowTopBar.vue

@@ -0,0 +1,95 @@
+<template>
+  <AppTopBar class="case-flow-topbar" :case-name="caseName">
+    <template #actions>
+      <!-- 模板库:与主界面顶栏一致,点开就地弹出,不打断当前上传 / 清洗流程 -->
+      <Tooltip title="模板库" placement="bottom" :mouseEnterDelay="0.5">
+        <Button type="text" class="case-flow-topbar-action" aria-label="模板库" @click="templateLibraryVisible = true">
+          <Icon icon="i-ant-design:book-outlined" />
+        </Button>
+      </Tooltip>
+
+      <!-- 下载管理:下载记录、保存位置与清空 / 刷新等操作,按主界面顶栏同款入口提供 -->
+      <AppDownloadManager />
+    </template>
+  </AppTopBar>
+
+  <CaseTemplateLibraryModal v-model:open="templateLibraryVisible" />
+</template>
+
+<script lang="ts" setup name="CaseFlowTopBar">
+  /**
+   * 清洗流程三段(文件上传 / 案件数据清洗 / 清洗导入进度)共用的顶栏。
+   *
+   * 这三页是不套 LAYOUT 的全屏路由,公共顶栏只能由页面自己渲染;
+   * 除品牌、案件、模式切换、账号与窗口控制外,这里补齐主界面顶栏的
+   * 「模板库」和「下载管理」两个入口,避免进入清洗流程后功能断档。
+   */
+  import { computed, ref } from 'vue';
+  import { Button, Tooltip } from 'ant-design-vue';
+  import { AppDownloadManager, AppTopBar } from '@/core/components/Application';
+  import { Icon } from '@/core/components/Icon';
+  import { useUserStore } from '@/core/store/modules/case';
+  import CaseTemplateLibraryModal from './CaseTemplateLibraryModal.vue';
+
+  const userStore = useUserStore();
+  const templateLibraryVisible = ref(false);
+  /** 顶栏案件胶囊:全屏流程里也保留当前案件上下文 */
+  const caseName = computed(() => String(userStore.getCaseInfo?.name || '').trim());
+</script>
+
+<style lang="less">
+  .case-flow-topbar {
+    /*
+     * 顶栏图标按钮:与主壳顶栏同规格(30×28,圆角 8)。
+     * 主壳那份规则在 layouts/default/header/index.less,独立全屏路由下不会加载,
+     * 这里自带一份,只作用于本组件,不影响传统壳原有外观。
+     */
+    .case-flow-topbar-action.ant-btn,
+    .jeesite-app-download-manager-trigger {
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+      width: 30px;
+      min-width: 30px;
+      height: 28px;
+      padding: 0;
+      color: inherit;
+      background: transparent;
+      border: 1px solid transparent;
+      border-radius: 8px;
+      box-shadow: none;
+      cursor: pointer;
+      transition:
+        background-color 0.2s ease,
+        color 0.2s ease;
+
+      .jeesite-icon,
+      .anticon {
+        font-size: 18px;
+      }
+
+      &:hover,
+      &:focus,
+      &:focus-visible {
+        color: inherit;
+        background-color: @header-light-bg-hover-color;
+        border-color: transparent;
+        outline: none;
+        box-shadow: none;
+      }
+    }
+  }
+
+  html[data-theme='dark'] {
+    .case-flow-topbar {
+      .case-flow-topbar-action.ant-btn,
+      .jeesite-app-download-manager-trigger {
+        &:hover,
+        &:focus,
+        &:focus-visible {
+          background-color: @header-dark-bg-hover-color;
+        }
+      }
+    }
+  }
+</style>

+ 11 - 0
ai-electron/frontend/src/case/views/data/components/CaseTemplateLibraryModal.vue

@@ -1867,6 +1867,17 @@
 
 <style lang="less">
   .case-template-library-modal {
+    /* 查询区:去掉默认的上下渐变,保持与弹窗内容一致的纯白底 */
+    .query-table-search {
+      background: #fff;
+    }
+
+    /* 查询区的「查询 / 重置」按钮:高度与输入框、右侧「新增模板」统一为 32px */
+    .query-table-search .search-actions .ant-btn {
+      height: 32px;
+      border-radius: 16px;
+    }
+
     .query-table-content {
       .ant-table-cell-fix-right {
         background: #fff;

+ 243 - 174
ai-electron/frontend/src/case/views/data/upload.vue

@@ -1,172 +1,178 @@
 <template>
   <div :class="prefixCls" class="case-upload-page">
-    <div class="page-header">
-      <div class="page-header__left">
-        <Button type="link" class="back-btn" @click="handleBack">
-          <template #icon><LeftOutlined /></template>
-          返回
-        </Button>
-        <div>
-          <div class="page-title">文件上传</div>
-          <div class="page-subtitle">
-            上传文件;xls / xlsx / csv 会自动解析,其余格式仅登记为「不支持解析」。 「智能」会同时跑既有模板清洗与 AI
-            识别:未匹配到模板的文件交给 AI 判定,判不出来的标为待人工确认。
+    <!-- 清洗流程公共顶栏:品牌 / 案件 / 模板库 / 下载管理 / 窗口控制 -->
+    <CaseFlowTopBar />
+
+    <div class="page-body">
+      <div class="page-header">
+        <div class="page-header__left">
+          <Tooltip title="返回" placement="bottom" :mouseEnterDelay="0.5">
+            <button type="button" class="back-btn" aria-label="返回" @click="handleBack">
+              <LeftOutlined />
+            </button>
+          </Tooltip>
+          <span class="page-header__divider" aria-hidden="true"></span>
+          <div class="page-title-block">
+            <div class="page-title">文件上传</div>
+            <div class="page-subtitle">
+              支持 xls / xlsx / csv 自动解析;智能清洗结合模板与 AI 识别,未能匹配的文件待人工确认。
+            </div>
           </div>
         </div>
+        <div class="page-header__actions">
+          <Button
+            type="primary"
+            class="next-btn"
+            :loading="smartCleaning"
+            :disabled="!hasCleanableSheet"
+            @click="handleSmartClean"
+          >
+            智能清洗
+          </Button>
+          <Button class="next-btn" :disabled="!hasCleanableSheet" @click="handleManualClean">手动清洗</Button>
+        </div>
       </div>
-      <div class="page-header__actions">
-        <Button
-          type="primary"
-          class="next-btn"
-          :loading="smartCleaning"
-          :disabled="!hasCleanableSheet"
-          @click="handleSmartClean"
-        >
-          智能
-        </Button>
-        <Button class="next-btn" :disabled="!hasCleanableSheet" @click="handleManualClean">手动清洗</Button>
-      </div>
-    </div>
 
-    <div class="upload-layout">
-      <aside class="left-panel">
-        <div class="upload-picker">
-          <div class="upload-mode-switch" role="tablist" aria-label="上传类型切换">
-            <button
-              type="button"
-              :class="['upload-mode-switch__item', { 'is-active': uploadMode === 'single' }]"
-              :disabled="isUploading"
-              @click="handleUploadModeChange('single')"
-            >
-              选择文件
-            </button>
-            <button
-              type="button"
-              :class="['upload-mode-switch__item', { 'is-active': uploadMode === 'folder' }]"
-              :disabled="isUploading"
-              @click="handleUploadModeChange('folder')"
-            >
-              选择文件夹
-            </button>
+      <div class="upload-layout">
+        <aside class="left-panel">
+          <div class="upload-picker">
+            <div class="upload-mode-switch" role="tablist" aria-label="上传类型切换">
+              <button
+                type="button"
+                :class="['upload-mode-switch__item', { 'is-active': uploadMode === 'single' }]"
+                :disabled="isUploading"
+                @click="handleUploadModeChange('single')"
+              >
+                选择文件
+              </button>
+              <button
+                type="button"
+                :class="['upload-mode-switch__item', { 'is-active': uploadMode === 'folder' }]"
+                :disabled="isUploading"
+                @click="handleUploadModeChange('folder')"
+              >
+                选择文件夹
+              </button>
+            </div>
           </div>
-        </div>
 
-        <!-- 隐藏的文件输入 -->
-        <input ref="fileInputRef" type="file" multiple style="display: none" @change="handleFileInputChange" />
-        <input
-          ref="folderInputRef"
-          type="file"
-          webkitdirectory
-          directory
-          style="display: none"
-          @change="handleFolderInputChange"
-        />
-
-        <div
-          class="upload-dropzone"
-          :class="{ 'is-dragover': dragOver }"
-          @dragover.prevent="handleDragOver"
-          @dragleave="handleDragLeave"
-          @drop.prevent="handleDrop"
-        >
-          <div class="upload-dragger-content">
-            <div class="upload-dragger-icon">⬆</div>
-            <div class="upload-dragger-title">
-              {{ uploadMode === 'single' ? '拖拽文件到这里,或点击选择文件' : '拖拽文件夹到这里,或点击选择文件夹' }}
-            </div>
-            <div class="upload-dragger-desc">
-              {{
-                uploadMode === 'single'
-                  ? '支持批量选择任意文件;xls / xlsx / csv 会自动解析,其余格式仅登记为「不支持解析」'
-                  : '支持选择整个文件夹,系统会自动读取符合条件的文件'
-              }}
+          <!-- 隐藏的文件输入 -->
+          <input ref="fileInputRef" type="file" multiple style="display: none" @change="handleFileInputChange" />
+          <input
+            ref="folderInputRef"
+            type="file"
+            webkitdirectory
+            directory
+            style="display: none"
+            @change="handleFolderInputChange"
+          />
+
+          <div
+            class="upload-dropzone"
+            :class="{ 'is-dragover': dragOver }"
+            @dragover.prevent="handleDragOver"
+            @dragleave="handleDragLeave"
+            @drop.prevent="handleDrop"
+          >
+            <div class="upload-dragger-content">
+              <div class="upload-dragger-icon">⬆</div>
+              <div class="upload-dragger-title">
+                {{ uploadMode === 'single' ? '拖拽文件到这里,或点击选择文件' : '拖拽文件夹到这里,或点击选择文件夹' }}
+              </div>
+              <div class="upload-dragger-desc">
+                {{
+                  uploadMode === 'single'
+                    ? '支持批量选择任意文件;xls / xlsx / csv 会自动解析,其余格式仅登记为「不支持解析」'
+                    : '支持选择整个文件夹,系统会自动读取符合条件的文件'
+                }}
+              </div>
+              <Button type="primary" class="upload-action-btn" :disabled="isUploading" @click="handleTriggerFileSelect">
+                {{ uploadMode === 'single' ? '选择文件' : '选择文件夹' }}
+              </Button>
             </div>
-            <Button type="primary" class="upload-action-btn" :disabled="isUploading" @click="handleTriggerFileSelect">
-              {{ uploadMode === 'single' ? '选择文件' : '选择文件夹' }}
-            </Button>
           </div>
-        </div>
 
-        <!-- 已选文件列表(加入后自动上传,成功即移除) -->
-        <div v-if="selectedFiles.length" class="upload-file-list">
-          <div class="upload-file-list__header">
-            <span>待上传 {{ selectedFiles.length }} 个文件</span>
-            <Button type="link" size="small" :disabled="isUploading" @click="clearSelectedFiles"> 清空 </Button>
-          </div>
-          <div class="upload-file-list__body">
-            <div
-              v-for="file in selectedFiles"
-              :key="file.name + file.size + file.lastModified"
-              class="upload-file-item"
-            >
-              <div class="upload-file-item__info">
-                <span class="upload-file-item__name">{{ file.name }}</span>
-                <span class="upload-file-item__size">{{ formatFileSize(file.size) }}</span>
+          <!-- 已选文件列表(加入后自动上传,成功即移除) -->
+          <div v-if="selectedFiles.length" class="upload-file-list">
+            <div class="upload-file-list__header">
+              <span>待上传 {{ selectedFiles.length }} 个文件</span>
+              <Button type="link" size="small" :disabled="isUploading" @click="clearSelectedFiles"> 清空 </Button>
+            </div>
+            <div class="upload-file-list__body">
+              <div
+                v-for="file in selectedFiles"
+                :key="file.name + file.size + file.lastModified"
+                class="upload-file-item"
+              >
+                <div class="upload-file-item__info">
+                  <span class="upload-file-item__name">{{ file.name }}</span>
+                  <span class="upload-file-item__size">{{ formatFileSize(file.size) }}</span>
+                </div>
+                <div class="upload-file-item__status">
+                  <template v-if="uploadStatuses.get(file) === 'error'">
+                    <span class="upload-file-item__error">✗</span>
+                  </template>
+                  <template v-else-if="uploadStatuses.get(file) === 'uploading'">
+                    <span class="upload-file-item__progress-text">{{ uploadProgressMap.get(file) || 0 }}%</span>
+                  </template>
+                  <template v-else>
+                    <span class="upload-file-item__pending">待上传</span>
+                  </template>
+                </div>
+                <button
+                  v-if="!isUploading && (uploadStatuses.get(file) || 'pending') === 'pending'"
+                  type="button"
+                  class="upload-file-item__remove"
+                  aria-label="移除文件"
+                  @click="removeSelectedFile(file)"
+                >
+                  <CloseOutlined />
+                </button>
               </div>
-              <div class="upload-file-item__status">
-                <template v-if="uploadStatuses.get(file) === 'error'">
-                  <span class="upload-file-item__error">✗</span>
-                </template>
-                <template v-else-if="uploadStatuses.get(file) === 'uploading'">
-                  <span class="upload-file-item__progress-text">{{ uploadProgressMap.get(file) || 0 }}%</span>
-                </template>
-                <template v-else>
-                  <span class="upload-file-item__pending">待上传</span>
-                </template>
+            </div>
+            <!-- 整体进度 -->
+            <div v-if="isUploading" class="upload-overall-progress">
+              <div class="upload-overall-progress__bar">
+                <div class="upload-overall-progress__fill" :style="{ width: overallProgress + '%' }"></div>
               </div>
-              <button
-                v-if="!isUploading && (uploadStatuses.get(file) || 'pending') === 'pending'"
-                type="button"
-                class="upload-file-item__remove"
-                aria-label="移除文件"
-                @click="removeSelectedFile(file)"
-              >
-                <CloseOutlined />
-              </button>
+              <span class="upload-overall-progress__text">总进度 {{ overallProgress }}%</span>
             </div>
           </div>
-          <!-- 整体进度 -->
-          <div v-if="isUploading" class="upload-overall-progress">
-            <div class="upload-overall-progress__bar">
-              <div class="upload-overall-progress__fill" :style="{ width: overallProgress + '%' }"></div>
-            </div>
-            <span class="upload-overall-progress__text">总进度 {{ overallProgress }}%</span>
+          <div v-else class="upload-file-list upload-file-list--empty">
+            <div class="upload-empty">暂无待上传文件,选择文件后将自动上传解析</div>
           </div>
-        </div>
-        <div v-else class="upload-file-list upload-file-list--empty">
-          <div class="upload-empty">暂无待上传文件,选择文件后将自动上传解析</div>
-        </div>
-      </aside>
+        </aside>
 
-      <section class="right-panel">
-        <div class="panel-header">
-          <div>
-            <div class="panel-title">解析结果(实时)</div>
-            <div class="panel-desc">每完成一份文件解析,结果树会实时追加到下方</div>
+        <section class="right-panel">
+          <div class="panel-header">
+            <div>
+              <div class="panel-title">解析结果(实时)</div>
+              <div class="panel-desc">每完成一份文件解析,结果树会实时追加到下方</div>
+            </div>
+            <div class="panel-header__actions">
+              <Button size="small" :disabled="!expandableCount" @click="toggleExpandAllRows">
+                {{ allExpanded ? '全部收起' : '全部展开' }}
+              </Button>
+            </div>
+            <div class="panel-stats">已解析 {{ resultTrees.length }} 个文件</div>
           </div>
-          <div class="panel-header__actions">
-            <Button size="small" :disabled="!expandableCount" @click="toggleExpandAllRows">
-              {{ allExpanded ? '全部收起' : '全部展开' }}
-            </Button>
+          <div class="table-wrapper">
+            <Table
+              class="result-tree-table"
+              :columns="resultTableColumns"
+              :data-source="tableDataSource"
+              :loading="tableLoading"
+              :pagination="false"
+              row-key="id"
+              :children-column-name="'children'"
+              :expanded-row-keys="expandedRowKeys"
+              size="small"
+              :scroll="{ x: 840 }"
+              @expandedRowsChange="onExpandedRowsChange"
+            />
           </div>
-          <div class="panel-stats">已解析 {{ resultTrees.length }} 个文件</div>
-        </div>
-        <div class="table-wrapper">
-          <Table
-            class="result-tree-table"
-            :columns="resultTableColumns"
-            :data-source="tableDataSource"
-            :loading="tableLoading"
-            :pagination="false"
-            row-key="id"
-            :children-column-name="'children'"
-            :expanded-row-keys="expandedRowKeys"
-            size="small"
-            :scroll="{ x: 840 }"
-            @expandedRowsChange="onExpandedRowsChange"
-          />
-        </div>
-      </section>
+        </section>
+      </div>
     </div>
   </div>
 </template>
@@ -175,7 +181,7 @@
   import { computed, h, onMounted, ref } from 'vue';
   import { onBeforeRouteLeave } from 'vue-router';
   import { CloseOutlined, LeftOutlined } from '@ant-design/icons-vue';
-  import { Button, Modal, Table, type TableColumnsType } from 'ant-design-vue';
+  import { Button, Modal, Table, Tooltip, type TableColumnsType } from 'ant-design-vue';
   import { useDesign } from '@/core/hooks/web/useDesign';
   import { useGo, useQuery } from '@/core/hooks/web/usePage';
   import { PageEnum } from '@/core/enums/pageEnum';
@@ -183,6 +189,7 @@
   import { useEmitter } from '@/core/store/modules/case';
   import { clearUploadBatch, deletedFileList, preFileUpload, smartClean } from '@/case/api/govern/governApi';
   import { FileStatusEnum } from '@/case/types/enum';
+  import CaseFlowTopBar from './components/CaseFlowTopBar.vue';
   import { resetImportProgressSession, setCleanFlowSource } from './importProgressSession';
   import type { CleanFlowSource } from './importProgressSession';
   import type { FileInfo } from '@/types/api';
@@ -911,64 +918,101 @@
   .@{prefix-cls}.case-upload-page {
     width: 100%;
     height: 100vh;
-    padding: 8px 12px;
     background: #f0f2f5;
     display: flex;
     flex-direction: column;
-    gap: 8px;
     box-sizing: border-box;
     overflow: hidden;
 
+    // 公共顶栏由 AppTopBar 自带高度,内容区在剩余高度里滚动
+    .page-body {
+      flex: 1 1 auto;
+      min-height: 0;
+      padding: 8px 12px;
+      display: flex;
+      flex-direction: column;
+      gap: 8px;
+      box-sizing: border-box;
+      overflow: hidden;
+    }
+
     .page-header {
       flex-shrink: 0;
       display: flex;
       align-items: center;
       justify-content: space-between;
-      gap: 12px;
-      padding: 2px 2px 0;
+      gap: 16px;
+      padding: 10px 14px;
+      border: 1px solid #e2e8f0;
+      border-radius: 10px;
+      background: #fff;
+      box-shadow: 0 1px 2px rgba(15, 23, 42, 0.03);
     }
 
     .page-header__left {
       display: flex;
       align-items: center;
-      gap: 10px;
+      gap: 12px;
       min-width: 0;
     }
 
+    .page-header__divider {
+      flex: 0 0 auto;
+      width: 1px;
+      height: 22px;
+      background: #e2e8f0;
+    }
+
     .back-btn {
       display: inline-flex;
       align-items: center;
-      gap: 4px;
-      padding: 0 4px 0 0;
-      height: 30px;
-      border-radius: 6px;
-      color: #2563eb;
+      justify-content: center;
+      flex: 0 0 auto;
+      width: 32px;
+      height: 32px;
+      padding: 0;
+      border: 1px solid #e2e8f0;
+      border-radius: 8px;
+      background: #f8fafc;
+      color: #475569;
       font-size: 14px;
-      font-weight: 600;
       line-height: 1;
-      flex-shrink: 0;
+      cursor: pointer;
+      transition:
+        color 0.2s ease,
+        border-color 0.2s ease,
+        background-color 0.2s ease;
 
-      :deep(.ant-btn-icon) {
-        font-size: 15px;
+      &:hover,
+      &:focus,
+      &:focus-visible {
+        color: #2563eb;
+        border-color: #bfdbfe;
+        background: #eff6ff;
+        outline: none;
       }
+    }
 
-      &:hover {
-        color: #1d4ed8;
-      }
+    .page-title-block {
+      min-width: 0;
+      overflow: hidden;
     }
 
     .page-title {
-      font-size: 15px;
-      line-height: 1.3;
+      font-size: 16px;
+      line-height: 1.4;
       font-weight: 600;
       color: #111827;
     }
 
     .page-subtitle {
-      margin-top: 2px;
+      margin-top: 1px;
+      overflow: hidden;
       font-size: 12px;
-      line-height: 1.5;
-      color: #6b7280;
+      line-height: 1.6;
+      color: #64748b;
+      white-space: nowrap;
+      text-overflow: ellipsis;
     }
 
     .page-header__actions {
@@ -980,8 +1024,9 @@
 
     .next-btn {
       height: 32px;
-      padding: 0 14px;
+      padding: 0 16px;
       border-radius: 8px;
+      font-weight: 500;
     }
 
     .upload-layout {
@@ -1423,6 +1468,30 @@
     .@{prefix-cls}.case-upload-page {
       background: #0f172a;
 
+      .page-header {
+        border-color: #1f2937;
+        background: #111827;
+        box-shadow: none;
+      }
+
+      .page-header__divider {
+        background: #1f2937;
+      }
+
+      .back-btn {
+        border-color: #334155;
+        background: #0f172a;
+        color: #cbd5e1;
+
+        &:hover,
+        &:focus,
+        &:focus-visible {
+          border-color: #3b82f6;
+          background: #1e293b;
+          color: #93c5fd;
+        }
+      }
+
       .page-title,
       .panel-title,
       .upload-dragger-title,