Browse Source

对齐蓝湖「基层减负统一风格」完成壳层与业务页视觉改造(P0–P5)。

透底侧栏+页签贴白卡;列表页 ListPageToolbar/固定列;展板多色 KPI;抽屉 append-to-body 与 z-index 分层;底图迁至 public/assets/bg。详见 README 更新记录(2026-10-23)。

Co-authored-by: Cursor <cursoragent@cursor.com>
wdq 1 week ago
parent
commit
c808f41d06
37 changed files with 3279 additions and 1560 deletions
  1. 1 0
      .gitignore
  2. 18 3
      README.md
  3. BIN
      public/assets/bg/background.png
  4. 2 2
      public/config/config.js
  5. 25 15
      src/api/sjnmtybt/realBiz.ts
  6. 62 62
      src/auto-imports.d.ts
  7. 10 1
      src/components.d.ts
  8. 13 16
      src/components/biz/BatchDetailDrawer.vue
  9. 10 1
      src/components/biz/PersonPickerDialog.vue
  10. 205 0
      src/components/common/ListPageToolbar.vue
  11. 23 0
      src/components/common/PagePanel.vue
  12. 45 44
      src/components/common/PieMixPanel.vue
  13. 15 2
      src/components/common/SensitiveValue.vue
  14. 35 0
      src/components/common/SplitPaneCard.vue
  15. 158 25
      src/components/common/StatCards.vue
  16. 11 1
      src/components/common/StatusTag.vue
  17. 2 18
      src/components/common/TablePagination.vue
  18. 47 73
      src/components/common/WorkbenchTipsPanel.vue
  19. 7 0
      src/composables/useServerPagination.ts
  20. 683 252
      src/layouts/MainLayout.vue
  21. 611 121
      src/styles/index.less
  22. 78 67
      src/styles/variables.less
  23. 1 1
      src/utils/appConfig.ts
  24. 55 39
      src/views/admin/AdminBatchesView.vue
  25. 66 54
      src/views/admin/PersonnelAdminView.vue
  26. 33 24
      src/views/admin/RegionsAdminView.vue
  27. 81 78
      src/views/alerts/AlertsView.vue
  28. 176 83
      src/views/approvals/ApprovalsView.vue
  29. 108 153
      src/views/base/BaseInfoView.vue
  30. 262 159
      src/views/dashboard/DashboardView.vue
  31. 50 28
      src/views/disk/DiskReturnView.vue
  32. 113 77
      src/views/insurance/InsuranceMatchView.vue
  33. 72 46
      src/views/monthly/MonthlyPaymentView.vue
  34. 58 9
      src/views/reports/ReportsView.vue
  35. 82 74
      src/views/special/SpecialBusinessView.vue
  36. 57 28
      src/views/status/StatusMaintenanceView.vue
  37. 4 4
      src/views/system/SystemAdminView.vue

+ 1 - 0
.gitignore

@@ -10,6 +10,7 @@ lerna-debug.log*
 node_modules
 dist
 dist-ssr
+tmp_lanhu_covers
 *.local
 
 # 不参与提交的目录

+ 18 - 3
README.md

@@ -41,6 +41,7 @@ npm run dev
 
 ```
 public/
+  assets/bg/background.png # 壳层氛围底图
   config/config.js         # 全局变量配置(含 useMock / apiBase,可部署后直接改)
   data/                    # 静态数据
   js/                      # 静态脚本
@@ -48,9 +49,23 @@ docs/
   api-integration.md       # 与 sj_nmtybt_server 联调说明
 src/
   api/                     # 业务接口(biz.ts + sjnmtybt 适配层)
+  components/common/       # 列表工具条、分页、KPI、左树右表等公共组件
   utils/                   # request / 防抖 / 节流 / appConfig
-  layouts/                 # 布局
+  layouts/                 # 布局(蓝湖壳:透底侧栏 + 页签贴白卡)
   router/                  # 路由
-  styles/                  # Less 全局样式与变量
-  views/                   # 页面(联调不改 UI)
+  styles/                  # Less 全局样式与变量(主色 #2f54eb)
+  views/                   # 业务页面
 ```
+
+## 更新记录
+
+### 2026-10-23 · 蓝湖「基层减负统一风格」视觉改造(P0–P5)
+
+按蓝湖统一风格完成壳层与业务页视觉收口(不改业务逻辑):
+
+- **壳层(P0)**:浅蓝氛围底(`public/assets/bg/background.png`)、透底侧栏(禁止整列蓝墙,仅选中项主蓝块)、顶栏 Logo+系统名、页签贴白内容卡;浏览器刷新回首页
+- **列表页风格 A(P1–P2)**:页名大号近黑;`ListPageToolbar`(关键词 / 查询 / 多条件 / 刷新 + 操作最多 3 个进「…」);状态分类 Tab;宽表状态与操作列右侧固定;分页右下对齐蓝湖
+- **数据展板风格 B(P3)**:与列表同白壳;多色渐变 KPI;顶筛发放周期;轻量告警条;图表/待办白卡网格
+- **组件与弹层(P4)**:弹窗/抽屉白头;图表主色对齐 `#2f54eb`;办理要点降噪;左树右表容器 `SplitPaneCard`;白卡左右约 30px 内边距
+- **走查收口(P5)**:区/镇/村等角色菜单与壳层抽检;页签与白卡贴合;窄屏侧栏收为图标栏;抽屉 `append-to-body` + 全站 z-index 分层,避免页签盖住侧栏抽屉
+- **资源整理**:删除未使用的 `public/assets/shell/` 调试图,底图迁至 `public/assets/bg/`

BIN
public/assets/bg/background.png


+ 2 - 2
public/config/config.js

@@ -14,8 +14,8 @@ window.AppConfig = {
   systemTitle: '农民土地退养补助金',
   // 侧栏副标题
   systemSubtitle: '发放管理平台',
-  // 请求超时(毫秒)
-  requestTimeout: 15000,
+  // 请求超时(毫秒);批次汇总类接口偏慢,默认 60s,个别重接口可在请求里再加长
+  requestTimeout: 60000,
   // 业务 API 前缀(部署后可改,无需重打包前端 JS):
   // - 2130 静态站:用 '/api'(该端口已反代 /api → 后端;勿用 /sjnmtybt/api,会被 SPA 回成 index.html)
   // - Tomcat 同域 /sjnmtybt:用 '/sjnmtybt/api'(须 Nginx 反代该路径到 WAR,不能 try_files 到前端)

+ 25 - 15
src/api/sjnmtybt/realBiz.ts

@@ -1853,27 +1853,37 @@ export async function fetchInsuranceBatchesPage(params?: {
       : 'PENDING_INSURANCE_MATCH,PENDING_STATUS_MAINTAIN'
   const townId = params?.townId || undefined
   const batchLevel = townId ? 'TOWN' : 'DISTRICT'
-  const res = await post<PageResult<Record<string, unknown>>>('/batches/page', {
-    pageNum,
-    pageSize,
-    batchLevel,
-    townId,
-    batchNo: params?.batchNo || undefined,
-    statusIn: statusIn || undefined,
-    lite: true,
-    withTownTotals: true,
-  })
-  let list = (res?.list || []).map(mapVillageBatch)
-  if (!list.length && !params?.batchNo && !townId) {
-    const fallback = await post<PageResult<Record<string, unknown>>>('/batches/page', {
+  // withTownTotals 会汇总各镇人数/金额,线上常超过默认 15s
+  const pageTimeout = { timeout: 120_000 }
+  const res = await post<PageResult<Record<string, unknown>>>(
+    '/batches/page',
+    {
       pageNum,
       pageSize,
-      batchLevel: 'TOWN',
+      batchLevel,
+      townId,
       batchNo: params?.batchNo || undefined,
       statusIn: statusIn || undefined,
       lite: true,
       withTownTotals: true,
-    })
+    },
+    pageTimeout,
+  )
+  let list = (res?.list || []).map(mapVillageBatch)
+  if (!list.length && !params?.batchNo && !townId) {
+    const fallback = await post<PageResult<Record<string, unknown>>>(
+      '/batches/page',
+      {
+        pageNum,
+        pageSize,
+        batchLevel: 'TOWN',
+        batchNo: params?.batchNo || undefined,
+        statusIn: statusIn || undefined,
+        lite: true,
+        withTownTotals: true,
+      },
+      pageTimeout,
+    )
     list = (fallback?.list || []).map(mapVillageBatch)
     return {
       list,

+ 62 - 62
src/auto-imports.d.ts

@@ -6,74 +6,74 @@
 // biome-ignore lint: disable
 export {}
 declare global {
-  const EffectScope: typeof import('vue')['EffectScope']
+  const EffectScope: typeof import('vue').EffectScope
   const ElMessage: typeof import('element-plus/es')['ElMessage']
-  const computed: typeof import('vue')['computed']
-  const createApp: typeof import('vue')['createApp']
-  const customRef: typeof import('vue')['customRef']
-  const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
-  const defineComponent: typeof import('vue')['defineComponent']
-  const effectScope: typeof import('vue')['effectScope']
-  const getCurrentInstance: typeof import('vue')['getCurrentInstance']
-  const getCurrentScope: typeof import('vue')['getCurrentScope']
+  const computed: typeof import('vue').computed
+  const createApp: typeof import('vue').createApp
+  const customRef: typeof import('vue').customRef
+  const defineAsyncComponent: typeof import('vue').defineAsyncComponent
+  const defineComponent: typeof import('vue').defineComponent
+  const effectScope: typeof import('vue').effectScope
+  const getCurrentInstance: typeof import('vue').getCurrentInstance
+  const getCurrentScope: typeof import('vue').getCurrentScope
   const getCurrentWatcher: typeof import('vue').getCurrentWatcher
-  const h: typeof import('vue')['h']
-  const inject: typeof import('vue')['inject']
-  const isProxy: typeof import('vue')['isProxy']
-  const isReactive: typeof import('vue')['isReactive']
-  const isReadonly: typeof import('vue')['isReadonly']
-  const isRef: typeof import('vue')['isRef']
+  const h: typeof import('vue').h
+  const inject: typeof import('vue').inject
+  const isProxy: typeof import('vue').isProxy
+  const isReactive: typeof import('vue').isReactive
+  const isReadonly: typeof import('vue').isReadonly
+  const isRef: typeof import('vue').isRef
   const isShallow: typeof import('vue').isShallow
-  const markRaw: typeof import('vue')['markRaw']
-  const nextTick: typeof import('vue')['nextTick']
-  const onActivated: typeof import('vue')['onActivated']
-  const onBeforeMount: typeof import('vue')['onBeforeMount']
-  const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
-  const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
-  const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
-  const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
-  const onDeactivated: typeof import('vue')['onDeactivated']
-  const onErrorCaptured: typeof import('vue')['onErrorCaptured']
-  const onMounted: typeof import('vue')['onMounted']
-  const onRenderTracked: typeof import('vue')['onRenderTracked']
-  const onRenderTriggered: typeof import('vue')['onRenderTriggered']
-  const onScopeDispose: typeof import('vue')['onScopeDispose']
-  const onServerPrefetch: typeof import('vue')['onServerPrefetch']
-  const onUnmounted: typeof import('vue')['onUnmounted']
-  const onUpdated: typeof import('vue')['onUpdated']
-  const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
-  const provide: typeof import('vue')['provide']
-  const reactive: typeof import('vue')['reactive']
-  const readonly: typeof import('vue')['readonly']
-  const ref: typeof import('vue')['ref']
-  const resolveComponent: typeof import('vue')['resolveComponent']
-  const shallowReactive: typeof import('vue')['shallowReactive']
-  const shallowReadonly: typeof import('vue')['shallowReadonly']
-  const shallowRef: typeof import('vue')['shallowRef']
-  const toRaw: typeof import('vue')['toRaw']
-  const toRef: typeof import('vue')['toRef']
-  const toRefs: typeof import('vue')['toRefs']
-  const toValue: typeof import('vue')['toValue']
-  const triggerRef: typeof import('vue')['triggerRef']
-  const unref: typeof import('vue')['unref']
-  const useAttrs: typeof import('vue')['useAttrs']
-  const useCssModule: typeof import('vue')['useCssModule']
-  const useCssVars: typeof import('vue')['useCssVars']
-  const useId: typeof import('vue')['useId']
-  const useLink: typeof import('vue-router')['useLink']
-  const useModel: typeof import('vue')['useModel']
-  const useRoute: typeof import('vue-router')['useRoute']
-  const useRouter: typeof import('vue-router')['useRouter']
-  const useSlots: typeof import('vue')['useSlots']
-  const useTemplateRef: typeof import('vue')['useTemplateRef']
-  const watch: typeof import('vue')['watch']
-  const watchEffect: typeof import('vue')['watchEffect']
-  const watchPostEffect: typeof import('vue')['watchPostEffect']
-  const watchSyncEffect: typeof import('vue')['watchSyncEffect']
+  const markRaw: typeof import('vue').markRaw
+  const nextTick: typeof import('vue').nextTick
+  const onActivated: typeof import('vue').onActivated
+  const onBeforeMount: typeof import('vue').onBeforeMount
+  const onBeforeRouteLeave: typeof import('vue-router').onBeforeRouteLeave
+  const onBeforeRouteUpdate: typeof import('vue-router').onBeforeRouteUpdate
+  const onBeforeUnmount: typeof import('vue').onBeforeUnmount
+  const onBeforeUpdate: typeof import('vue').onBeforeUpdate
+  const onDeactivated: typeof import('vue').onDeactivated
+  const onErrorCaptured: typeof import('vue').onErrorCaptured
+  const onMounted: typeof import('vue').onMounted
+  const onRenderTracked: typeof import('vue').onRenderTracked
+  const onRenderTriggered: typeof import('vue').onRenderTriggered
+  const onScopeDispose: typeof import('vue').onScopeDispose
+  const onServerPrefetch: typeof import('vue').onServerPrefetch
+  const onUnmounted: typeof import('vue').onUnmounted
+  const onUpdated: typeof import('vue').onUpdated
+  const onWatcherCleanup: typeof import('vue').onWatcherCleanup
+  const provide: typeof import('vue').provide
+  const reactive: typeof import('vue').reactive
+  const readonly: typeof import('vue').readonly
+  const ref: typeof import('vue').ref
+  const resolveComponent: typeof import('vue').resolveComponent
+  const shallowReactive: typeof import('vue').shallowReactive
+  const shallowReadonly: typeof import('vue').shallowReadonly
+  const shallowRef: typeof import('vue').shallowRef
+  const toRaw: typeof import('vue').toRaw
+  const toRef: typeof import('vue').toRef
+  const toRefs: typeof import('vue').toRefs
+  const toValue: typeof import('vue').toValue
+  const triggerRef: typeof import('vue').triggerRef
+  const unref: typeof import('vue').unref
+  const useAttrs: typeof import('vue').useAttrs
+  const useCssModule: typeof import('vue').useCssModule
+  const useCssVars: typeof import('vue').useCssVars
+  const useId: typeof import('vue').useId
+  const useLink: typeof import('vue-router').useLink
+  const useModel: typeof import('vue').useModel
+  const useRoute: typeof import('vue-router').useRoute
+  const useRouter: typeof import('vue-router').useRouter
+  const useSlots: typeof import('vue').useSlots
+  const useTemplateRef: typeof import('vue').useTemplateRef
+  const watch: typeof import('vue').watch
+  const watchEffect: typeof import('vue').watchEffect
+  const watchPostEffect: typeof import('vue').watchPostEffect
+  const watchSyncEffect: typeof import('vue').watchSyncEffect
 }
 // for type re-export
 declare global {
   // @ts-ignore
-  export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
+  export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
   import('vue')
 }

+ 10 - 1
src/components.d.ts

@@ -1,8 +1,11 @@
 /* eslint-disable */
 // @ts-nocheck
+// biome-ignore lint: disable
+// oxlint-disable
+// ------
 // Generated by unplugin-vue-components
 // Read more: https://github.com/vuejs/core/pull/3399
-// biome-ignore lint: disable
+
 export {}
 
 /* prettier-ignore */
@@ -13,6 +16,9 @@ declare module 'vue' {
     ChartBox: typeof import('./components/common/ChartBox.vue')['default']
     ElAlert: typeof import('element-plus/es')['ElAlert']
     ElAside: typeof import('element-plus/es')['ElAside']
+    ElBadge: typeof import('element-plus/es')['ElBadge']
+    ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
+    ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
     ElButton: typeof import('element-plus/es')['ElButton']
     ElCol: typeof import('element-plus/es')['ElCol']
     ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
@@ -39,6 +45,7 @@ declare module 'vue' {
     ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
     ElOption: typeof import('element-plus/es')['ElOption']
     ElPagination: typeof import('element-plus/es')['ElPagination']
+    ElPopover: typeof import('element-plus/es')['ElPopover']
     ElRadio: typeof import('element-plus/es')['ElRadio']
     ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
     ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
@@ -59,6 +66,7 @@ declare module 'vue' {
     ElTooltip: typeof import('element-plus/es')['ElTooltip']
     ElUpload: typeof import('element-plus/es')['ElUpload']
     FlowAttachments: typeof import('./components/biz/FlowAttachments.vue')['default']
+    ListPageToolbar: typeof import('./components/common/ListPageToolbar.vue')['default']
     PagePanel: typeof import('./components/common/PagePanel.vue')['default']
     PersonNameCell: typeof import('./components/common/PersonNameCell.vue')['default']
     PersonnelDetailDescriptions: typeof import('./components/biz/PersonnelDetailDescriptions.vue')['default']
@@ -71,6 +79,7 @@ declare module 'vue' {
     RouterView: typeof import('vue-router')['RouterView']
     SensitiveRevealSwitch: typeof import('./components/common/SensitiveRevealSwitch.vue')['default']
     SensitiveValue: typeof import('./components/common/SensitiveValue.vue')['default']
+    SplitPaneCard: typeof import('./components/common/SplitPaneCard.vue')['default']
     StatCards: typeof import('./components/common/StatCards.vue')['default']
     StatusTag: typeof import('./components/common/StatusTag.vue')['default']
     TablePagination: typeof import('./components/common/TablePagination.vue')['default']

+ 13 - 16
src/components/biz/BatchDetailDrawer.vue

@@ -3,7 +3,9 @@
     :model-value="modelValue"
     :title="drawerTitle"
     size="820px"
+    append-to-body
     destroy-on-close
+    class="batch-detail-drawer"
     @update:model-value="emit('update:modelValue', $event)"
     @closed="onClosed"
   >
@@ -80,7 +82,8 @@
                 {{ a.label }}
               </el-tag>
             </div>
-            <p class="batch-drawer__tip">以上为流程提示,具体办理请在对应业务菜单操作。</p>
+            <!-- 说明 tip 由全局 .page-tip { display:none } 隐藏,勿删 -->
+            <p class="page-tip batch-drawer__tip">以上为流程提示,具体办理请在对应业务菜单操作。</p>
           </div>
           <el-alert
             v-else
@@ -113,12 +116,11 @@
             class="batch-drawer__adjust-tip"
             title="本批为年调标差额补发:不展示月标,本批只发调标差额,应发合计等于补发。"
           />
-          <div class="batch-drawer__people-search">
+          <div class="inline-search batch-drawer__people-search">
             <el-input
               v-model="peopleSearchName"
               placeholder="姓名"
               clearable
-              size="small"
               style="width: 120px"
               @keyup.enter="onPeopleSearch"
             />
@@ -126,12 +128,11 @@
               v-model="peopleSearchIdCard"
               placeholder="身份证(可后几位)"
               clearable
-              size="small"
               style="width: 160px"
               @keyup.enter="onPeopleSearch"
             />
-            <el-button size="small" type="primary" @click="onPeopleSearch">查询</el-button>
-            <el-button size="small" @click="resetPeopleSearch">重置</el-button>
+            <el-button type="primary" @click="onPeopleSearch">查询</el-button>
+            <el-button @click="resetPeopleSearch">重置</el-button>
             <SensitiveRevealSwitch v-model="revealSensitive" />
           </div>
           <el-table
@@ -726,7 +727,7 @@ watch(tab, (name) => {
   &__progress {
     margin-top: 14px;
     padding: 12px 8px 4px;
-    background: #faf8f4;
+    background: #f5f7fa;
     border: 1px solid @border-light;
     border-radius: @radius-sm;
   }
@@ -740,7 +741,7 @@ watch(tab, (name) => {
   &__actions {
     margin-top: 12px;
     padding: 12px;
-    background: #f7f2e8;
+    background: @el-primary-light-9;
     border: 1px solid @border-light;
     border-radius: @radius-sm;
   }
@@ -761,7 +762,7 @@ watch(tab, (name) => {
   &__tip {
     margin: 8px 0 0;
     font-size: 12px;
-    color: #8a7350;
+    color: #8a9099;
   }
 
   &__alert {
@@ -788,11 +789,7 @@ watch(tab, (name) => {
   }
 
   &__people-search {
-    display: flex;
-    flex-wrap: wrap;
-    align-items: center;
-    gap: 8px;
-    margin-bottom: 8px;
+    /* 布局走全局 .inline-search */
   }
 
   &__upload-btn {
@@ -826,7 +823,7 @@ watch(tab, (name) => {
 
   &__flow-meta {
     font-size: 12px;
-    color: #8a7350;
+    color: #8a9099;
   }
 
   &__flow-opinion {
@@ -841,7 +838,7 @@ watch(tab, (name) => {
   &__note {
     margin-top: 16px;
     padding: 12px;
-    background: #f7f2e8;
+    background: #f5f7fa;
     border: 1px solid @border-light;
     border-radius: @radius-sm;
   }

+ 10 - 1
src/components/biz/PersonPickerDialog.vue

@@ -247,12 +247,21 @@ function onPick(row: Person) {
 .person-picker-dialog {
   &__filter {
     margin-bottom: 8px;
+
+    :deep(.el-form-item) {
+      margin-bottom: 8px;
+    }
+
+    :deep(.el-button) {
+      min-height: 32px !important;
+      height: 32px !important;
+    }
   }
 
   &__tip {
     margin: 0 0 8px;
     font-size: 12px;
-    color: #8a7a66;
+    color: @text-secondary;
     line-height: 1.5;
   }
 }

+ 205 - 0
src/components/common/ListPageToolbar.vue

@@ -0,0 +1,205 @@
+<template>
+  <!-- 蓝湖风格 A:关键词 + 查询 + 多条件 + 刷新 | 操作(最多 3 个,其余进 …) -->
+  <div class="list-page-toolbar">
+    <div class="list-page-toolbar__left">
+      <el-input
+        v-if="showKeyword"
+        :model-value="keyword"
+        :placeholder="keywordPlaceholder"
+        clearable
+        class="list-page-toolbar__keyword"
+        @update:model-value="emit('update:keyword', String($event ?? ''))"
+        @keyup.enter="emit('search')"
+      >
+        <template #prefix>
+          <el-icon><Search /></el-icon>
+        </template>
+      </el-input>
+
+      <el-button type="primary" @click="emit('search')">查询</el-button>
+
+      <el-popover
+        v-if="$slots.filters"
+        v-model:visible="filterOpen"
+        placement="bottom-start"
+        :width="filterWidth"
+        trigger="click"
+      >
+        <template #reference>
+          <el-button>
+            多条件查询
+            <el-icon class="list-page-toolbar__caret"><ArrowDown /></el-icon>
+          </el-button>
+        </template>
+        <div class="list-page-toolbar__filters">
+          <slot name="filters" />
+          <div class="list-page-toolbar__filter-footer">
+            <el-button @click="onFilterCancel">取消</el-button>
+            <el-button type="primary" @click="onFilterSearch">查询</el-button>
+          </div>
+        </div>
+      </el-popover>
+
+      <el-button
+        class="list-page-toolbar__refresh"
+        title="刷新"
+        @click="emit('refresh')"
+      >
+        <el-icon :size="16"><RefreshRight /></el-icon>
+      </el-button>
+    </div>
+
+    <div v-if="visibleActions.length || $slots.actions" class="list-page-toolbar__right">
+      <slot name="actions">
+        <el-button
+          v-for="act in primaryActions"
+          :key="act.key"
+          :type="act.type || 'default'"
+          :loading="act.loading"
+          :disabled="act.disabled"
+          @click="act.onClick"
+        >
+          {{ act.label }}
+        </el-button>
+        <el-dropdown v-if="moreActions.length" trigger="click">
+          <el-button class="list-page-toolbar__more" title="更多">
+            <el-icon :size="16"><MoreFilled /></el-icon>
+          </el-button>
+          <template #dropdown>
+            <el-dropdown-menu>
+              <el-dropdown-item
+                v-for="act in moreActions"
+                :key="act.key"
+                :disabled="act.disabled || act.loading"
+                @click="act.onClick"
+              >
+                {{ act.label }}
+              </el-dropdown-item>
+            </el-dropdown-menu>
+          </template>
+        </el-dropdown>
+      </slot>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, ref } from 'vue'
+import { ArrowDown, MoreFilled, RefreshRight, Search } from '@element-plus/icons-vue'
+
+export interface ListToolbarAction {
+  key: string
+  label: string
+  type?: 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'default'
+  loading?: boolean
+  disabled?: boolean
+  /** 默认 true */
+  visible?: boolean
+  onClick: () => void
+}
+
+const props = withDefaults(
+  defineProps<{
+    keyword?: string
+    keywordPlaceholder?: string
+    showKeyword?: boolean
+    /** 主区最多展示几个按钮,其余进「…」 */
+    maxActions?: number
+    actions?: ListToolbarAction[]
+    filterWidth?: number
+  }>(),
+  {
+    keyword: '',
+    keywordPlaceholder: '请输入关键词查询',
+    showKeyword: true,
+    maxActions: 3,
+    actions: () => [],
+    filterWidth: 360,
+  },
+)
+
+const emit = defineEmits<{
+  'update:keyword': [value: string]
+  search: []
+  refresh: []
+  'filter-cancel': []
+}>()
+
+const filterOpen = ref(false)
+
+const visibleActions = computed(() =>
+  (props.actions || []).filter((a) => a.visible !== false),
+)
+
+const primaryActions = computed(() =>
+  visibleActions.value.slice(0, props.maxActions),
+)
+
+const moreActions = computed(() =>
+  visibleActions.value.slice(props.maxActions),
+)
+
+function onFilterCancel() {
+  filterOpen.value = false
+  emit('filter-cancel')
+}
+
+function onFilterSearch() {
+  filterOpen.value = false
+  emit('search')
+}
+</script>
+
+<style scoped lang="less">
+.list-page-toolbar {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  margin-bottom: 12px;
+}
+
+.list-page-toolbar__left,
+.list-page-toolbar__right {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 8px;
+}
+
+.list-page-toolbar__keyword {
+  width: 220px;
+}
+
+.list-page-toolbar__caret {
+  margin-left: 4px;
+}
+
+.list-page-toolbar__refresh,
+.list-page-toolbar__more {
+  width: 32px;
+  padding: 0 !important;
+}
+
+.list-page-toolbar__filters {
+  display: grid;
+  gap: 12px;
+}
+
+.list-page-toolbar__filter-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 8px;
+  padding-top: 4px;
+}
+
+:deep(.el-button) {
+  min-height: 32px !important;
+  height: 32px !important;
+}
+
+:deep(.el-input__wrapper) {
+  min-height: 32px !important;
+}
+</style>

+ 23 - 0
src/components/common/PagePanel.vue

@@ -18,18 +18,41 @@ defineProps<{ title?: string }>()
 .page-panel__title {
   display: flex;
   align-items: center;
+  box-sizing: border-box;
+  min-height: 36px;
 }
 
 .page-panel__text {
   flex: 1;
   min-width: 0;
+  line-height: 1.4;
+  /* 蓝湖风格 A:页名大号近黑加粗 */
+  font-size: 18px;
+  font-weight: 700;
+  color: #262626;
 }
 
 .page-panel__extra {
   display: flex;
   align-items: center;
+  flex-wrap: wrap;
   gap: 8px;
   font-weight: 400;
   margin-left: 8px;
+  min-height: 32px;
+
+  :deep(.el-button) {
+    min-height: 32px !important;
+    height: 32px !important;
+    padding: 0 14px !important;
+    font-size: 13px !important;
+  }
+
+  :deep(.el-button--small) {
+    min-height: 32px !important;
+    height: 32px !important;
+    padding: 0 14px !important;
+    font-size: 13px !important;
+  }
 }
 </style>

+ 45 - 44
src/components/common/PieMixPanel.vue

@@ -1,43 +1,43 @@
 <template>
-  <PagePanel :title="title" class="pie-mix-panel">
-    <div v-if="items.length" class="pie-mix">
-      <div class="pie-mix__chart">
-        <ChartBox :option="chartOption" height="100%" />
-      </div>
-      <ul class="pie-mix__list">
-        <li v-for="(item, idx) in items" :key="item.name">
-          <i class="pie-mix__dot" :style="{ background: PIE_COLORS[idx % PIE_COLORS.length] }" />
-          <span class="pie-mix__name">{{ item.name }}</span>
-          <strong>{{ formatCount(item.value) }}</strong>
-          <em>{{ percentOf(item.value) }}</em>
-        </li>
-        <li class="pie-mix__total">
-          <span class="pie-mix__name">合计</span>
-          <strong>{{ formatCount(total) }}</strong>
-          <em>{{ unit }}</em>
-        </li>
-      </ul>
+  <!-- 仅内容区;外层标题由页面 PagePanel 统一提供 -->
+  <div v-if="items.length" class="pie-mix">
+    <div class="pie-mix__chart">
+      <ChartBox :option="chartOption" height="100%" />
     </div>
-    <div v-else class="pie-mix__empty">暂无数据</div>
-  </PagePanel>
+    <ul class="pie-mix__list">
+      <li v-for="(item, idx) in items" :key="item.name">
+        <i class="pie-mix__dot" :style="{ background: PIE_COLORS[idx % PIE_COLORS.length] }" />
+        <span class="pie-mix__name">{{ item.name }}</span>
+        <strong>{{ formatCount(item.value) }}</strong>
+        <em>{{ percentOf(item.value) }}</em>
+      </li>
+      <li class="pie-mix__total">
+        <span class="pie-mix__name">合计</span>
+        <strong>{{ formatCount(total) }}</strong>
+        <em>{{ unit }}</em>
+      </li>
+    </ul>
+  </div>
+  <div v-else class="pie-mix__empty">暂无数据</div>
 </template>
 
 <script setup lang="ts">
 import { computed } from 'vue'
 import type { EChartsCoreOption } from 'echarts/core'
-import PagePanel from '@/components/common/PagePanel.vue'
 import ChartBox from '@/components/common/ChartBox.vue'
 import type { ChartPieItem } from '@/types/biz'
 
-const PIE_COLORS = ['#2B73F8', '#27C98E', '#FA8C16', '#722ED1', '#1890FF']
+/* 对齐蓝湖主色 + 有限辅色 */
+const PIE_COLORS = ['#2f54eb', '#13c2c2', '#faad14', '#722ed1', '#52c41a']
 
 const props = withDefaults(
   defineProps<{
-    title: string
+    /** @deprecated 标题改由外层 PagePanel 传入 */
+    title?: string
     items: ChartPieItem[]
     unit?: string
   }>(),
-  { unit: '人' },
+  { title: '', unit: '人' },
 )
 
 const total = computed(() => props.items.reduce((sum, item) => sum + Number(item.value || 0), 0))
@@ -63,7 +63,7 @@ const chartOption = computed<EChartsCoreOption>(() => ({
   legend: { show: false },
   series: [
     {
-      name: props.title,
+      name: props.title || '占比',
       type: 'pie',
       radius: ['48%', '72%'],
       center: ['50%', '50%'],
@@ -73,7 +73,7 @@ const chartOption = computed<EChartsCoreOption>(() => ({
       itemStyle: { borderColor: '#fff', borderWidth: 2 },
       emphasis: {
         scaleSize: 4,
-        itemStyle: { shadowBlur: 8, shadowColor: 'rgba(43,115,248,0.28)' },
+        itemStyle: { shadowBlur: 8, shadowColor: 'rgba(47,84,235,0.28)' },
       },
     },
   ],
@@ -81,24 +81,22 @@ const chartOption = computed<EChartsCoreOption>(() => ({
 </script>
 
 <style scoped lang="less">
-.pie-mix-panel {
-  min-height: 0;
-  overflow: visible;
-}
-
+/* 占满 PagePanel 标题下剩余高度,整块(图+图例)垂直居中 */
 .pie-mix {
   flex: 1;
   display: flex;
   align-items: center;
-  gap: 4px 12px;
+  justify-content: flex-start;
+  gap: 12px 16px;
+  width: 100%;
   min-height: 200px;
-  overflow: visible;
+  box-sizing: border-box;
 }
 
 .pie-mix__chart {
-  flex: 1 1 46%;
-  min-width: 140px;
-  height: 188px;
+  flex: 0 0 160px;
+  width: 160px;
+  height: 160px;
   position: relative;
 
   :deep(.chart-box) {
@@ -109,12 +107,11 @@ const chartOption = computed<EChartsCoreOption>(() => ({
 }
 
 .pie-mix__list {
-  flex: 1 1 54%;
-  min-width: 168px;
+  flex: 1 1 auto;
+  min-width: 0;
   margin: 0;
-  padding: 0 2px 0 0;
+  padding: 0;
   list-style: none;
-  overflow: visible;
 }
 
 .pie-mix__list li {
@@ -123,7 +120,7 @@ const chartOption = computed<EChartsCoreOption>(() => ({
   align-items: center;
   gap: 8px;
   padding: 5px 0;
-  border-bottom: 1px dashed #efe8dc;
+  border-bottom: 1px dashed #e8eef5;
   font-size: 12px;
   line-height: 1.4;
 }
@@ -185,7 +182,7 @@ const chartOption = computed<EChartsCoreOption>(() => ({
   display: flex;
   align-items: center;
   justify-content: center;
-  min-height: 188px;
+  min-height: 200px;
   font-size: 13px;
   color: @text-secondary;
 }
@@ -193,12 +190,16 @@ const chartOption = computed<EChartsCoreOption>(() => ({
 @media (max-width: 520px) {
   .pie-mix {
     flex-direction: column;
-    align-items: stretch;
+    align-items: center;
   }
 
   .pie-mix__chart {
+    width: 150px;
+    height: 150px;
+  }
+
+  .pie-mix__list {
     width: 100%;
-    height: 200px;
   }
 }
 </style>

+ 15 - 2
src/components/common/SensitiveValue.vue

@@ -1,5 +1,5 @@
 <template>
-  <span class="sensitive-value">
+  <span class="sensitive-value" :class="{ 'is-revealed': revealedEffective }">
     <span class="sensitive-value__text" :title="revealedEffective ? raw : undefined">
       {{ display }}
     </span>
@@ -105,9 +105,22 @@ function toggle() {
 }
 
 .sensitive-value__text {
+  white-space: nowrap;
+}
+
+.sensitive-value:not(.is-revealed) .sensitive-value__text {
   overflow: hidden;
   text-overflow: ellipsis;
-  white-space: nowrap;
+  max-width: 100%;
+}
+
+.sensitive-value.is-revealed {
+  max-width: none;
+}
+
+.sensitive-value.is-revealed .sensitive-value__text {
+  overflow: visible;
+  text-overflow: clip;
 }
 
 .sensitive-value__btn {

+ 35 - 0
src/components/common/SplitPaneCard.vue

@@ -0,0 +1,35 @@
+<template>
+  <!-- 蓝湖左树右表:同一白卡、竖线分割 -->
+  <div class="style-a-split" :class="rootClass">
+    <aside class="style-a-split__aside">
+      <slot name="aside" />
+    </aside>
+    <section class="style-a-split__main">
+      <div v-if="title || $slots.extra" class="style-a-split__main-head">
+        <span v-if="title" class="style-a-split__main-title">{{ title }}</span>
+        <div v-if="$slots.extra" class="style-a-split__extra">
+          <slot name="extra" />
+        </div>
+      </div>
+      <slot />
+    </section>
+  </div>
+</template>
+
+<script setup lang="ts">
+defineProps<{
+  title?: string
+  /** 附加到根节点的 class */
+  rootClass?: string
+}>()
+</script>
+
+<style scoped lang="less">
+.style-a-split__extra {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 8px;
+  margin-left: auto;
+}
+</style>

+ 158 - 25
src/components/common/StatCards.vue

@@ -1,11 +1,26 @@
 <template>
-  <div class="stat-cards" :class="{ 'stat-cards--five': items.length >= 5 }">
-    <div v-for="item in items" :key="item.label" class="stat-cards__item">
-      <div class="stat-cards__label">
-        <span>{{ item.label }}</span>
-        <el-tooltip v-if="item.tip" :content="item.tip" placement="top" :show-after="200">
-          <span class="stat-cards__tip" tabindex="0">?</span>
-        </el-tooltip>
+  <!-- 蓝湖风格 B:多色渐变 KPI 卡(非整排同色蓝条) -->
+  <div
+    class="stat-cards"
+    :class="[
+      items.length >= 5 ? 'stat-cards--five' : '',
+      items.length === 6 ? 'stat-cards--six' : '',
+    ]"
+  >
+    <div
+      v-for="(item, idx) in items"
+      :key="item.label"
+      class="stat-cards__item"
+      :class="`stat-cards__item--${resolveTone(item, idx)}`"
+    >
+      <div class="stat-cards__top">
+        <div class="stat-cards__label">
+          <span>{{ item.label }}</span>
+          <el-tooltip v-if="item.tip" :content="item.tip" placement="top" :show-after="200">
+            <span class="stat-cards__tip" tabindex="0">?</span>
+          </el-tooltip>
+        </div>
+        <span v-if="item.badge" class="stat-cards__badge">{{ item.badge }}</span>
       </div>
       <div class="stat-cards__value">
         {{ item.value }}
@@ -16,16 +31,28 @@
 </template>
 
 <script setup lang="ts">
+export type StatCardTone = 'blue' | 'green' | 'purple' | 'orange' | 'cyan' | 'red'
+
 export interface StatCardItem {
   label: string
   value: string | number
   unit?: string
   tip?: string
+  /** 右上角小徽章,如占比 */
+  badge?: string
+  /** 指定色调;不传则按序号循环 */
+  tone?: StatCardTone
 }
 
-defineProps<{
+const TONE_CYCLE: StatCardTone[] = ['blue', 'green', 'purple', 'orange', 'cyan', 'red']
+
+const props = defineProps<{
   items: StatCardItem[]
 }>()
+
+function resolveTone(item: StatCardItem, idx: number): StatCardTone {
+  return item.tone || TONE_CYCLE[idx % TONE_CYCLE.length]
+}
 </script>
 
 <style scoped lang="less">
@@ -33,35 +60,103 @@ defineProps<{
   display: grid;
   grid-template-columns: repeat(4, 1fr);
   gap: 12px;
-  margin-bottom: 12px;
+  margin-bottom: 4px;
 
   @media (max-width: 1100px) {
     grid-template-columns: repeat(2, 1fr);
   }
 
-  &__item {
-    background: @primary-color;
-    border: none;
-    border-radius: @radius-card;
-    box-shadow: 0 2px 8px rgba(43, 115, 248, 0.22);
-    padding: 14px 16px;
-  }
-
   &--five {
     grid-template-columns: repeat(5, 1fr);
 
-    @media (max-width: 1200px) {
+    @media (max-width: 1280px) {
+      grid-template-columns: repeat(3, 1fr);
+    }
+
+    @media (max-width: 900px) {
+      grid-template-columns: repeat(2, 1fr);
+    }
+  }
+
+  &--six {
+    grid-template-columns: repeat(6, 1fr);
+
+    @media (max-width: 1400px) {
       grid-template-columns: repeat(3, 1fr);
     }
   }
 
+  &__item {
+    position: relative;
+    border: 0;
+    border-radius: 12px;
+    padding: 16px 16px 14px;
+    min-height: 92px;
+    color: #fff;
+    overflow: hidden;
+    box-shadow: 0 8px 20px rgba(47, 84, 235, 0.12);
+
+    /* 淡纹理,贴近看板稿 KPI 卡 */
+    &::after {
+      content: '';
+      position: absolute;
+      right: -20%;
+      top: -40%;
+      width: 120px;
+      height: 120px;
+      border-radius: 50%;
+      background: rgba(255, 255, 255, 0.12);
+      pointer-events: none;
+    }
+
+    &--blue {
+      background: linear-gradient(135deg, #5b8ff9 0%, #2f54eb 55%, #1d39c4 100%);
+      box-shadow: 0 8px 20px rgba(47, 84, 235, 0.28);
+    }
+
+    &--green {
+      background: linear-gradient(135deg, #73d13d 0%, #52c41a 55%, #389e0d 100%);
+      box-shadow: 0 8px 20px rgba(82, 196, 26, 0.28);
+    }
+
+    &--purple {
+      background: linear-gradient(135deg, #b37feb 0%, #722ed1 55%, #531dab 100%);
+      box-shadow: 0 8px 20px rgba(114, 46, 209, 0.28);
+    }
+
+    &--orange {
+      background: linear-gradient(135deg, #ffc53d 0%, #fa8c16 55%, #d46b08 100%);
+      box-shadow: 0 8px 20px rgba(250, 140, 22, 0.28);
+    }
+
+    &--cyan {
+      background: linear-gradient(135deg, #36cfc9 0%, #13c2c2 55%, #08979c 100%);
+      box-shadow: 0 8px 20px rgba(19, 194, 194, 0.28);
+    }
+
+    &--red {
+      background: linear-gradient(135deg, #ff7875 0%, #f5222d 55%, #cf1322 100%);
+      box-shadow: 0 8px 20px rgba(245, 34, 45, 0.28);
+    }
+  }
+
+  &__top {
+    display: flex;
+    align-items: flex-start;
+    justify-content: space-between;
+    gap: 8px;
+    margin-bottom: 10px;
+    position: relative;
+    z-index: 1;
+  }
+
   &__label {
     display: flex;
     align-items: center;
     gap: 6px;
     font-size: 13px;
-    color: rgba(255, 255, 255, 0.88);
-    margin-bottom: 8px;
+    color: rgba(255, 255, 255, 0.92);
+    line-height: 1.3;
   }
 
   &__tip {
@@ -76,20 +171,58 @@ defineProps<{
     line-height: 1;
     cursor: help;
     color: rgba(255, 255, 255, 0.9);
+    flex-shrink: 0;
+  }
+
+  &__badge {
+    flex-shrink: 0;
+    min-width: 40px;
+    height: 22px;
+    padding: 0 8px;
+    border-radius: 11px;
+    background: rgba(255, 255, 255, 0.92);
+    color: inherit;
+    font-size: 12px;
+    font-weight: 600;
+    line-height: 22px;
+    text-align: center;
+    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
+  }
+
+  &__item--blue .stat-cards__badge {
+    color: #2f54eb;
+  }
+  &__item--green .stat-cards__badge {
+    color: #389e0d;
+  }
+  &__item--purple .stat-cards__badge {
+    color: #722ed1;
+  }
+  &__item--orange .stat-cards__badge {
+    color: #d46b08;
+  }
+  &__item--cyan .stat-cards__badge {
+    color: #08979c;
+  }
+  &__item--red .stat-cards__badge {
+    color: #cf1322;
   }
 
   &__value {
-    font-size: 22px;
+    position: relative;
+    z-index: 1;
+    font-size: 26px;
     font-weight: 700;
-    color: @text-white;
-    line-height: 1.2;
+    color: #fff;
+    line-height: 1.15;
+    letter-spacing: 0.02em;
   }
 
   &__unit {
     margin-left: 4px;
-    font-size: 12px;
+    font-size: 13px;
     font-weight: 400;
-    color: rgba(255, 255, 255, 0.8);
+    color: rgba(255, 255, 255, 0.88);
   }
 }
 </style>

+ 11 - 1
src/components/common/StatusTag.vue

@@ -1,5 +1,8 @@
 <template>
-  <el-tag :type="type" size="small" effect="plain">{{ display }}</el-tag>
+  <!-- 蓝湖状态:浅底色字 Tag,避免实心块 -->
+  <el-tag class="status-tag" :type="type" size="small" effect="plain">
+    {{ display }}
+  </el-tag>
 </template>
 
 <script setup lang="ts">
@@ -38,3 +41,10 @@ const type = computed(() =>
   props.mode === 'level' ? levelTagType(props.status) : statusTagType(display.value || props.status),
 )
 </script>
+
+<style scoped lang="less">
+.status-tag {
+  border-radius: 4px !important;
+  font-weight: 500;
+}
+</style>

+ 2 - 18
src/components/common/TablePagination.vue

@@ -1,8 +1,6 @@
 <template>
+  <!-- 蓝湖分页:总数 + 每页条数 + 页码 + 跳转(右下对齐) -->
   <div v-if="total > 0" class="table-pagination">
-    <el-button size="small" :disabled="innerPage <= 1" @click="goFirst">
-      首页
-    </el-button>
     <el-pagination
       v-model:current-page="innerPage"
       v-model:page-size="innerSize"
@@ -12,9 +10,6 @@
       background
       layout="total, sizes, prev, pager, next, jumper"
     />
-    <el-button size="small" :disabled="innerPage >= lastPage" @click="goLast">
-      末页
-    </el-button>
   </div>
 </template>
 
@@ -43,10 +38,6 @@ const emit = defineEmits<{
   'update:pageSize': [value: number]
 }>()
 
-const lastPage = computed(() =>
-  Math.max(1, Math.ceil(props.total / props.pageSize) || 1),
-)
-
 const innerPage = computed({
   get: () => props.page,
   set: (v: number) => emit('update:page', v),
@@ -60,14 +51,6 @@ const innerSize = computed({
     emit('update:page', 1)
   },
 })
-
-function goFirst() {
-  emit('update:page', 1)
-}
-
-function goLast() {
-  emit('update:page', lastPage.value)
-}
 </script>
 
 <style scoped lang="less">
@@ -82,6 +65,7 @@ function goLast() {
   :deep(.el-pagination) {
     margin-top: 0;
     flex-wrap: wrap;
+    justify-content: flex-end;
   }
 }
 </style>

+ 47 - 73
src/components/common/WorkbenchTipsPanel.vue

@@ -14,19 +14,16 @@
         </el-icon>
       </span>
     </button>
-    <div v-show="open" class="workbench-tips__body">
-      <div class="workbench-tips__steps">
-        <div v-for="(tip, idx) in tips" :key="idx" class="workbench-tips__item">
-          <div class="workbench-tips__rail">
-            <span class="workbench-tips__index">{{ idx + 1 }}</span>
-            <span v-if="idx < tips.length - 1" class="workbench-tips__line" />
-          </div>
-          <div class="workbench-tips__card">
+    <div v-if="open" class="workbench-tips__body">
+      <ol class="workbench-tips__steps">
+        <li v-for="(tip, idx) in tips" :key="idx" class="workbench-tips__item">
+          <span class="workbench-tips__index">{{ idx + 1 }}</span>
+          <div class="workbench-tips__text">
             <div class="workbench-tips__title">{{ tip.title }}</div>
             <p class="workbench-tips__desc">{{ tip.desc }}</p>
           </div>
-        </div>
-      </div>
+        </li>
+      </ol>
     </div>
   </section>
 </template>
@@ -59,42 +56,45 @@ watch(
 </script>
 
 <style scoped lang="less">
+/* 风格 B 辅助说明:轻量灰条,避免抢 KPI/图表视觉 */
 .workbench-tips {
-  background: @block-bg;
+  background: #fff;
   border: 1px solid @border-light;
+  border-radius: 8px;
   overflow: hidden;
 }
 
 .workbench-tips__toggle {
   display: flex;
   align-items: center;
-  gap: 12px;
+  gap: 10px;
   width: 100%;
   margin: 0;
   padding: 10px 14px;
   border: 0;
-  background: linear-gradient(90deg, #eef4ff 0%, #f7f9fc 55%, #fff 100%);
+  background: #fafafa;
   cursor: pointer;
   text-align: left;
   color: inherit;
 
   &:hover {
-    background: linear-gradient(90deg, #e4eeff 0%, #f4f7fb 55%, #fff 100%);
+    background: #f5f5f5;
   }
 }
 
 .workbench-tips__mark {
   flex-shrink: 0;
-  width: 28px;
-  height: 28px;
+  width: 24px;
+  height: 24px;
   border-radius: 6px;
   display: inline-flex;
   align-items: center;
   justify-content: center;
-  font-size: 13px;
+  font-size: 12px;
   font-weight: 700;
-  color: #fff;
-  background: @primary-color;
+  color: @primary-color;
+  background: @el-primary-light-9;
+  border: 1px solid @el-primary-light-7;
 }
 
 .workbench-tips__head {
@@ -104,20 +104,18 @@ watch(
   flex-shrink: 0;
 
   strong {
-    font-size: 14px;
-    font-weight: 700;
-    color: @text-primary;
+    font-size: 13px;
+    font-weight: 600;
+    color: #262626;
   }
 
   em {
     font-style: normal;
     font-size: 12px;
-    color: @primary-color;
-    background: rgba(43, 115, 248, 0.08);
-    border: 1px solid #d6e4ff;
-    border-radius: 10px;
-    padding: 0 8px;
-    line-height: 20px;
+    color: @text-secondary;
+    background: transparent;
+    border: 0;
+    padding: 0;
   }
 }
 
@@ -150,82 +148,58 @@ watch(
 }
 
 .workbench-tips__body {
-  padding: 4px 14px 12px;
+  padding: 8px 14px 12px;
   border-top: 1px solid @border-light;
   background: #fff;
 }
 
 .workbench-tips__steps {
+  margin: 0;
+  padding: 0;
+  list-style: none;
   display: flex;
   flex-direction: column;
+  gap: 8px;
 }
 
 .workbench-tips__item {
   display: grid;
-  grid-template-columns: 28px minmax(0, 1fr);
-  gap: 12px;
-}
-
-.workbench-tips__rail {
-  position: relative;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  padding-top: 10px;
+  grid-template-columns: 22px minmax(0, 1fr);
+  gap: 10px;
+  align-items: flex-start;
 }
 
 .workbench-tips__index {
-  position: relative;
-  z-index: 1;
-  flex-shrink: 0;
-  width: 26px;
-  height: 26px;
+  width: 22px;
+  height: 22px;
   border-radius: 50%;
   display: inline-flex;
   align-items: center;
   justify-content: center;
   font-size: 12px;
-  font-weight: 700;
-  color: #fff;
-  background: linear-gradient(145deg, @brand-gold-deep 0%, @primary-color 100%);
-  box-shadow: 0 1px 3px rgba(43, 115, 248, 0.28);
-}
-
-.workbench-tips__line {
-  flex: 1;
-  width: 2px;
-  margin: 4px 0 2px;
-  min-height: 12px;
-  background: linear-gradient(180deg, rgba(43, 115, 248, 0.55), rgba(43, 115, 248, 0.12));
-  border-radius: 1px;
+  font-weight: 600;
+  color: @text-secondary;
+  background: #f5f5f5;
+  border: 1px solid @border-color;
+  margin-top: 1px;
 }
 
-.workbench-tips__card {
+.workbench-tips__text {
   min-width: 0;
-  padding: 10px 12px;
-  margin: 8px 0 2px;
-  background: linear-gradient(135deg, #f5f9ff 0%, #eef4ff 100%);
-  border: 1px solid #d6e4ff;
-  border-left: 3px solid @primary-color;
-  border-radius: 0 4px 4px 0;
-}
-
-.workbench-tips__item:last-child .workbench-tips__card {
-  margin-bottom: 0;
 }
 
 .workbench-tips__title {
   font-size: 13px;
-  font-weight: 700;
-  color: @text-primary;
-  line-height: 1.3;
+  font-weight: 600;
+  color: #262626;
+  line-height: 1.35;
 }
 
 .workbench-tips__desc {
-  margin: 4px 0 0;
+  margin: 2px 0 0;
   font-size: 12px;
   color: @text-secondary;
-  line-height: 1.6;
+  line-height: 1.55;
 }
 
 @media (max-width: 768px) {

+ 7 - 0
src/composables/useServerPagination.ts

@@ -34,6 +34,13 @@ export function useServerPagination<T>(
       if (gen !== fetchGen) return
       list.value = res.list || []
       total.value = Number(res.total || 0)
+    } catch {
+      // 超时/网络失败时保留上一屏或空表,避免挂载钩子未捕获异常打断页面
+      if (gen !== fetchGen) return
+      if (blockUi) {
+        list.value = []
+        total.value = 0
+      }
     } finally {
       if (gen === fetchGen) loading.value = false
     }

+ 683 - 252
src/layouts/MainLayout.vue

@@ -1,59 +1,67 @@
 <template>
-  <el-container class="main-layout">
-    <el-aside
-      :width="isCollapse ? '64px' : '220px'"
-      class="main-layout__aside"
-    >
-      <div class="logo" :class="{ 'is-collapse': isCollapse }">
-        <template v-if="!isCollapse">
-          <div class="logo__title">{{ systemName }}</div>
-        </template>
-        <div v-else class="logo__mark" :title="systemName">{{ systemLogoChar }}</div>
+  <!--
+    蓝湖「基层减负统一风格」壳层(对照 0ed5dec / 表格标准说明)
+    浅蓝氛围底 + 浅白浮层侧栏 + 页签贴白内容卡;禁止整列蓝墙、禁止旧后台白顶栏骨架
+  -->
+  <div class="app-shell">
+    <header class="brand-bar">
+      <div class="brand-left" @click="goHome" title="返回首页">
+        <div class="brand-logo" aria-hidden="true">
+          <span class="brand-logo__char">{{ logoChar }}</span>
+        </div>
+        <span class="brand-title">{{ displayTitle }}</span>
       </div>
 
-      <el-scrollbar class="main-layout__menu-scroll">
-        <el-menu
-          :default-active="activeMenu"
-          :collapse="isCollapse"
-          :collapse-transition="false"
-          router
-          class="main-layout__menu"
-        >
-          <el-menu-item v-for="item in visibleMenus" :key="item.path" :index="item.path">
-            <el-icon>
-              <component :is="menuIcon(item.icon)" />
-            </el-icon>
-            <template #title>
-              <span>{{ item.title }}</span>
-            </template>
-          </el-menu-item>
-        </el-menu>
-      </el-scrollbar>
-    </el-aside>
-
-    <el-container class="main-layout__right">
-      <el-header class="main-layout__header" height="48px">
-        <div class="main-layout__header-left">
-          <el-icon class="collapse-btn" @click="isCollapse = !isCollapse">
-            <Fold v-if="!isCollapse" />
-            <Expand v-else />
-          </el-icon>
-          <div class="header-page-title">{{ pageTitle }}</div>
-          <!-- <el-breadcrumb separator="/" class="header-breadcrumb">
-            <el-breadcrumb-item
-              v-for="item in breadcrumbs"
-              :key="item.path"
-            >
-              {{ item.meta?.title }}
-            </el-breadcrumb-item>
-          </el-breadcrumb> -->
-        </div>
-        <div class="main-layout__header-right">
+      <div class="brand-right">
+        <button type="button" class="header-chip" title="首页" @click="goHome">
+          <el-icon :size="18"><HomeFilled /></el-icon>
+        </button>
+
+        <button type="button" class="header-chip header-chip--msg" title="消息">
+          <el-badge :value="2" class="header-badge">
+            <el-icon :size="18"><Bell /></el-icon>
+          </el-badge>
+          <span class="header-chip__label">消息</span>
+        </button>
+
+        <el-dropdown trigger="click">
+          <span class="header-user">
+            <span class="header-user__avatar">
+              <el-icon><User /></el-icon>
+            </span>
+            <span class="header-user__name">{{ roleInfo?.label || 'Admin' }}</span>
+            <el-icon class="header-user__arrow"><ArrowDown /></el-icon>
+          </span>
+          <template #dropdown>
+            <el-dropdown-menu>
+              <el-dropdown-item disabled>
+                {{ roleInfo?.org || roleInfo?.label || '-' }}
+              </el-dropdown-item>
+              <el-dropdown-item divided @click="scopeVisible = true">
+                切换角色
+              </el-dropdown-item>
+              <el-dropdown-item>退出登录</el-dropdown-item>
+            </el-dropdown-menu>
+          </template>
+        </el-dropdown>
+      </div>
+    </header>
+
+    <!-- 角色/区划:收进弹层,不占顶栏主视觉 -->
+    <el-dialog
+      v-model="scopeVisible"
+      title="切换角色与区划"
+      width="420px"
+      append-to-body
+      destroy-on-close
+    >
+      <div class="scope-form">
+        <div class="scope-form__row">
+          <label>模拟角色</label>
           <el-select
             :model-value="userStore.currentRole"
-            size="small"
-            class="header-select header-select--role"
             placeholder="模拟角色"
+            style="width: 100%"
             @change="onRoleChange"
           >
             <el-option
@@ -63,61 +71,125 @@
               :value="r.key"
             />
           </el-select>
-          <!-- 暂无用户体系:按角色切换演示数据范围(街镇/村居) -->
+        </div>
+        <div v-if="userStore.needTownScope" class="scope-form__row">
+          <label>街镇</label>
           <el-select
-            v-if="userStore.needTownScope"
             :model-value="userStore.scopeTown"
-            size="small"
-            class="header-select header-select--town"
             :placeholder="userStore.townScopeRequired ? '选择街镇' : '全区'"
             filterable
             :clearable="!userStore.townScopeRequired"
+            style="width: 100%"
             @change="onTownChange"
           >
             <el-option v-for="t in townOptions" :key="t" :label="t" :value="t" />
           </el-select>
+        </div>
+        <div v-if="userStore.needVillageScope" class="scope-form__row">
+          <label>村居</label>
           <el-select
-            v-if="userStore.needVillageScope"
             :model-value="userStore.scopeVillage"
-            size="small"
-            class="header-select header-select--village"
             placeholder="选择村居"
             filterable
             :disabled="!userStore.scopeTown"
+            style="width: 100%"
             @change="onVillageChange"
           >
             <el-option v-for="v in villageOptions" :key="v" :label="v" :value="v" />
           </el-select>
-          <el-dropdown trigger="click">
-            <span class="header-user">
-              <el-icon><User /></el-icon>
-              <span class="header-user__name">{{ roleInfo?.label || '用户' }}</span>
-              <el-icon class="header-user__arrow"><ArrowDown /></el-icon>
-            </span>
-            <template #dropdown>
-              <el-dropdown-menu>
-                <el-dropdown-item disabled>
-                  {{ roleInfo?.org || roleInfo?.label || '-' }}
-                </el-dropdown-item>
-                <el-dropdown-item divided>退出登录</el-dropdown-item>
-              </el-dropdown-menu>
-            </template>
-          </el-dropdown>
         </div>
-      </el-header>
-
-      <el-main class="main-layout__main">
-        <!-- scopeKey:切换角色/镇/村时刷新当前页数据 -->
-        <router-view :key="userStore.scopeKey" />
-      </el-main>
-    </el-container>
-  </el-container>
+      </div>
+      <template #footer>
+        <el-button type="primary" @click="scopeVisible = false">确定</el-button>
+      </template>
+    </el-dialog>
+
+    <div class="shell-body">
+      <!-- 浅白浮层侧栏:非整列蓝底 -->
+      <aside class="sidebar" :class="{ collapsed: isCollapse }">
+        <div class="sidebar-toolbar">
+          <button
+            type="button"
+            class="sidebar-toggle"
+            :title="isCollapse ? '展开菜单' : '收起菜单'"
+            @click="isCollapse = !isCollapse"
+          >
+            <el-icon :size="18">
+              <Expand v-if="isCollapse" />
+              <Fold v-else />
+            </el-icon>
+            <span v-if="!isCollapse" class="sidebar-toggle__text">收起</span>
+          </button>
+        </div>
+
+        <el-scrollbar class="sidebar-scrollbar" wrap-class="scrollbar-wrapper">
+          <nav class="sidebar-nav">
+            <router-link
+              v-for="item in visibleMenus"
+              :key="item.path"
+              :to="item.path"
+              class="nav-item"
+              :class="{ active: activeMenu === item.path }"
+              :title="item.title"
+            >
+              <el-icon class="nav-icon" :size="20">
+                <component :is="menuIcon(item.icon)" />
+              </el-icon>
+              <span class="nav-text">{{ item.title }}</span>
+            </router-link>
+          </nav>
+        </el-scrollbar>
+      </aside>
+
+      <!-- 主区:与侧栏间距 30px;右/底 30px -->
+      <div class="main-wrap">
+        <div class="page-tabs" v-if="openedTabs.length">
+          <div
+            v-for="tab in openedTabs"
+            :key="tab.path"
+            class="page-tab"
+            :class="{ active: activeMenu === tab.path }"
+            @click="router.push(tab.path)"
+          >
+            <span class="page-tab__label">{{ tab.title }}</span>
+            <el-icon
+              v-if="tab.path !== homePath"
+              class="page-tab__close"
+              @click.stop="closeTab(tab.path)"
+            >
+              <Close />
+            </el-icon>
+          </div>
+        </div>
+
+        <!-- 列表/展板共用白底圆角内容卡;is-board 仅区分卡内布局 -->
+        <div
+          ref="contentBodyRef"
+          class="content-body"
+          :class="{ 'is-board': isBoardPage }"
+        >
+          <div class="content-surface" :class="{ 'is-board': isBoardPage }">
+            <router-view :key="userStore.scopeKey" />
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
 </template>
 
 <script setup lang="ts">
-import { computed, onMounted, ref, watch, type Component } from 'vue'
+import { computed, nextTick, onMounted, onUnmounted, ref, watch, type Component } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
 import * as Icons from '@element-plus/icons-vue'
+import {
+  ArrowDown,
+  Bell,
+  Close,
+  Expand,
+  Fold,
+  HomeFilled,
+  User,
+} from '@element-plus/icons-vue'
 import { ElMessageBox } from 'element-plus'
 import { fetchDistrictAdjustNotice } from '@/api/biz'
 import { getAppConfig } from '@/utils/appConfig'
@@ -132,22 +204,90 @@ import {
   listVillageNames,
 } from '@/api/sjnmtybt/regionStore'
 
-/** 菜单图标解析:避免全局注册名冲突(如 Setting)导致不渲染 */
+interface OpenTab {
+  path: string
+  title: string
+}
+
 function menuIcon(name: string): Component {
   const map = Icons as Record<string, Component>
   return map[name] || map.Tools || map.Menu
 }
 
-const { systemName, systemLogoChar } = getAppConfig()
+const cfg = getAppConfig()
+const { systemName, systemTitle } = cfg
+const logoChar = (cfg.systemLogoChar || systemTitle || systemName || '退').slice(0, 1)
+const displayTitle = computed(() => systemTitle || systemName)
+
 const route = useRoute()
 const router = useRouter()
 const userStore = useUserStore()
 const isCollapse = ref(false)
+const scopeVisible = ref(false)
 const remoteTowns = ref<string[]>([])
 const remoteVillages = ref<string[]>([])
+const contentBodyRef = ref<HTMLElement | null>(null)
+const homePath = '/dashboard'
+const openedTabs = ref<OpenTab[]>([])
+
+/** 风格 B:数据展板不套外层白卡 */
+const isBoardPage = computed(() => route.path === homePath || route.path.startsWith('/dashboard'))
+
+function menuTitleByPath(path: string): string {
+  return MENU_LIST.find((m) => m.path === path)?.title || (route.meta.title as string) || '页面'
+}
+
+function ensureTab(path: string) {
+  if (!path || path === '/') return
+  if (openedTabs.value.some((t) => t.path === path)) return
+  openedTabs.value.push({ path, title: menuTitleByPath(path) })
+}
+
+function closeTab(path: string) {
+  const idx = openedTabs.value.findIndex((t) => t.path === path)
+  if (idx < 0) return
+  openedTabs.value.splice(idx, 1)
+  if (activeMenu.value === path) {
+    const fallback = openedTabs.value[idx - 1] || openedTabs.value[0]
+    router.push(fallback?.path || homePath)
+  }
+}
+
+function goHome() {
+  router.push(homePath)
+}
+
+/** 产品确认:浏览器刷新后回到首页 */
+function redirectHomeOnReload() {
+  try {
+    const nav = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming | undefined
+    const isReload =
+      nav?.type === 'reload' ||
+      (performance as Performance & { navigation?: { type?: number } }).navigation?.type === 1
+    if (isReload && route.path !== homePath) {
+      router.replace(homePath)
+    }
+  } catch {
+    /* ignore */
+  }
+}
+
+watch(
+  () => route.fullPath,
+  async () => {
+    ensureTab(route.path)
+    await nextTick()
+    const body = contentBodyRef.value
+    const surface = body?.querySelector('.content-surface') as HTMLElement | null
+    const stack = surface?.querySelector('.page-stack') as HTMLElement | null
+    stack?.scrollTo({ top: 0, left: 0 })
+    ;(surface || body)?.scrollTo({ top: 0, left: 0 })
+    window.scrollTo(0, 0)
+  },
+  { immediate: true },
+)
 
 const activeMenu = computed(() => route.path)
-const pageTitle = computed(() => (route.meta.title as string) || '')
 const roleInfo = computed(() => userStore.roleInfo)
 
 const useMock = () => getAppConfig().useMock
@@ -164,7 +304,6 @@ const villageOptions = computed(() => {
   return remoteVillages.value.length ? remoteVillages.value : listVillageNames(town)
 })
 
-/** 按当前角色权限过滤菜单(权限改 config 即可) */
 const visibleMenus = computed(() =>
   MENU_LIST.filter((m) => userStore.canAccess(m.path)),
 )
@@ -201,7 +340,6 @@ async function loadVillageOptions(town: string) {
 }
 
 function ensureDefaultScope() {
-  // 区级不强制街镇;村/镇级无已选时补默认
   if (
     userStore.townScopeRequired &&
     userStore.needTownScope &&
@@ -219,7 +357,6 @@ function ensureDefaultScope() {
   if (userStore.needVillageScope && userStore.scopeTown && villages.length) {
     const roleVillage = userStore.roles.find((r) => r.key === userStore.currentRole)?.village
     const current = userStore.scopeVillage
-    // 当前选中不在区划列表中(如历史写死的「同建村」)必须纠正
     if (!current || !villages.includes(current)) {
       const prefer =
         roleVillage && villages.includes(roleVillage) ? roleVillage : villages[0]
@@ -232,7 +369,7 @@ function onRoleChange(key: string) {
   userStore.switchRole(key)
   void loadVillageOptions(userStore.scopeTown || '').then(() => ensureDefaultScope())
   if (!userStore.canAccess(route.path)) {
-    router.push(userStore.allowedPaths[0] || '/dashboard')
+    router.push(userStore.allowedPaths[0] || homePath)
   }
   void maybeShowAdjustNotice()
 }
@@ -281,222 +418,516 @@ watch(
   },
 )
 
+/** 窄屏自动收起侧栏文案(CSS 也会强制 64px) */
+function syncCollapseByViewport() {
+  if (window.innerWidth <= 900) isCollapse.value = true
+}
+
 onMounted(async () => {
+  redirectHomeOnReload()
+  ensureTab(homePath)
+  ensureTab(route.path)
+  syncCollapseByViewport()
+  window.addEventListener('resize', syncCollapseByViewport)
   await loadTownOptions()
   if (userStore.scopeTown) await loadVillageOptions(userStore.scopeTown)
   ensureDefaultScope()
   void maybeShowAdjustNotice()
 })
+
+onUnmounted(() => {
+  window.removeEventListener('resize', syncCollapseByViewport)
+})
 </script>
 
 <style scoped lang="less">
-.main-layout {
-  height: 100%;
-  background: @page-bg;
-
-  &__aside {
-    display: flex;
-    flex-direction: column;
-    background: @aside-bg;
-    border-right: 1px solid @border-color;
-    transition: width 0.2s;
-    overflow: hidden;
+/* ========== 蓝湖氛围底:宽与页面一致,高度按比例自适应 ========== */
+.app-shell {
+  height: 100vh;
+  max-height: 100vh;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  background-color: #e8eef8;
+  background-image: url('/assets/bg/background.png');
+  background-repeat: no-repeat;
+  background-position: left top;
+  background-size: 100% auto;
+}
 
-    .logo {
-      flex-shrink: 0;
-      display: flex;
-      align-items: center;
-      min-height: @header-height;
-      padding: 10px 12px;
-      box-sizing: border-box;
-      background: @header-bg;
-      color: @text-primary;
-      border-bottom: 1px solid @border-color;
-
-      &.is-collapse {
-        justify-content: center;
-        padding: 8px 0;
-      }
+/* ========== 顶栏:透底,非白条工具栏 ========== */
+.brand-bar {
+  position: relative;
+  z-index: var(--z-shell-brand, 30);
+  flex-shrink: 0;
+  height: 56px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 30px 0 20px;
+  background: transparent;
+  border: 0;
+  box-shadow: none;
+}
 
-      &__mark {
-        width: 32px;
-        height: 32px;
-        border-radius: 50%;
-        background: @primary-color;
-        color: @text-white;
-        font-size: 16px;
-        font-weight: 700;
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        line-height: 1;
-      }
+.brand-left {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  min-width: 0;
+  cursor: pointer;
+}
 
-      &__title {
-        font-size: 13px;
-        font-weight: 700;
-        line-height: 1.35;
-        letter-spacing: 0.5px;
-        color: @primary-dark;
-        word-break: break-all;
-      }
-    }
+.brand-logo {
+  width: 36px;
+  height: 36px;
+  border-radius: 50%;
+  background: linear-gradient(145deg, @primary-hover, @primary-color);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+  box-shadow: 0 4px 12px rgba(47, 84, 235, 0.25);
+
+  &__char {
+    color: #fff;
+    font-size: 15px;
+    font-weight: 700;
+    line-height: 1;
   }
+}
 
-  &__menu-scroll {
-    flex: 1;
-    min-height: 0;
+.brand-title {
+  font-size: 18px;
+  font-weight: 700;
+  color: #1f1f1f;
+  letter-spacing: 0.02em;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.brand-right {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  flex-shrink: 0;
+}
+
+.header-chip {
+  min-width: 34px;
+  height: 34px;
+  padding: 0 8px;
+  border: 0;
+  background: rgba(255, 255, 255, 0.72);
+  color: #595959;
+  border-radius: 50%;
+  cursor: pointer;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  gap: 4px;
+  box-shadow: 0 2px 8px rgba(47, 84, 235, 0.08);
+  transition: color 0.15s, background 0.15s;
+
+  &:hover {
+    color: @primary-color;
+    background: #fff;
   }
 
-  &__menu {
-    border-right: none !important;
-    background: @aside-bg;
-    padding: 8px 10px;
+  &--msg {
+    border-radius: 17px;
+    padding: 0 12px 0 10px;
+  }
 
-    :deep(.el-menu-item) {
-      height: 42px;
-      line-height: 42px;
-      margin-bottom: 4px;
-      color: @text-regular;
-      font-size: 13px;
-      border-radius: @radius-md;
+  &__label {
+    font-size: 13px;
+    color: inherit;
+  }
+}
 
-      &:hover {
-        background-color: @el-primary-light-9 !important;
-        color: @primary-color;
-      }
+.header-badge {
+  display: inline-flex;
+  line-height: 1;
+}
 
-      &.is-active {
-        color: @text-white !important;
-        background-color: @primary-color !important;
-        border-right: none !important;
-      }
-    }
+.header-user {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  cursor: pointer;
+  color: #262626;
+  font-size: 13px;
+  outline: none;
+  padding: 2px 4px 2px 2px;
+  border-radius: 20px;
+  background: rgba(255, 255, 255, 0.55);
+
+  &__avatar {
+    width: 28px;
+    height: 28px;
+    border-radius: 50%;
+    background: rgba(47, 84, 235, 0.12);
+    color: @primary-color;
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
   }
 
-  &__right {
-    min-width: 0;
-    background: @page-bg;
+  &__name {
+    max-width: 96px;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
   }
 
-  &__header {
-    display: flex;
-    align-items: center;
-    justify-content: space-between;
-    background: @header-bg;
-    border-bottom: 1px solid @border-color;
-    padding: 0 12px 0 8px;
-    height: @header-height !important;
+  &__arrow {
+    font-size: 12px;
+    color: #8c8c8c;
+    margin-right: 4px;
   }
+}
 
-  &__header-left {
-    display: flex;
-    align-items: center;
-    gap: 10px;
-    min-width: 0;
+.scope-form {
+  display: grid;
+  gap: 14px;
 
-    .collapse-btn {
-      font-size: 18px;
-      cursor: pointer;
-      color: @text-regular;
-      padding: 4px;
+  &__row {
+    display: grid;
+    gap: 6px;
 
-      &:hover {
-        color: @primary-color;
-      }
+    label {
+      font-size: 12px;
+      color: #8c8c8c;
+    }
+  }
+}
+
+/* ========== 侧栏(浅白浮层)+ 主区 ========== */
+.shell-body {
+  flex: 1;
+  min-height: 0;
+  display: grid;
+  /* 侧栏 | 主区;列间距 30px 透出氛围底(蓝湖标注) */
+  grid-template-columns: auto minmax(0, 1fr);
+  column-gap: 30px;
+  grid-template-rows: 1fr;
+  padding: 0 0 0 16px;
+  overflow: hidden;
+}
+
+/* 侧栏:设计稿无白底浮层,直接透出氛围底;仅选中项蓝块 */
+.sidebar {
+  width: 200px;
+  background: transparent;
+  border: 0;
+  border-radius: 0;
+  box-shadow: none;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  transition: width 0.2s ease;
+  z-index: var(--z-shell-sidebar, 20);
+  margin-bottom: 30px;
+  backdrop-filter: none;
+
+  &.collapsed {
+    width: 64px;
+
+    .nav-text {
+      display: none;
     }
 
-    .header-page-title {
-      font-size: 15px;
-      font-weight: 600;
-      color: @text-primary;
-      white-space: nowrap;
+    .nav-item {
+      justify-content: center;
+      padding: 10px 0;
     }
 
-    .header-breadcrumb {
-      margin-left: 4px;
+    .sidebar-nav {
+      padding: 4px 8px 16px;
     }
   }
+}
 
-  &__header-right {
-    display: flex;
-    align-items: center;
-    gap: 8px;
-    flex-shrink: 0;
+.sidebar-toolbar {
+  height: 44px;
+  display: flex;
+  align-items: center;
+  padding: 0 14px;
+  flex-shrink: 0;
+}
 
-    .header-select {
-      &--role {
-        width: 168px;
-      }
-      &--town {
-        width: 120px;
-      }
-      &--village {
-        width: 120px;
-      }
+.sidebar-toggle {
+  min-width: 32px;
+  height: 32px;
+  border: 0;
+  background: transparent;
+  color: #595959;
+  border-radius: 6px;
+  cursor: pointer;
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  padding: 0 4px;
+
+  &__text {
+    font-size: 13px;
+    color: #595959;
+  }
+
+  &:hover {
+    background: rgba(255, 255, 255, 0.35);
+    color: #262626;
+  }
+}
+
+.sidebar-scrollbar {
+  flex: 1;
+  min-height: 0;
+  overflow: hidden;
+
+  :deep(.scrollbar-wrapper) {
+    overflow-x: hidden !important;
+  }
+
+  :deep(.el-scrollbar__bar.is-horizontal) {
+    display: none;
+  }
+}
+
+.sidebar-nav {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  padding: 4px 12px 20px;
+}
+
+.nav-item {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding: 10px 12px;
+  border-radius: 8px;
+  color: #595959;
+  text-decoration: none;
+  font-size: 14px;
+  font-weight: 500;
+  transition: background 0.15s, color 0.15s;
+
+  .nav-icon {
+    color: #8c8c8c;
+  }
+
+  &:hover {
+    background: rgba(255, 255, 255, 0.4);
+    color: #262626;
+    text-decoration: none;
+
+    .nav-icon {
+      color: #595959;
     }
+  }
 
-    .header-meta {
-      font-size: 12px;
-      color: @text-secondary;
-      max-width: 280px;
-      overflow: hidden;
-      text-overflow: ellipsis;
-      white-space: nowrap;
+  /* 仅选中项蓝块 */
+  &.active {
+    background: @primary-color;
+    color: #fff;
+    box-shadow: 0 4px 12px rgba(47, 84, 235, 0.28);
+
+    .nav-icon {
+      color: #fff;
     }
 
-    .header-icon {
-      font-size: 18px;
-      color: @text-regular;
-      cursor: pointer;
+    &:hover {
+      background: @primary-hover;
+      color: #fff;
 
-      &:hover {
-        color: @primary-color;
+      .nav-icon {
+        color: #fff;
       }
     }
+  }
+}
 
-    .header-icon-badge {
-      line-height: 1;
-      display: inline-flex;
-    }
+.nav-icon {
+  flex-shrink: 0;
+  width: 20px;
+  height: 20px;
+}
 
-    .header-user {
-      display: inline-flex;
-      align-items: center;
-      gap: 4px;
-      cursor: pointer;
-      color: @text-regular;
-      font-size: 13px;
-      outline: none;
-
-      &:hover {
-        color: @primary-color;
-      }
+.nav-text {
+  min-width: 0;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
 
-      &__name {
-        max-width: 90px;
-        overflow: hidden;
-        text-overflow: ellipsis;
-        white-space: nowrap;
-      }
+.main-wrap {
+  min-width: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  background: transparent;
+  padding-right: 30px;
+  padding-bottom: 30px;
+}
 
-      &__arrow {
-        font-size: 12px;
-      }
+/* 页签贴住下方白卡(文件夹页签);横滑隐藏滚动条避免撑出空隙 */
+.page-tabs {
+  flex-shrink: 0;
+  display: flex;
+  align-items: flex-end;
+  gap: 4px;
+  padding: 0;
+  margin: 0;
+  min-height: 34px;
+  overflow-x: auto;
+  overflow-y: hidden;
+  background: transparent;
+  /* 不占布局高度,避免页签与白卡之间露缝 */
+  scrollbar-width: none;
+
+  &::-webkit-scrollbar {
+    width: 0;
+    height: 0;
+    display: none;
+  }
+}
+
+.page-tab {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  height: 34px;
+  padding: 0 14px;
+  font-size: 13px;
+  color: #8c8c8c;
+  background: rgba(255, 255, 255, 0.55);
+  border: 0;
+  border-radius: 8px 8px 0 0;
+  cursor: pointer;
+  white-space: nowrap;
+  user-select: none;
+  position: relative;
+  z-index: var(--z-shell-tab, 2);
+
+  &:hover {
+    color: @primary-color;
+  }
+
+  &.active {
+    color: @primary-color;
+    font-weight: 600;
+    background: #fff;
+    box-shadow: none;
+    /* 压住白卡顶边 1px,视觉无缝(仅壳内层级,不影响 append-to-body 抽屉) */
+    margin-bottom: -1px;
+    padding-bottom: 1px;
+    z-index: calc(var(--z-shell-tab, 2) + 1);
+  }
+
+  &__close {
+    font-size: 12px;
+    color: #bfbfbf;
+    border-radius: 50%;
+
+    &:hover {
+      color: @danger-color;
+      background: @el-danger-light-9;
     }
   }
+}
+
+.page-tab.active .page-tab__close {
+  color: @primary-color;
+}
+
+.content-body {
+  flex: 1;
+  min-height: 0;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  margin-top: 0;
+}
 
-  &__main {
-    padding: @content-padding;
-    background: @page-bg;
-    overflow: auto;
-    display: flex;
-    flex-direction: column;
+.content-surface {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  background: #fff;
+  /* 左上与活动页签齐平,右上圆角 */
+  border-radius: 0 12px 12px 12px;
+  box-shadow: 0 8px 28px rgba(47, 84, 235, 0.1);
+  border: 0;
+  position: relative;
+  /* 不设 z-index,避免白卡成为层叠上下文把未 teleport 的弹层压在页签下 */
+
+  > .page-fill {
+    flex: 1 1 auto;
     min-height: 0;
+    height: 100%;
+    overflow: hidden;
+  }
 
-    > *:not(.page-fill) {
-      flex-shrink: 0;
+  /* 展板与列表同壳:白底圆角内容卡;差异在卡内布局/KPI */
+  &.is-board {
+    background: #fff;
+    box-shadow: 0 8px 28px rgba(47, 84, 235, 0.1);
+    border-radius: 0 12px 12px 12px;
+    overflow: hidden;
+  }
+}
+
+.content-body.is-board {
+  overflow: hidden;
+}
+
+/* 窄屏:保持左右布局,侧栏强制收起为图标栏(避免整列撑满) */
+@media (max-width: 900px) {
+  .shell-body {
+    column-gap: 12px;
+    padding: 0 0 0 8px;
+  }
+
+  .sidebar {
+    width: 64px !important;
+    margin-bottom: 16px;
+
+    .nav-text,
+    .sidebar-toggle__text {
+      display: none !important;
+    }
+
+    .nav-item {
+      justify-content: center;
+      padding: 10px 0;
+    }
+
+    .sidebar-nav {
+      padding: 4px 8px 16px;
+    }
+
+    .sidebar-toolbar {
+      padding: 0 8px;
+      justify-content: center;
     }
   }
+
+  .main-wrap {
+    padding-right: 8px;
+    padding-bottom: 12px;
+    min-width: 0;
+  }
+
+  .content-surface {
+    border-radius: 12px;
+  }
+
+  .brand-title {
+    font-size: 15px;
+  }
 }
 </style>

+ 611 - 121
src/styles/index.less

@@ -5,13 +5,39 @@
 }
 
 :root {
-  /* 松江政务浅蓝行政台 — 全局 CSS 变量 */
+  /* 蓝湖统一风格 Token:浅蓝氛围底 + 主蓝;侧栏浅白非蓝墙 */
+  --blue-900: #1d39c4;
+  --blue-700: #2f54eb;
+  --blue-600: #597ef7;
+  --blue-100: #f0f5ff;
+  --bg: #e8eef8;
+  --card: #ffffff;
+  --text: #262626;
+  --muted: #8c8c8c;
+  --line: #e8e8e8;
+
+  /*
+   * 全站层级(低 → 高)
+   * 壳层页签/侧栏 < 内容 < 下拉/气泡 < 抽屉/对话框遮罩 < 消息
+   * 抽屉/对话框必须 append-to-body,避免被 .content-surface overflow 裁切
+   */
+  --z-shell-content: 1;
+  --z-shell-tab: 2;
+  --z-shell-sidebar: 20;
+  --z-shell-brand: 30;
+  --z-popper: 2000;
+  --z-overlay: 3000;
+  --z-message: 4000;
+
+  --el-index-popper: var(--z-popper);
+  --el-index-normal: 1;
+
   --color-primary: @primary-color;
   --color-primary-hover: @primary-hover;
   --color-primary-active: @primary-active;
   --color-brand-gold: @brand-gold;
   --color-brand-gold-deep: @brand-gold-deep;
-  --color-header-bg: @aside-logo-bg;
+  --color-header-bg: @header-bg;
   --color-block-bar: @primary-color;
   --color-input-editable: @input-editable-bg;
   --color-table-selected: @table-row-selected;
@@ -24,9 +50,15 @@
   --radius-sm: @radius-sm;
   --radius-md: @radius-md;
   --radius-card: @radius-card;
-  --ui-shadow-card: 0 2px 8px rgba(0, 0, 0, 0.08);
-
-  /* Element Plus 主题覆盖(政务蓝) */
+  --ui-shadow-card: 0 1px 4px rgba(0, 0, 0, 0.06);
+  --ui-chart-blue: @chart-blue;
+  --ui-chart-green: @chart-green;
+  --ui-chart-orange: @chart-orange;
+  --ui-chart-purple: @chart-purple;
+  --ui-chart-axis: #f0f0f0;
+  --ui-chart-label: #8c8c8c;
+
+  /* Element Plus 主题 */
   --el-color-primary: @primary-color;
   --el-color-primary-light-3: @el-primary-light-3;
   --el-color-primary-light-5: @el-primary-light-5;
@@ -36,8 +68,11 @@
   --el-color-primary-dark-2: @el-primary-dark-2;
   --el-border-radius-base: @radius-md;
   --el-border-radius-small: @radius-sm;
+  --el-border-radius-round: 20px;
   --el-font-size-base: 13px;
   --el-component-size: 32px;
+  --el-component-size-small: 32px;
+  --el-font-size-small: 13px;
   --el-border-color: @border-color;
   --el-text-color-primary: @text-primary;
   --el-text-color-regular: @text-regular;
@@ -75,11 +110,14 @@ body,
 #app {
   width: 100%;
   height: 100%;
-  font-family: 'Microsoft YaHei', '微软雅黑', 'PingFang SC', 'Hiragino Sans GB',
-    'Helvetica Neue', Arial, sans-serif;
+  overflow: hidden;
+  font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif;
   font-size: 13px;
-  background: @page-bg;
+  line-height: 1.55;
   color: @text-primary;
+  background: @page-bg;
+  -webkit-font-smoothing: antialiased;
+  text-rendering: optimizeLegibility;
 }
 
 a {
@@ -91,39 +129,45 @@ a {
   }
 }
 
-/* ========== 区块标题:左蓝竖条 + 底部分割线 ========== */
+/* ========== 区块标题 ========== */
+@panel-title-height: 36px;
+
 .block-title {
   display: flex;
   align-items: center;
   gap: 8px;
-  margin: 0 0 12px;
-  padding: 0 0 8px;
-  font-size: 14px;
+  box-sizing: border-box;
+  min-height: @panel-title-height;
+  margin: 0 0 16px;
+  padding: 0;
+  font-size: 16px;
   font-weight: 600;
   color: @text-primary;
   line-height: 1.4;
-  border-bottom: 1px solid @border-light;
+  border-bottom: none;
 
+  /* 设计稿页面名无左侧竖条,去掉装饰条更贴近稿 */
   &::before {
-    content: '';
-    flex-shrink: 0;
-    width: 3px;
-    height: 14px;
-    background: @primary-color;
-    border-radius: 1px;
+    display: none;
   }
 }
 
-/* ========== 折叠区块头 ========== */
+.content-panel > .block-title,
+.content-panel > .page-panel__title {
+  min-height: @panel-title-height;
+  /* 设计稿:页面名与筛选区间距约 20px */
+  margin-bottom: 20px;
+}
+
 .collapse-panel-header {
   display: flex;
   align-items: center;
   justify-content: space-between;
   padding: 8px 12px;
-  background: linear-gradient(90deg, #eef4ff 0%, #f7f9fc 100%);
-  border: 1px solid @border-light;
+  background: @table-header-bg;
+  border: 1px solid @border-color;
   border-left: 3px solid @primary-color;
-  border-radius: 0 @radius-sm @radius-sm 0;
+  border-radius: @radius-md;
   font-size: 13px;
   font-weight: 600;
   color: @text-primary;
@@ -140,29 +184,145 @@ a {
   }
 }
 
-/* ========== 查询区 ========== */
-.query-panel {
+/* ========== 查询区 / 内容面板 ========== */
+.query-panel,
+.content-panel {
   background: @block-bg;
-  border: 1px solid @border-light;
+  border: 1px solid @border-color;
   border-radius: @radius-card;
-  box-shadow: 0 1px 4px rgba(43, 115, 248, 0.04);
-  padding: 12px 12px 4px;
-  margin-bottom: 8px;
+  box-shadow: var(--ui-shadow-card);
+  padding: 16px 20px;
+  margin-bottom: 12px;
+}
+
+/*
+ * 设计稿:壳层已有一整块白内容卡(.content-surface)。
+ * 列表页 PagePanel 去边框阴影,避免双重卡片;
+ * 看板等多面板页保留轻分割。
+ */
+.content-surface {
+  > .page-fill > .content-panel,
+  > .content-panel {
+    background: transparent;
+    border: 0;
+    border-radius: 0;
+    box-shadow: none;
+    margin-bottom: 0;
+  }
+
+  > .page-fill > .content-panel + .content-panel {
+    border-top: 1px solid @border-light;
+    margin-top: 0;
+  }
+
+  > .page-stack {
+    flex: 1 1 auto;
+    min-height: 0;
+    overflow: auto;
+    /* 蓝湖:左右约 30px、页名上约 20px */
+    padding: @content-padding-top @content-padding-x 24px;
+    box-sizing: border-box;
+
+    /* 风格 A:白卡内区块轻分割 */
+    > .content-panel {
+      box-shadow: none;
+      border: 1px solid @border-light;
+      background: #fff;
+      border-radius: 8px;
+    }
+
+    > .content-panel > .block-title,
+    > .content-panel > .page-panel__title {
+      color: #262626;
+      font-size: 16px;
+      font-weight: 700;
+      margin-bottom: 16px;
+    }
+  }
+
+  /* 风格 B:与列表同白壳;卡内区块轻边框分割(非透底浮层) */
+  &.is-board > .page-stack {
+    padding: @content-padding-top @content-padding-x 24px;
+    overflow: auto;
+
+    > .content-panel {
+      border: 1px solid @border-light;
+      box-shadow: none;
+      background: #fff;
+      border-radius: 8px;
+    }
+
+    > .dashboard-view {
+      gap: 14px;
+    }
+  }
+}
+
+/* 内容区滚动条:细轨、圆角,贴白卡内侧(列表/展板共用) */
+.content-surface > .page-stack,
+.content-surface > .page-fill {
+  scrollbar-width: thin;
+  scrollbar-color: rgba(47, 84, 235, 0.35) transparent;
+
+  &::-webkit-scrollbar {
+    width: 6px;
+    height: 6px;
+  }
+
+  &::-webkit-scrollbar-track {
+    background: transparent;
+    margin: 4px 0;
+  }
+
+  &::-webkit-scrollbar-thumb {
+    background: rgba(47, 84, 235, 0.28);
+    border-radius: 6px;
+    border: 1px solid transparent;
+    background-clip: padding-box;
+  }
+
+  &::-webkit-scrollbar-thumb:hover {
+    background: rgba(47, 84, 235, 0.45);
+    background-clip: padding-box;
+  }
+}
+
+.query-panel {
+  padding-bottom: 8px;
 
   > .block-title {
     margin-bottom: 16px;
   }
 
   .el-form-item {
-    margin-bottom: 12px;
+    margin-bottom: 10px;
   }
 
   .el-form-item__label {
-    color: @text-regular;
+    color: @text-secondary;
     font-weight: 400;
+    font-size: 12px;
+    padding: 0 8px 0 0;
+  }
+
+  .el-input__wrapper,
+  .el-select__wrapper,
+  .el-date-editor.el-input__wrapper {
+    min-height: 32px !important;
+    height: 32px !important;
+  }
+
+  .el-button {
+    min-height: 32px !important;
+    height: 32px !important;
+    padding: 0 16px !important;
+    font-size: 13px !important;
+  }
+
+  .el-button + .el-button {
+    margin-left: 8px;
   }
 
-  /* 查询/重置:与同列控件内容区左对齐(保留 label 占位) */
   .query-form-actions {
     .el-form-item__content {
       flex-wrap: nowrap;
@@ -171,36 +331,95 @@ a {
   }
 }
 
-/* 行内筛选:控件与按钮垂直居中对齐 */
+/* 嵌在内容卡内的筛选条:轻底,贴近设计稿检索行 */
+.content-panel .query-panel {
+  background: #f7f9fc;
+  box-shadow: none;
+  border: 1px solid @border-light;
+  border-radius: @radius-md;
+  padding: 12px 14px 2px;
+  margin-bottom: 12px;
+}
+
 .el-form--inline {
   display: flex;
   flex-wrap: wrap;
-  align-items: center;
-  column-gap: 4px;
+  align-items: flex-end;
+  column-gap: 12px;
   row-gap: 0;
 
   .el-form-item {
-    margin-right: 12px;
+    display: inline-flex;
+    align-items: center;
+    margin-right: 0;
     margin-bottom: 8px;
     vertical-align: middle;
 
     .el-form-item__content {
+      display: inline-flex;
       align-items: center;
+      flex-wrap: nowrap;
+      gap: 8px;
+      line-height: normal;
+    }
+
+    .el-form-item__label {
+      float: none;
+      display: inline-flex;
+      align-items: center;
+      height: auto;
+      line-height: 1.4;
+      padding-right: 8px;
     }
   }
 }
 
-/* ========== 内容面板 ========== */
-.content-panel {
-  background: @block-bg;
-  border: 1px solid @border-light;
-  border-radius: @radius-card;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
-  padding: 12px;
-  margin-bottom: 12px;
+/* 表格操作列:左对齐链接(设计稿「查看详情」) */
+.table-ops {
+  display: inline-flex;
+  flex-wrap: wrap;
+  justify-content: flex-start;
+  align-items: center;
+  gap: 2px 12px;
+  max-width: 100%;
+  line-height: 1.4;
+
+  .el-button + .el-button {
+    margin-left: 0 !important;
+  }
+
+  .el-button.is-link {
+    padding: 0 2px;
+    height: auto;
+    min-height: 0;
+  }
+}
+
+.inline-search {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 8px;
+  margin-bottom: 8px;
+
+  .el-input__wrapper,
+  .el-select__wrapper {
+    min-height: 32px !important;
+    height: 32px !important;
+  }
+
+  .el-button {
+    min-height: 32px !important;
+    height: 32px !important;
+    padding: 0 16px !important;
+  }
+
+  .el-button + .el-button {
+    margin-left: 0 !important;
+  }
 }
 
-/* ========== 列表页铺满主内容区,避免表下大块留白 ========== */
+/* ========== 列表页铺满 ========== */
 .page-fill {
   flex: 1 1 auto;
   min-height: 0;
@@ -216,14 +435,15 @@ a {
     display: flex;
     flex-direction: column;
     overflow: hidden;
-    padding: 10px 12px;
+    /* 蓝湖白卡内边距:上 20 / 左右 30 */
+    padding: @content-padding-top @content-padding-x 16px;
   }
 
   > .content-panel + .content-panel {
     flex: 0 1 auto;
     max-height: 240px;
     overflow: auto;
-    margin-top: 8px;
+    margin-top: 12px;
   }
 
   > .content-panel > .el-alert,
@@ -236,15 +456,11 @@ a {
     margin-bottom: 16px;
   }
 
-  > .content-panel > .el-alert + .el-form {
-    margin-top: 0;
-  }
-
   .page-panel__title,
   .block-title {
     flex-shrink: 0;
-    margin-bottom: 8px;
-    padding-bottom: 6px;
+    min-height: @panel-title-height;
+    margin-bottom: 20px;
   }
 
   .el-tabs__header {
@@ -252,24 +468,18 @@ a {
     margin-bottom: 8px;
   }
 
+  /* 设计稿:分页固定在底部 */
   .table-pagination {
     flex-shrink: 0;
-    margin-top: 6px;
-    padding-top: 4px;
+    margin-top: 12px;
   }
 
   .query-panel {
     flex-shrink: 0;
-    margin-bottom: 8px;
-    padding: 12px 12px 4px;
-
-    .el-form-item {
-      margin-bottom: 12px;
-    }
+    margin-bottom: 12px;
   }
 }
 
-/* 表格在 Tab 内:让 tabs/content/pane 一起撑满 */
 .page-fill--tabs {
   > .content-panel > .el-tabs {
     flex: 1;
@@ -303,10 +513,71 @@ a {
   }
 }
 
-/* 多区块页:至少铺满视口,内容超出时由主区滚动 */
+/* 蓝湖风格 A:表上方状态分类 Tab(文字 + 底边) */
+.style-a-tabs {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 4px 20px;
+  margin-bottom: 12px;
+  border-bottom: 1px solid @border-light;
+}
+
+.style-a-tab {
+  position: relative;
+  padding: 8px 0 10px;
+  border: 0;
+  background: transparent;
+  color: @text-regular;
+  font-size: 14px;
+  cursor: pointer;
+  line-height: 1.4;
+
+  &:hover {
+    color: @primary-color;
+  }
+
+  &.is-active {
+    color: @primary-color;
+    font-weight: 600;
+
+    &::after {
+      content: '';
+      position: absolute;
+      left: 0;
+      right: 0;
+      bottom: -1px;
+      height: 2px;
+      background: @primary-color;
+      border-radius: 1px;
+    }
+  }
+}
+
+/* 风格 A:表头/单元格左对齐(序号、勾选列除外) */
+.content-surface .el-table {
+  .el-table__header th.el-table__cell {
+    text-align: left;
+  }
+
+  .el-table__body td.el-table__cell {
+    text-align: left;
+  }
+
+  .el-table__header th.is-center,
+  .el-table__body td.is-center {
+    text-align: center;
+  }
+
+  .el-table__header th.is-right,
+  .el-table__body td.is-right {
+    text-align: right;
+  }
+}
+
 .page-stack {
-  flex: 1 0 auto;
-  min-height: calc(100vh - @header-height - @content-padding * 2);
+  flex: 1 1 auto;
+  min-height: 0;
 }
 
 .page-tip {
@@ -317,6 +588,11 @@ a {
   color: @text-secondary;
 }
 
+.page-tip,
+.dashboard-stat-note {
+  display: none !important;
+}
+
 .page-alert {
   flex-shrink: 0;
   margin-bottom: 16px;
@@ -335,13 +611,11 @@ a {
   }
 }
 
-/* 提示条与下方筛选表单之间强制留白(避免 margin 折叠) */
 .page-alert + .el-form,
 .page-alert + .query-panel {
   margin-top: 4px;
 }
 
-/* ========== 页面底部居中主操作 ========== */
 .page-actions {
   display: flex;
   justify-content: center;
@@ -356,7 +630,6 @@ a {
   }
 }
 
-/* ========== 可编辑输入浅黄底 ========== */
 .input-editable {
   .el-input__wrapper,
   .el-textarea__inner,
@@ -372,7 +645,75 @@ a {
   background-color: @input-editable-bg !important;
 }
 
-/* ========== 主按钮:政务蓝实心 ========== */
+/* ========== 输入 / 下拉 ========== */
+.el-input__wrapper,
+.el-select__wrapper {
+  min-height: var(--el-component-size, 32px);
+  border-radius: @radius-sm !important;
+  border: 1px solid @border-color !important;
+  background: #fff !important;
+  box-shadow: none !important;
+  padding: 0 11px !important;
+  transition:
+    border-color 0.15s,
+    box-shadow 0.15s !important;
+}
+
+.el-input--small .el-input__wrapper,
+.el-select--small .el-select__wrapper {
+  min-height: var(--el-component-size-small, 32px);
+}
+
+.el-textarea__inner {
+  border-radius: @radius-sm !important;
+  border: 1px solid @border-color !important;
+  background: #fff !important;
+  box-shadow: none !important;
+}
+
+.el-input__wrapper:hover,
+.el-select__wrapper:hover {
+  border-color: @primary-color !important;
+  box-shadow: none !important;
+}
+
+.el-input__wrapper.is-focus,
+.el-select__wrapper.is-focused {
+  border-color: @primary-color !important;
+  box-shadow: 0 0 0 2px rgba(47, 84, 235, 0.12) !important;
+}
+
+.el-input__inner {
+  font-size: 13px !important;
+  color: @text-primary !important;
+}
+
+.el-input__inner::placeholder,
+.el-select__placeholder {
+  color: @text-placeholder !important;
+  font-size: 13px !important;
+}
+
+.el-select__selected-item {
+  font-size: 13px !important;
+  color: @text-primary !important;
+}
+
+.el-input__wrapper .el-icon,
+.el-select__caret {
+  color: @text-secondary !important;
+}
+
+/* ========== 按钮 ========== */
+.el-button {
+  font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif !important;
+  border-radius: @radius-sm;
+  font-size: 13px;
+  transition:
+    background-color 0.15s ease,
+    border-color 0.15s ease !important;
+}
+
 .el-button--primary {
   --el-button-bg-color: @primary-color;
   --el-button-border-color: @primary-color;
@@ -383,7 +724,6 @@ a {
   background: @primary-color;
   border-color: @primary-color;
   color: @text-white;
-  border-radius: @radius-sm;
   font-weight: 500;
 
   &:hover,
@@ -399,34 +739,57 @@ a {
   }
 }
 
-.el-button {
-  border-radius: @radius-sm;
+.el-button--danger {
+  --el-button-bg-color: @danger-color;
+  --el-button-border-color: @danger-color;
 }
 
-/* ========== 表格:浅灰表头、细边框、选中淡蓝 ========== */
+/* ========== 表格(左对齐 / 斑马纹 / 浅灰表头) ========== */
 .el-table {
   --el-table-header-bg-color: @table-header-bg;
   --el-table-row-hover-bg-color: @table-row-hover;
   --el-table-current-row-bg-color: @table-row-selected;
   --el-table-border-color: @border-color;
+  --el-table-text-color: @text-primary;
+  --el-table-header-text-color: @table-header-color;
   font-size: 13px;
+  color: @text-primary;
 
   th.el-table__cell {
     background: @table-header-bg !important;
-    color: @text-primary;
+    color: @table-header-color;
     font-weight: 600;
-    height: 36px;
-    padding: 4px 0;
+    height: @table-header-height;
+    padding: 10px 0;
+    font-size: 13px;
+    text-align: left;
   }
 
   td.el-table__cell {
-    padding: 4px 0;
-    height: 34px;
+    padding: 10px 0;
+    height: @table-row-height;
+    color: @text-primary;
+    text-align: left;
+  }
+
+  .el-table__cell .cell {
+    line-height: 1.45;
+    padding-left: 12px;
+    padding-right: 12px;
+    text-align: left;
   }
 
   .el-table__body tr.current-row > td.el-table__cell {
     background-color: @table-row-selected !important;
   }
+
+  .el-table__empty-text {
+    color: @text-secondary;
+  }
+}
+
+.el-table--striped .el-table__body tr.el-table__row--striped td.el-table__cell {
+  background: @table-stripe-bg;
 }
 
 /* ========== Tabs ========== */
@@ -445,43 +808,187 @@ a {
 
 /* ========== Pagination ========== */
 .el-pagination {
-  margin-top: 10px;
+  margin-top: 12px;
   justify-content: flex-end;
   font-size: 13px;
 
+  &.is-background .el-pager li {
+    background-color: #fff;
+    border: 1px solid @border-color;
+    border-radius: @radius-sm;
+    font-weight: 400;
+    min-width: 28px;
+    height: 28px;
+    line-height: 26px;
+  }
+
+  &.is-background .el-pager li.is-active {
+    background-color: @primary-color !important;
+    border-color: @primary-color !important;
+    color: @text-white !important;
+  }
+
   .el-pager li.is-active {
     background-color: @primary-color !important;
     color: @text-white !important;
   }
 }
 
-/* ========== Menu 侧栏激活态(实心蓝圆角 + 白字) ========== */
-.el-menu {
-  border-right: none !important;
+/* ========== Dialog / Drawer(白底头,非蓝渐变) ========== */
+.el-dialog {
+  border-radius: @radius-card !important;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  max-height: calc(100vh - 48px);
+  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
+
+  .el-dialog__header {
+    flex-shrink: 0;
+    margin-right: 0;
+    padding: 14px 20px;
+    background: #fff;
+    border-bottom: 1px solid @border-light;
+  }
+
+  .el-dialog__title {
+    color: @text-primary;
+    font-size: 15px;
+    font-weight: 600;
+    line-height: 1.4;
+  }
+
+  .el-dialog__headerbtn {
+    top: 14px;
+    right: 14px;
+    width: 28px;
+    height: 28px;
+
+    .el-dialog__close {
+      color: @text-secondary;
+      font-size: 16px;
+
+      &:hover {
+        color: @primary-color;
+      }
+    }
+  }
+
+  .el-dialog__body {
+    flex: 1 1 auto;
+    min-height: 0;
+    overflow-x: hidden;
+    overflow-y: auto;
+    padding: 18px 20px;
+    color: @text-primary;
+    font-size: 13px;
+  }
+
+  .el-dialog__footer {
+    flex-shrink: 0;
+    padding: 10px 20px 16px;
+    border-top: 1px solid @border-light;
+    background: #fff;
+  }
 }
 
-.el-menu-item.is-active {
-  color: @text-white !important;
-  background-color: @primary-color !important;
-  border-right: none !important;
-  border-radius: @radius-md;
+.el-overlay-dialog .el-dialog {
+  margin-top: 24px !important;
+  margin-bottom: 24px !important;
 }
 
-.el-sub-menu.is-active > .el-sub-menu__title {
-  color: @primary-color !important;
+/* 遮罩/抽屉/对话框:高于壳层页签与侧栏(须配合 append-to-body) */
+.el-overlay {
+  z-index: var(--z-overlay) !important;
 }
 
-/* ========== 面包屑 ========== */
-.el-breadcrumb__inner {
-  color: @text-secondary;
-  font-weight: 400 !important;
+.el-drawer {
+  border-radius: @radius-card 0 0 @radius-card;
+
+  .el-drawer__header {
+    margin-bottom: 0;
+    padding: 14px 20px;
+    background: #fff;
+    border-bottom: 1px solid @border-light;
+    color: @text-primary;
+  }
+
+  .el-drawer__title {
+    color: @text-primary;
+    font-size: 15px;
+    font-weight: 600;
+    line-height: 1.4;
+  }
+
+  .el-drawer__close-btn {
+    color: @text-secondary;
+
+    &:hover {
+      color: @primary-color;
+    }
+  }
+
+  .el-drawer__body {
+    padding: 16px 20px 20px;
+    color: @text-primary;
+    font-size: 13px;
+  }
+
+  .el-drawer__footer {
+    padding: 10px 20px 16px;
+    border-top: 1px solid @border-light;
+    background: #fff;
+  }
 }
 
-.el-breadcrumb__item:last-child .el-breadcrumb__inner {
-  color: @text-primary;
+/* 左树右表:同一白卡、竖线分割(蓝湖 choose_style) */
+.style-a-split {
+  display: flex;
+  min-height: 360px;
+  background: #fff;
+  border: 1px solid @border-light;
+  border-radius: 8px;
+  overflow: hidden;
+
+  &__aside {
+    flex: 0 0 260px;
+    width: 260px;
+    min-width: 200px;
+    max-width: 320px;
+    padding: 12px;
+    overflow: auto;
+    border-right: 1px solid @border-light;
+    background: #fff;
+    box-sizing: border-box;
+  }
+
+  &__main {
+    flex: 1 1 auto;
+    min-width: 0;
+    padding: 16px 16px 12px;
+    overflow: auto;
+    background: #fff;
+    box-sizing: border-box;
+  }
+
+  &__main-head {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    gap: 12px;
+    min-height: 36px;
+    margin-bottom: 12px;
+  }
+
+  &__main-title {
+    font-size: 16px;
+    font-weight: 700;
+    color: #262626;
+    line-height: 1.4;
+  }
 }
 
-/* ========== Collapse 折叠面板 ========== */
+/* ========== Collapse ========== */
 .el-collapse {
   border: none;
 
@@ -489,7 +996,7 @@ a {
     height: 40px;
     line-height: 40px;
     padding: 0 12px;
-    background: linear-gradient(90deg, #eef4ff 0%, #f7f9fc 100%);
+    background: @table-header-bg;
     border: 1px solid @border-light;
     border-left: 3px solid @primary-color;
     border-radius: 0 @radius-sm @radius-sm 0;
@@ -516,34 +1023,17 @@ a {
   }
 }
 
-/* ========== 工号水印占位(可选) ========== */
-.page-watermark {
-  position: relative;
+.el-breadcrumb__inner {
+  color: @text-secondary;
+  font-weight: 400 !important;
+}
 
-  &::after {
-    content: attr(data-watermark);
-    pointer-events: none;
-    position: absolute;
-    inset: 0;
-    z-index: 0;
-    opacity: 0.06;
-    font-size: 14px;
-    color: @text-secondary;
-    background-image: repeating-linear-gradient(
-      -30deg,
-      transparent,
-      transparent 80px,
-      currentColor 80px,
-      currentColor 81px
-    );
-    /* 实际水印文字由业务页用 el-watermark 或自定义实现 */
-  }
+.el-breadcrumb__item:last-child .el-breadcrumb__inner {
+  color: @text-primary;
 }
 
-/* ========== Message 提示(函数式调用样式兜底) ========== */
 .el-message {
-  z-index: 4000 !important;
+  z-index: var(--z-message) !important;
   min-width: 280px;
   box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
 }
-

+ 78 - 67
src/styles/variables.less

@@ -1,85 +1,96 @@
-// ========== 松江政务浅蓝行政台主题 ==========
-// 对齐 界面UI风格/UI风格分析与Prompt.md(主色 #2B73F8 / #1890FF)
-@primary-color: #2b73f8;
-@primary-hover: #4a8aff;
-@primary-active: #1f5fd1;
-@primary-dark: #1a56c4;
-// 兼容旧变量名(历史 brand-gold 引用统一落到主色系,避免残留金色)
-@brand-gold: #5b8ff9;
-@brand-gold-deep: #1890ff;
+// ========== 蓝湖「基层减负统一风格」Token(P0 壳层)==========
+// 禁止整列蓝侧栏;主蓝仅用于选中项/按钮/链接等
 
-// 功能色
-@success-color: #27c98e;
-@warning-color: #fa8c16;
+@primary-color: #2f54eb;
+@primary-hover: #597ef7;
+@primary-active: #1d39c4;
+@primary-dark: #1d39c4;
+
+// 兼容旧变量名
+@brand-gold: #597ef7;
+@brand-gold-deep: #2f54eb;
+
+@success-color: #52c41a;
+@warning-color: #faad14;
 @danger-color: #f5222d;
-@info-color: #8a9099;
-@link-color: #2b73f8;
+@info-color: #8c8c8c;
+@link-color: #2f54eb;
+@chart-blue: #2f54eb;
+@chart-green: #52c41a;
+@chart-orange: #faad14;
+@chart-purple: #722ed1;
 
-// 布局尺寸
-@header-height: 48px;
+// 布局(蓝湖标注:左右约 30px、页名上约 20px)
+@header-height: 56px;
+@brand-bar-height: 64px;
 @aside-width: 220px;
 @aside-collapsed-width: 64px;
-@content-padding: 12px;
+@content-padding-x: 30px;
+@content-padding-top: 20px;
+@content-padding: 20px;
 
-// 背景
-@page-bg: #eef3f9;
-@page-bg-alt: #f5f7fa;
-@aside-bg: #f5f7fa;
-@aside-logo-bg: #2b73f8;
-@header-bg: #ffffff;
+// 氛围底浅蓝;侧栏/顶栏浅白(非蓝墙)
+@page-bg: #e8eef8;
+@page-bg-top: #dce8f8;
+@page-bg-mid: #e4ecf7;
+@page-bg-alt: #f5f8fc;
+/* 侧栏透出氛围底(设计稿无白墙);顶栏亦透明 */
+@aside-bg: transparent;
+@aside-bg-start: transparent;
+@aside-bg-end: transparent;
+@aside-logo-bg: transparent;
+@aside-border: transparent;
+@header-bg: transparent;
 @block-bg: #ffffff;
 
-// 文字
-@text-primary: #1f2329;
+@text-primary: #262626;
 @text-regular: #595959;
-@text-secondary: #8a9099;
+@text-secondary: #8c8c8c;
 @text-white: #ffffff;
+@text-placeholder: #bfbfbf;
 
-// 边框 / 分割
-@border-color: #e5e8ef;
-@border-light: #eef1f6;
-@divider-gold: #d6e4ff; // 历史名:区块分割线,现为浅蓝
+@border-color: #e8e8e8;
+@border-light: #f0f0f0;
+@divider-gold: #e8e8e8;
 
-// 表格
-@table-header-bg: #f5f7fa;
-@table-stripe-bg: #fafbfc;
-@table-row-hover: #f5f7fa;
-@table-row-selected: #e8f1ff;
+@table-header-bg: #fafafa;
+@table-header-color: #262626;
+@table-stripe-bg: #fafafa;
+@table-row-hover: #e6f4ff;
+@table-row-selected: #e6f4ff;
+@table-row-height: 48px;
+@table-header-height: 44px;
 
-// 可编辑输入浅蓝底(原浅黄,随主色调整)
-@input-editable-bg: #f0f7ff;
+@input-editable-bg: #fafafa;
 
-// 圆角(卡片略柔和,控件略收)
 @radius-sm: 4px;
 @radius-md: 6px;
-@radius-card: 8px;
+@radius-card: 10px;
 
-// Element Plus 主色衍生
-@el-primary-light-3: #6b9cfa;
-@el-primary-light-5: #95b8fc;
-@el-primary-light-7: #bfd4fd;
-@el-primary-light-8: #d4e3fe;
-@el-primary-light-9: #e8f1ff;
-@el-primary-dark-2: #2260d0;
+@el-primary-light-3: #597ef7;
+@el-primary-light-5: #85a5ff;
+@el-primary-light-7: #adc6ff;
+@el-primary-light-8: #d6e4ff;
+@el-primary-light-9: #f0f5ff;
+@el-primary-dark-2: #1d39c4;
 
-// 功能色浅色阶(Message / Tag 背景依赖)
-@el-success-light-3: #6ed9ae;
-@el-success-light-5: #95e4c4;
-@el-success-light-7: #bcefd9;
-@el-success-light-8: #d0f5e5;
-@el-success-light-9: #e8faf2;
-@el-warning-light-3: #fcb35b;
-@el-warning-light-5: #fdc98b;
-@el-warning-light-7: #fedebb;
-@el-warning-light-8: #fee8d0;
-@el-warning-light-9: #fff4e8;
-@el-danger-light-3: #f86a71;
-@el-danger-light-5: #fa979c;
-@el-danger-light-7: #fcc4c7;
-@el-danger-light-8: #fdd8da;
-@el-danger-light-9: #feecee;
-@el-info-light-3: #adadad;
-@el-info-light-5: #c4c4c4;
-@el-info-light-7: #dbdbdb;
-@el-info-light-8: #e7e7e7;
-@el-info-light-9: #f3f3f3;
+@el-success-light-3: #73d13d;
+@el-success-light-5: #95de64;
+@el-success-light-7: #b7eb8f;
+@el-success-light-8: #d9f7be;
+@el-success-light-9: #f6ffed;
+@el-warning-light-3: #ffc53d;
+@el-warning-light-5: #ffd666;
+@el-warning-light-7: #ffe58f;
+@el-warning-light-8: #fff1b8;
+@el-warning-light-9: #fffbe6;
+@el-danger-light-3: #ff4d4f;
+@el-danger-light-5: #ff7875;
+@el-danger-light-7: #ffa39e;
+@el-danger-light-8: #ffccc7;
+@el-danger-light-9: #fff1f0;
+@el-info-light-3: #a6a6a6;
+@el-info-light-5: #bfbfbf;
+@el-info-light-7: #d9d9d9;
+@el-info-light-8: #f0f0f0;
+@el-info-light-9: #fafafa;

+ 1 - 1
src/utils/appConfig.ts

@@ -44,7 +44,7 @@ const fallbackConfig: AppConfig = {
   systemLogoChar: '松',
   systemTitle: '农民土地退养补助金',
   systemSubtitle: '发放管理平台',
-  requestTimeout: 15000,
+  requestTimeout: 60000,
   apiBase: '/api',
   useMock: false,
   currentRole: 'social',

+ 55 - 39
src/views/admin/AdminBatchesView.vue

@@ -1,47 +1,42 @@
 <template>
   <div class="page-fill admin-batches-view">
     <PagePanel title="批次管理">
-      <template #extra>
-        <el-button type="warning" size="small" :loading="generating" @click="onGenerateDistrict(false)">
-          生成/重跑本月全区批次
-        </el-button>
-        <el-button type="danger" size="small" :loading="generating" @click="onGenerateDistrict(true)">
-          联调:按15号纳入新增
-        </el-button>
-      </template>
       <p class="page-tip">
         每月 15 号生成一份全区月批,各镇是统计切片(不是一村一批)。村/镇角色在各自页面按范围看人,不单独建村级批次。联调测本月发放请点「按15号纳入新增」。
       </p>
-      <div class="query-panel">
-        <el-form :inline="true" size="small">
-          <el-form-item label="发放年月">
-            <el-date-picker
-              v-model="q.payMonth"
-              type="month"
-              value-format="YYYYMM"
-              format="YYYY-MM"
-              clearable
-              placeholder="选择月份"
-              style="width: 140px"
-            />
-          </el-form-item>
-          <el-form-item label="街镇">
-            <el-select v-model="q.townId" clearable style="width: 140px">
-              <el-option v-for="t in towns" :key="t.id" :label="t.name" :value="t.id" />
-            </el-select>
-          </el-form-item>
-          <el-form-item label="级别">
-            <el-select v-model="q.batchLevel" clearable style="width: 110px">
-              <el-option label="全区" value="DISTRICT" />
-              <el-option label="镇统计" value="TOWN" />
-            </el-select>
-          </el-form-item>
-          <el-form-item>
-            <el-button type="primary" @click="search">查询</el-button>
-            <el-button @click="reset">重置</el-button>
-          </el-form-item>
-        </el-form>
-      </div>
+      <ListPageToolbar
+        :show-keyword="false"
+        :actions="toolbarActions"
+        @search="search"
+        @refresh="reset"
+      >
+        <template #filters>
+          <el-form label-position="top" size="default">
+            <el-form-item label="发放年月">
+              <el-date-picker
+                v-model="q.payMonth"
+                type="month"
+                value-format="YYYYMM"
+                format="YYYY-MM"
+                clearable
+                placeholder="选择月份"
+                style="width: 100%"
+              />
+            </el-form-item>
+            <el-form-item label="街镇">
+              <el-select v-model="q.townId" clearable style="width: 100%">
+                <el-option v-for="town in towns" :key="town.id" :label="town.name" :value="town.id" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="级别">
+              <el-select v-model="q.batchLevel" clearable style="width: 100%">
+                <el-option label="全区" value="DISTRICT" />
+                <el-option label="镇统计" value="TOWN" />
+              </el-select>
+            </el-form-item>
+          </el-form>
+        </template>
+      </ListPageToolbar>
       <div class="page-fill__table">
       <el-table v-loading="loading" :data="pagedList" border stripe size="small" height="100%">
         <el-table-column prop="batchNo" label="统一批次号" min-width="120" show-overflow-tooltip />
@@ -120,9 +115,12 @@
 </template>
 
 <script setup lang="ts">
-import { onMounted, reactive, ref } from 'vue'
+import { computed, onMounted, reactive, ref } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import PagePanel from '@/components/common/PagePanel.vue'
+import ListPageToolbar, {
+  type ListToolbarAction,
+} from '@/components/common/ListPageToolbar.vue'
 import TablePagination from '@/components/common/TablePagination.vue'
 import BatchDetailDrawer from '@/components/biz/BatchDetailDrawer.vue'
 import { generateDistrictMonthBatch, listRegionsAdmin, pageAdminBatches, purgeBatchAdmin } from '@/api/biz'
@@ -162,6 +160,24 @@ const q = reactive({
 })
 const towns = ref<RegionRow[]>([])
 const generating = ref(false)
+
+const toolbarActions = computed<ListToolbarAction[]>(() => [
+  {
+    key: 'gen',
+    label: '生成/重跑本月全区批次',
+    type: 'primary',
+    loading: generating.value,
+    onClick: () => void onGenerateDistrict(false),
+  },
+  {
+    key: 'gen15',
+    label: '联调:按15号纳入新增',
+    type: 'danger',
+    loading: generating.value,
+    onClick: () => void onGenerateDistrict(true),
+  },
+])
+
 const totalsLoading = ref(false)
 const totalsEnrichGen = ref(0)
 const drawer = ref(false)

+ 66 - 54
src/views/admin/PersonnelAdminView.vue

@@ -1,62 +1,46 @@
 <template>
   <div class="page-fill personnel-admin-view">
     <PagePanel title="人员底数管理">
-      <template #extra>
-        <el-button type="warning" size="small" :loading="initing" @click="openInit">
-          初始化状态
-        </el-button>
-        <el-button
-          type="danger"
-          size="small"
-          :disabled="selected.length === 0"
-          :loading="batchDeleting"
-          @click="onBatchDelete"
-        >
-          批量删除{{ selected.length ? `(${selected.length})` : '' }}
-        </el-button>
-        <el-button type="danger" size="small" :loading="clearing" @click="onClearAll">
-          清空全部
-        </el-button>
-      </template>
       <p class="page-tip">
         全区人员底数运维。可改基础信息、变更状态、删除(归档)。「初始化状态」可一键改全员(或当前筛选街镇/村)的业务状态、居保匹配与退养资格,不改姓名银行卡。「清空全部」会销毁栏目中全部人员底数。
       </p>
-      <div class="query-panel">
-        <el-form :inline="true" size="small">
-          <el-form-item label="街镇">
-            <el-select v-model="filterTown" clearable style="width: 140px" @change="onTownChange">
-              <el-option v-for="t in townOptions" :key="t" :label="t" :value="t" />
-            </el-select>
-          </el-form-item>
-          <el-form-item label="村居">
-            <el-select v-model="filterVillage" clearable style="width: 140px">
-              <el-option v-for="v in villageOptions" :key="v" :label="v" :value="v" />
-            </el-select>
-          </el-form-item>
-          <el-form-item label="姓名">
-            <el-input v-model="keyword" clearable placeholder="模糊" style="width: 120px" />
-          </el-form-item>
-          <el-form-item label="业务状态">
-            <el-select v-model="filterBizStatus" clearable placeholder="全部" style="width: 130px">
-              <el-option v-for="s in bizStatusOptions" :key="s.value" :label="s.label" :value="s.value" />
-            </el-select>
-          </el-form-item>
-          <el-form-item label="居保匹配">
-            <el-select v-model="filterInsurance" clearable placeholder="全部" style="width: 120px">
-              <el-option v-for="s in insuranceOptions" :key="s.value" :label="s.label" :value="s.value" />
-            </el-select>
-          </el-form-item>
-          <el-form-item label="退养资格">
-            <el-select v-model="filterQualify" clearable placeholder="全部" style="width: 130px">
-              <el-option v-for="s in qualifyOptions" :key="s.value" :label="s.label" :value="s.value" />
-            </el-select>
-          </el-form-item>
-          <el-form-item>
-            <el-button type="primary" @click="search">查询</el-button>
-            <el-button @click="reset">重置</el-button>
-          </el-form-item>
-        </el-form>
-      </div>
+      <ListPageToolbar
+        v-model:keyword="keyword"
+        keyword-placeholder="请输入姓名查询"
+        :actions="toolbarActions"
+        @search="search"
+        @refresh="reset"
+      >
+        <template #filters>
+          <el-form label-position="top" size="default">
+            <el-form-item label="街镇">
+              <el-select v-model="filterTown" clearable style="width: 100%" @change="onTownChange">
+                <el-option v-for="town in townOptions" :key="town" :label="town" :value="town" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="村居">
+              <el-select v-model="filterVillage" clearable style="width: 100%">
+                <el-option v-for="v in villageOptions" :key="v" :label="v" :value="v" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="业务状态">
+              <el-select v-model="filterBizStatus" clearable placeholder="全部" style="width: 100%">
+                <el-option v-for="s in bizStatusOptions" :key="s.value" :label="s.label" :value="s.value" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="居保匹配">
+              <el-select v-model="filterInsurance" clearable placeholder="全部" style="width: 100%">
+                <el-option v-for="s in insuranceOptions" :key="s.value" :label="s.label" :value="s.value" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="退养资格">
+              <el-select v-model="filterQualify" clearable placeholder="全部" style="width: 100%">
+                <el-option v-for="s in qualifyOptions" :key="s.value" :label="s.label" :value="s.value" />
+              </el-select>
+            </el-form-item>
+          </el-form>
+        </template>
+      </ListPageToolbar>
       <div class="page-fill__table">
       <el-table
         ref="tableRef"
@@ -245,10 +229,13 @@
 </template>
 
 <script setup lang="ts">
-import { onMounted, ref } from 'vue'
+import { computed,  onMounted, ref  } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import type { TableInstance } from 'element-plus'
 import PagePanel from '@/components/common/PagePanel.vue'
+import ListPageToolbar, {
+  type ListToolbarAction,
+} from '@/components/common/ListPageToolbar.vue'
 import StatusTag from '@/components/common/StatusTag.vue'
 import TablePagination from '@/components/common/TablePagination.vue'
 import PersonnelDetailDescriptions from '@/components/biz/PersonnelDetailDescriptions.vue'
@@ -285,6 +272,31 @@ const {
 } = useTownVillageOptions()
 
 const keyword = ref('')
+
+const toolbarActions = computed<ListToolbarAction[]>(() => [
+  {
+    key: 'init',
+    label: '初始化状态',
+    loading: initing.value,
+    onClick: () => openInit(),
+  },
+  {
+    key: 'batch-del',
+    label: selected.value.length ? `批量删除(${selected.value.length})` : '批量删除',
+    type: 'danger',
+    disabled: selected.value.length === 0,
+    loading: batchDeleting.value,
+    onClick: () => void onBatchDelete(),
+  },
+  {
+    key: 'clear',
+    label: '清空全部',
+    type: 'danger',
+    loading: clearing.value,
+    onClick: () => void onClearAll(),
+  },
+])
+
 const filterBizStatus = ref('')
 const filterInsurance = ref('')
 const filterQualify = ref('')

+ 33 - 24
src/views/admin/RegionsAdminView.vue

@@ -1,31 +1,32 @@
 <template>
   <div class="page-fill regions-admin-view">
     <PagePanel title="行政区划管理">
-      <template #extra>
-        <el-button type="primary" size="small" @click="openCreate">新增区划</el-button>
-      </template>
       <p class="page-tip">
         维护区/街镇/村居名称与资金分担比例。业务页只展示中文名;「区划编码」仅供运维对接。街镇的区+镇承担比例之和须为 100。
       </p>
-      <div class="query-panel">
-        <el-form :inline="true" size="small">
-          <el-form-item label="层级">
-            <el-select v-model="q.level" style="width: 120px" @change="onLevel">
-              <el-option label="区" value="DISTRICT" />
-              <el-option label="街镇" value="TOWN" />
-              <el-option label="村居" value="VILLAGE" />
-            </el-select>
-          </el-form-item>
-          <el-form-item v-if="q.level === 'VILLAGE'" label="上级街镇">
-            <el-select v-model="q.parentId" clearable style="width: 160px">
-              <el-option v-for="t in towns" :key="t.id" :label="t.name" :value="t.id" />
-            </el-select>
-          </el-form-item>
-          <el-form-item>
-            <el-button type="primary" @click="search">查询</el-button>
-          </el-form-item>
-        </el-form>
-      </div>
+      <ListPageToolbar
+        :show-keyword="false"
+        :actions="toolbarActions"
+        @search="search"
+        @refresh="search"
+      >
+        <template #filters>
+          <el-form label-position="top" size="default">
+            <el-form-item label="层级">
+              <el-select v-model="q.level" style="width: 100%" @change="onLevel">
+                <el-option label="区" value="DISTRICT" />
+                <el-option label="街镇" value="TOWN" />
+                <el-option label="村居" value="VILLAGE" />
+              </el-select>
+            </el-form-item>
+            <el-form-item v-if="q.level === 'VILLAGE'" label="上级街镇">
+              <el-select v-model="q.parentId" clearable style="width: 100%">
+                <el-option v-for="town in towns" :key="town.id" :label="town.name" :value="town.id" />
+              </el-select>
+            </el-form-item>
+          </el-form>
+        </template>
+      </ListPageToolbar>
       <div class="page-fill__table">
       <el-table v-loading="loading" :data="pagedList" border stripe size="small" height="100%">
         <el-table-column prop="name" label="名称" min-width="140" />
@@ -48,7 +49,7 @@
         <el-table-column label="启用" width="70" align="center">
           <template #default="{ row }">{{ row.enabled === false ? '否' : '是' }}</template>
         </el-table-column>
-        <el-table-column label="操作" width="140" align="center" fixed="right">
+        <el-table-column label="操作" width="140" fixed="right">
           <template #default="{ row }">
             <el-button link type="primary" size="small" @click="openEdit(row)">编辑</el-button>
             <el-button link type="danger" size="small" @click="onDelete(row)">删除</el-button>
@@ -127,9 +128,12 @@
 </template>
 
 <script setup lang="ts">
-import { onMounted, reactive, ref } from 'vue'
+import { computed, onMounted, reactive, ref } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import PagePanel from '@/components/common/PagePanel.vue'
+import ListPageToolbar, {
+  type ListToolbarAction,
+} from '@/components/common/ListPageToolbar.vue'
 import TablePagination from '@/components/common/TablePagination.vue'
 import {
   deleteRegionAdmin,
@@ -141,6 +145,11 @@ import { guardBusy, runSubmit } from '@/composables/useSubmitLock'
 import type { RegionRow } from '@/api/sjnmtybt/regionStore'
 
 const q = reactive({ level: 'TOWN', parentId: '' })
+
+const toolbarActions = computed<ListToolbarAction[]>(() => [
+  { key: 'create', label: '新增区划', type: 'primary', onClick: () => openCreate() },
+])
+
 const list = ref<RegionRow[]>([])
 const towns = ref<RegionRow[]>([])
 const loading = ref(false)

+ 81 - 78
src/views/alerts/AlertsView.vue

@@ -2,57 +2,59 @@
   <div class="page-fill alerts-view">
     <PagePanel title="监控告警">
       <p class="page-tip">{{ tipText }}</p>
-      <div class="query-panel alerts-view__filter">
-        <el-form :inline="true" size="small">
-          <el-form-item v-if="isAdmin" label="街镇">
-            <el-select
-              v-model="filterTown"
-              placeholder="全部"
-              clearable
-              style="width: 140px"
-              @change="onTownChange"
-            >
-              <el-option v-for="t in townOptions" :key="t" :label="t" :value="t" />
-            </el-select>
-          </el-form-item>
-          <el-form-item v-if="isAdmin" label="村居">
-            <el-select v-model="filterVillage" placeholder="全部" clearable style="width: 140px">
-              <el-option v-for="v in villageOptions" :key="v" :label="v" :value="v" />
-            </el-select>
-          </el-form-item>
-          <el-form-item label="人员">
-            <el-input v-model="filterName" placeholder="姓名" clearable style="width: 120px" />
-          </el-form-item>
-          <el-form-item v-if="!isVillageRole && !isTownRole" label="类型">
-            <el-select v-model="filterType" placeholder="全部" clearable style="width: 140px">
-              <el-option label="回盘失败" value="RETURN_FAIL" />
-              <el-option label="审批超时" value="APPROVAL_TIMEOUT" />
-              <el-option label="死亡停发预警" value="DEATH_STATUS" />
-              <el-option label="居保匹配失败" value="INSURANCE_MATCH_FAIL" />
-              <el-option label="退养资格驳回" value="QUALIFY_REJECT" />
-              <el-option label="材料待补" value="MATERIAL_PENDING" />
-            </el-select>
-          </el-form-item>
-          <el-form-item label="等级">
-            <el-select v-model="filterLevel" placeholder="全部" clearable style="width: 100px">
-              <el-option label="高" value="HIGH" />
-              <el-option label="中" value="MEDIUM" />
-              <el-option label="低" value="LOW" />
-            </el-select>
-          </el-form-item>
-          <el-form-item label="状态">
-            <el-select v-model="filterStatus" placeholder="全部" clearable style="width: 110px">
-              <el-option label="待处理" value="PENDING" />
-              <el-option label="处理中" value="PROCESSING" />
-              <el-option label="已关闭" value="CLOSED" />
-            </el-select>
-          </el-form-item>
-          <el-form-item>
-            <el-button type="primary" @click="applyFilter">查询</el-button>
-            <el-button @click="resetFilter">重置</el-button>
-          </el-form-item>
-        </el-form>
-      </div>
+
+      <ListPageToolbar
+        v-model:keyword="filterName"
+        keyword-placeholder="请输入人员姓名查询"
+        @search="applyFilter"
+        @refresh="resetFilter"
+      >
+        <template #filters>
+          <el-form label-position="top">
+            <el-form-item v-if="isAdmin" label="街镇">
+              <el-select
+                v-model="filterTown"
+                placeholder="全部"
+                clearable
+                style="width: 100%"
+                @change="onTownChange"
+              >
+                <el-option v-for="t in townOptions" :key="t" :label="t" :value="t" />
+              </el-select>
+            </el-form-item>
+            <el-form-item v-if="isAdmin" label="村居">
+              <el-select v-model="filterVillage" placeholder="全部" clearable style="width: 100%">
+                <el-option v-for="v in villageOptions" :key="v" :label="v" :value="v" />
+              </el-select>
+            </el-form-item>
+            <el-form-item v-if="!isVillageRole && !isTownRole" label="类型">
+              <el-select v-model="filterType" placeholder="全部" clearable style="width: 100%">
+                <el-option label="回盘失败" value="RETURN_FAIL" />
+                <el-option label="审批超时" value="APPROVAL_TIMEOUT" />
+                <el-option label="死亡停发预警" value="DEATH_STATUS" />
+                <el-option label="居保匹配失败" value="INSURANCE_MATCH_FAIL" />
+                <el-option label="退养资格驳回" value="QUALIFY_REJECT" />
+                <el-option label="材料待补" value="MATERIAL_PENDING" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="等级">
+              <el-select v-model="filterLevel" placeholder="全部" clearable style="width: 100%">
+                <el-option label="高" value="HIGH" />
+                <el-option label="中" value="MEDIUM" />
+                <el-option label="低" value="LOW" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="状态">
+              <el-select v-model="filterStatus" placeholder="全部" clearable style="width: 100%">
+                <el-option label="待处理" value="PENDING" />
+                <el-option label="处理中" value="PROCESSING" />
+                <el-option label="已关闭" value="CLOSED" />
+              </el-select>
+            </el-form-item>
+          </el-form>
+        </template>
+      </ListPageToolbar>
+
       <div class="page-fill__table">
       <el-table v-loading="loading" :data="pagedList" border stripe size="small" height="100%">
         <el-table-column label="人员" width="160" fixed>
@@ -70,7 +72,7 @@
             </span>
           </template>
         </el-table-column>
-        <el-table-column label="等级" width="72" align="center">
+        <el-table-column label="等级" width="72">
           <template #default="{ row }">
             <StatusTag :status="row.level" mode="level" />
           </template>
@@ -85,10 +87,9 @@
           prop="batchNo"
           label="批次号"
           width="100"
-          align="center"
           show-overflow-tooltip
         />
-        <el-table-column label="状态" width="88" align="center">
+        <el-table-column label="状态" width="88" fixed="right">
           <template #default="{ row }">
             <StatusTag :status="row.status" />
           </template>
@@ -97,29 +98,31 @@
         <el-table-column prop="handleTime" label="处理时间" width="140" />
         <el-table-column prop="result" label="处理结果" min-width="140" show-overflow-tooltip />
         <el-table-column prop="createdAt" label="产生时间" width="140" />
-        <el-table-column label="操作" width="160" align="center" fixed="right">
+        <el-table-column label="操作" width="180" fixed="right">
           <template #default="{ row }">
-            <el-button link type="primary" size="small" @click="openDetailDialog(row as AlertItem)">
-              详情
-            </el-button>
-            <el-button
-              v-if="isBatchLevelAlertType((row as AlertItem).type) && row.status !== '已关闭'"
-              link
-              type="warning"
-              size="small"
-              @click="goApproveBatch(row as AlertItem)"
-            >
-              去审批
-            </el-button>
-            <el-button
-              v-else-if="!userStore.alertReadonly && row.status !== '已关闭'"
-              link
-              type="warning"
-              size="small"
-              @click="openHandleDialog(row as AlertItem)"
-            >
-              处理告警
-            </el-button>
+            <div class="table-ops">
+              <el-button link type="primary" size="small" @click="openDetailDialog(row as AlertItem)">
+                详情
+              </el-button>
+              <el-button
+                v-if="isBatchLevelAlertType((row as AlertItem).type) && row.status !== '已关闭'"
+                link
+                type="primary"
+                size="small"
+                @click="goApproveBatch(row as AlertItem)"
+              >
+                去审批
+              </el-button>
+              <el-button
+                v-else-if="!userStore.alertReadonly && row.status !== '已关闭'"
+                link
+                type="primary"
+                size="small"
+                @click="openHandleDialog(row as AlertItem)"
+              >
+                处理告警
+              </el-button>
+            </div>
           </template>
         </el-table-column>
       </el-table>
@@ -297,6 +300,7 @@ import { useRoute, useRouter } from 'vue-router'
 import type { FormInstance, FormRules, UploadFile, UploadUserFile } from 'element-plus'
 import { ElMessage } from 'element-plus'
 import PagePanel from '@/components/common/PagePanel.vue'
+import ListPageToolbar from '@/components/common/ListPageToolbar.vue'
 import StatusTag from '@/components/common/StatusTag.vue'
 import TablePagination from '@/components/common/TablePagination.vue'
 import AlertLifecycleTimeline from '@/components/biz/AlertLifecycleTimeline.vue'
@@ -818,8 +822,7 @@ watch(
 
 <style scoped lang="less">
 .alerts-view__filter {
-  margin-bottom: 10px;
-  padding: 8px 12px 0;
+  /* 样式走全局 .content-panel .query-panel */
 }
 
 .alerts-view__danger-type {

+ 176 - 83
src/views/approvals/ApprovalsView.vue

@@ -1,28 +1,6 @@
 <template>
   <div class="page-fill approvals-view">
     <PagePanel :title="pageTitle">
-      <template #extra>
-        <template v-if="!isHistoryTab">
-          <el-button
-            type="primary"
-            :disabled="!selectedIds.length"
-            @click="onBatchDecide(true)"
-          >
-            批量通过
-          </el-button>
-          <el-button :disabled="!selectedIds.length" @click="onBatchDecide(false)">
-            批量驳回
-          </el-button>
-        </template>
-        <el-button
-          v-if="isTownReview"
-          :loading="newMonthExporting"
-          @click="onExportNewMonth"
-        >
-          导出新增名单
-        </el-button>
-      </template>
-
       <p class="page-tip">
         <template v-if="isHistoryTab && isTownReview">
           已审记录按月名单表展示。通过为「新增」(等 15 号纳入月发),无资格为异常待处理,补材料退回「待提交资格」。可导出纸质新增名单。
@@ -44,59 +22,68 @@
         </template>
       </p>
 
-      <el-tabs v-model="activeTab" @tab-change="onTabChange">
-        <el-tab-pane :label="isTownReview ? '待资格审批' : '待审批'" name="pending" />
-        <el-tab-pane :label="isTownReview ? '已审记录' : '历史记录'" name="history" />
-      </el-tabs>
-
-      <div class="query-panel approvals-view__filter">
-        <el-form :inline="true" size="small">
-          <el-form-item v-if="isAdmin" label="街镇">
-            <el-select
-              v-model="filterTown"
-              placeholder="全部"
-              clearable
-              style="width: 140px"
-              @change="onTownChange"
-            >
-              <el-option v-for="t in townOptions" :key="t" :label="t" :value="t" />
-            </el-select>
-          </el-form-item>
-          <el-form-item label="村居">
-            <el-select v-model="filterVillage" placeholder="全部" clearable style="width: 140px">
-              <el-option v-for="v in villageOptions" :key="v" :label="v" :value="v" />
-            </el-select>
-          </el-form-item>
-          <el-form-item v-if="isTownReview" label="名单月份">
-            <el-date-picker
-              v-model="filterMonth"
-              type="month"
-              value-format="YYYY-MM"
-              format="YYYY年M月"
-              style="width: 140px"
-              @change="applyFilter"
-            />
-          </el-form-item>
-          <el-form-item v-if="!isTownReview" label="批次号">
-            <el-input
-              v-model="filterBatchNo"
-              placeholder="如 202608"
-              clearable
-              style="width: 130px"
-              @keyup.enter="applyFilter"
-            />
-          </el-form-item>
-          <el-form-item v-if="isHistoryTab" label="状态">
-            <el-select v-model="filterStatus" placeholder="全部" clearable style="width: 120px">
-              <el-option label="已通过" value="已通过" />
-              <el-option label="已驳回" value="已驳回" />
-            </el-select>
-          </el-form-item>
-          <el-form-item>
-            <el-button type="primary" @click="applyFilter">查询</el-button>
-            <el-button @click="resetFilter">重置</el-button>
-          </el-form-item>
-        </el-form>
+      <ListPageToolbar
+        v-model:keyword="filterBatchNo"
+        keyword-placeholder="请输入批次号查询"
+        :show-keyword="!isTownReview"
+        :actions="toolbarActions"
+        @search="applyFilter"
+        @refresh="resetFilter"
+      >
+        <template #filters>
+          <el-form label-position="top" size="default">
+            <el-form-item v-if="isAdmin" label="街镇">
+              <el-select
+                v-model="filterTown"
+                placeholder="全部"
+                clearable
+                style="width: 100%"
+                @change="onTownChange"
+              >
+                <el-option v-for="town in townOptions" :key="town" :label="town" :value="town" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="村居">
+              <el-select v-model="filterVillage" placeholder="全部" clearable style="width: 100%">
+                <el-option v-for="v in villageOptions" :key="v" :label="v" :value="v" />
+              </el-select>
+            </el-form-item>
+            <el-form-item v-if="isTownReview" label="名单月份">
+              <el-date-picker
+                v-model="filterMonth"
+                type="month"
+                value-format="YYYY-MM"
+                format="YYYY年M月"
+                style="width: 100%"
+              />
+            </el-form-item>
+            <el-form-item v-if="isHistoryTab" label="状态">
+              <el-select v-model="filterStatus" placeholder="全部" clearable style="width: 100%">
+                <el-option label="已通过" value="已通过" />
+                <el-option label="已驳回" value="已驳回" />
+              </el-select>
+            </el-form-item>
+          </el-form>
+        </template>
+      </ListPageToolbar>
+
+      <div class="style-a-tabs" role="tablist">
+        <button
+          type="button"
+          class="style-a-tab"
+          :class="{ 'is-active': activeTab === 'pending' }"
+          @click="switchTab('pending')"
+        >
+          {{ isTownReview ? '待资格审批' : '待审批' }}
+        </button>
+        <button
+          type="button"
+          class="style-a-tab"
+          :class="{ 'is-active': activeTab === 'history' }"
+          @click="switchTab('history')"
+        >
+          {{ isTownReview ? '已审记录' : '历史记录' }}
+        </button>
       </div>
 
       <div v-if="isTownReview" class="approvals-view__paper">
@@ -120,7 +107,12 @@
           <el-table-column label="序号" width="48" align="center">
             <template #default="{ $index }">{{ (page - 1) * pageSize + $index + 1 }}</template>
           </el-table-column>
-          <el-table-column label="姓名" width="80">
+          <el-table-column
+            label="姓名"
+            width="96"
+            class-name="approvals-view__col-name"
+            label-class-name="approvals-view__col-name"
+          >
             <template #default="{ row }">
               <PersonNameCell
                 :name="(row as Approval).personName"
@@ -129,7 +121,11 @@
               />
             </template>
           </el-table-column>
-          <el-table-column label="身份证号码" width="148">
+          <el-table-column label="身份证号码"
+            width="196"
+            class-name="approvals-view__col-id"
+            label-class-name="approvals-view__col-id"
+          >
             <template #default="{ row }">
               <SensitiveValue
                 :key="`q-id-${(row as Approval).id}`"
@@ -143,7 +139,13 @@
           <el-table-column label="户籍性质(农/非)" width="96" align="center">
             <template #default="{ row }">{{ paperHousehold((row as Approval).householdType) }}</template>
           </el-table-column>
-          <el-table-column label="村集体经济合作社" width="120" show-overflow-tooltip>
+          <el-table-column
+            label="村集体经济合作社"
+            width="136"
+            show-overflow-tooltip
+            class-name="approvals-view__col-coop"
+            label-class-name="approvals-view__col-coop"
+          >
             <template #default="{ row }">{{ (row as Approval).village || '' }}</template>
           </el-table-column>
           <el-table-column label="享受起始年月" width="108" align="center">
@@ -159,8 +161,13 @@
               </span>
             </template>
           </el-table-column>
-          <el-table-column label="备注" min-width="72">
-            <template #default>{{ '' }}</template>
+          <el-table-column
+            label="备注"
+            min-width="120"
+            class-name="approvals-view__col-remark"
+            label-class-name="approvals-view__col-remark"
+          >
+            <template #default="{ row }">{{ (row as Approval).remark || '' }}</template>
           </el-table-column>
           <el-table-column label="新增时间" width="150" align="center" show-overflow-tooltip>
             <template #default="{ row }">{{ (row as Approval).addedAt || '' }}</template>
@@ -865,6 +872,9 @@ import { useRoute } from 'vue-router'
 import { ElMessage } from 'element-plus'
 import type { UploadFile, UploadUserFile } from 'element-plus'
 import PagePanel from '@/components/common/PagePanel.vue'
+import ListPageToolbar, {
+  type ListToolbarAction,
+} from '@/components/common/ListPageToolbar.vue'
 import StatusTag from '@/components/common/StatusTag.vue'
 import TablePagination from '@/components/common/TablePagination.vue'
 import BatchDetailDrawer from '@/components/biz/BatchDetailDrawer.vue'
@@ -919,6 +929,42 @@ const {
 
 const activeTab = ref('pending')
 const isHistoryTab = computed(() => activeTab.value === 'history')
+
+
+const toolbarActions = computed<ListToolbarAction[]>(() => {
+  const list: ListToolbarAction[] = []
+  if (!isHistoryTab.value) {
+    list.push({
+      key: 'batch-pass',
+      label: '批量通过',
+      type: 'primary',
+      disabled: !selectedIds.value.length,
+      onClick: () => onBatchDecide(true),
+    })
+    list.push({
+      key: 'batch-reject',
+      label: '批量驳回',
+      disabled: !selectedIds.value.length,
+      onClick: () => onBatchDecide(false),
+    })
+  }
+  if (isTownReview.value) {
+    list.push({
+      key: 'export-new',
+      label: '导出新增名单',
+      loading: newMonthExporting.value,
+      onClick: () => void onExportNewMonth(),
+    })
+  }
+  return list
+})
+
+function switchTab(name: string) {
+  if (activeTab.value === name) return
+  activeTab.value = name
+  onTabChange()
+}
+
 const filterBatchNo = ref('')
 const filterStatus = ref('')
 const filterMonth = ref(userStore.currentMonth || '')
@@ -1990,8 +2036,7 @@ watch(
 }
 
 .approvals-view__filter {
-  margin-bottom: 10px;
-  padding: 8px 12px 0;
+  /* ????? .content-panel .query-panel */
 }
 
 .approvals-view__people {
@@ -2015,6 +2060,7 @@ watch(
     align-items: center;
     justify-content: space-between;
     gap: 12px;
+    min-height: 36px;
     margin-bottom: 8px;
   }
 
@@ -2024,6 +2070,19 @@ watch(
     align-items: center;
     gap: 8px;
     width: 100%;
+    min-height: 36px;
+
+    :deep(.el-input__wrapper),
+    :deep(.el-select__wrapper) {
+      min-height: 36px !important;
+      height: 36px !important;
+    }
+
+    :deep(.el-button) {
+      min-height: 36px;
+      height: 36px;
+      padding: 0 16px;
+    }
   }
 
   &-title {
@@ -2048,7 +2107,7 @@ watch(
   }
 
   &-warn {
-    color: #c45c26 !important;
+    color: #fa8c16 !important;
   }
 
   &-amount {
@@ -2100,6 +2159,40 @@ watch(
 .approvals-view__paper {
   min-height: 0;
   overflow-x: auto;
+
+  /* ????????????? */
+  :deep(.approvals-view__col-name .cell) {
+    white-space: nowrap;
+  }
+
+  :deep(.approvals-view__col-name .person-name-cell) {
+    flex-wrap: nowrap;
+  }
+
+  :deep(.approvals-view__col-id .cell) {
+    white-space: nowrap;
+  }
+
+  :deep(.approvals-view__col-id .sensitive-value) {
+    max-width: none;
+  }
+
+  :deep(.approvals-view__col-id .sensitive-value__text) {
+    overflow: visible;
+    text-overflow: clip;
+  }
+
+  /* ???????????/?????? */
+  :deep(.approvals-view__col-coop .cell) {
+    white-space: nowrap;
+  }
+
+  /* ??????? */
+  :deep(.approvals-view__col-remark .cell) {
+    white-space: normal;
+    line-height: 1.45;
+    word-break: break-word;
+  }
 }
 
 .approvals-view__paper-title {

+ 108 - 153
src/views/base/BaseInfoView.vue

@@ -1,33 +1,6 @@
 <template>
   <div class="page-fill base-info-view">
     <PagePanel :title="isAdmin ? '人员列表' : '本村人员列表'">
-      <template #extra>
-        <el-button
-          type="success"
-          size="small"
-          :disabled="!selectedSubmittable.length"
-          :loading="qualifyBatching"
-          @click="onBatchSubmitQualify"
-        >
-          批量提交资格{{ selectedSubmittable.length ? `(${selectedSubmittable.length})` : '' }}
-        </el-button>
-        <el-button type="primary" size="small" @click="openPersonDialog">单人信息录入</el-button>
-        <el-button size="small" :loading="templateDownloading" @click="onDownloadTemplate">
-          下载导入模板
-        </el-button>
-        <el-upload
-          class="base-info-view__import-upload"
-          :show-file-list="false"
-          :auto-upload="false"
-          accept=".xlsx,.xls,.csv"
-          :disabled="importing"
-          :on-change="onImportFileChange"
-        >
-          <el-button size="small" type="success" :loading="importing">Excel 导入</el-button>
-        </el-upload>
-        <el-button size="small" @click="openImportHistory">导入历史</el-button>
-      </template>
-
       <el-alert class="page-alert" type="info" :closable="false" show-icon>
         <template #title>
           <template v-if="isAdmin">
@@ -41,140 +14,99 @@
         </template>
       </el-alert>
 
-      <el-form v-if="isAdmin" :inline="true" size="small" class="base-info-view__admin-filter">
-        <el-form-item label="街镇">
-          <el-select
-            v-model="filterTown"
-            placeholder="请选择街镇"
-            clearable
-            style="width: 140px"
-            @change="onAdminTownChange"
-          >
-            <el-option v-for="t in townOptions" :key="t" :label="t" :value="t" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="村居">
-          <el-select
-            v-model="filterVillage"
-            placeholder="请选择村居"
-            clearable
-            style="width: 140px"
-            @change="onAdminScopeChange"
-          >
-            <el-option v-for="v in villageOptions" :key="v" :label="v" :value="v" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label=" ">
-          <el-button type="primary" @click="onAdminScopeChange">查询</el-button>
-          <el-button @click="resetAdminScope">重置</el-button>
-        </el-form-item>
-      </el-form>
-
-      <el-form :inline="true" size="small" class="base-info-view__roster-filter">
-        <el-form-item label="姓名">
-          <el-input
-            v-model="rosterFilter.name"
-            clearable
-            placeholder="模糊搜索"
-            style="width: 110px"
-            class="is-editable"
-            @keyup.enter="applyRosterFilter"
-          />
-        </el-form-item>
-        <el-form-item label="身份证">
-          <el-input
-            v-model="rosterFilter.idCard"
-            clearable
-            placeholder="后几位亦可"
-            style="width: 140px"
-            class="is-editable"
-            @keyup.enter="applyRosterFilter"
-          />
-        </el-form-item>
-        <el-form-item label="手机号">
-          <el-input
-            v-model="rosterFilter.phone"
-            clearable
-            placeholder="模糊"
-            style="width: 120px"
-            class="is-editable"
-            @keyup.enter="applyRosterFilter"
-          />
-        </el-form-item>
-        <el-form-item label="业务状态">
-          <el-select
-            v-model="rosterFilter.bizStatus"
-            clearable
-            placeholder="全部"
-            style="width: 130px"
-            class="is-editable"
-            popper-class="base-info-view__status-popper"
-          >
-            <el-option
-              v-for="s in rosterStatusOptions"
-              :key="s.value"
-              :label="s.label"
-              :value="s.value"
-            />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="居保匹配">
-          <el-select
-            v-model="rosterFilter.insuranceMatchResult"
-            clearable
-            placeholder="全部"
-            style="width: 120px"
-            class="is-editable"
-          >
-            <el-option
-              v-for="s in rosterInsuranceOptions"
-              :key="s.value"
-              :label="s.label"
-              :value="s.value"
-            />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="退养资格">
-          <el-select
-            v-model="rosterFilter.retireQualifyType"
-            clearable
-            placeholder="全部"
-            style="width: 130px"
-            class="is-editable"
-          >
-            <el-option
-              v-for="s in rosterQualifyOptions"
-              :key="s.value"
-              :label="s.label"
-              :value="s.value"
-            />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="来源">
-          <el-select
-            v-model="rosterFilter.source"
-            clearable
-            filterable
-            allow-create
-            placeholder="全部"
-            style="width: 120px"
-            class="is-editable"
+      <ListPageToolbar
+        v-model:keyword="rosterFilter.name"
+        keyword-placeholder="请输入姓名查询"
+        @search="applyRosterFilter"
+        @refresh="onToolbarRefresh"
+      >
+        <template #filters>
+          <el-form label-position="top" size="default">
+            <template v-if="isAdmin">
+              <el-form-item label="街镇">
+                <el-select v-model="filterTown" placeholder="请选择街镇" clearable style="width: 100%" @change="onAdminTownChange">
+                  <el-option v-for="town in townOptions" :key="town" :label="town" :value="town" />
+                </el-select>
+              </el-form-item>
+              <el-form-item label="村居">
+                <el-select v-model="filterVillage" placeholder="请选择村居" clearable style="width: 100%" @change="onAdminScopeChange">
+                  <el-option v-for="v in villageOptions" :key="v" :label="v" :value="v" />
+                </el-select>
+              </el-form-item>
+            </template>
+            <el-form-item label="身份证">
+              <el-input v-model="rosterFilter.idCard" clearable placeholder="后几位亦可" style="width: 100%" class="is-editable" />
+            </el-form-item>
+            <el-form-item label="手机号">
+              <el-input v-model="rosterFilter.phone" clearable placeholder="模糊" style="width: 100%" class="is-editable" />
+            </el-form-item>
+            <el-form-item label="业务状态">
+              <el-select v-model="rosterFilter.bizStatus" clearable placeholder="全部" style="width: 100%" class="is-editable">
+                <el-option v-for="s in rosterStatusOptions" :key="s.value" :label="s.label" :value="s.value" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="居保匹配">
+              <el-select v-model="rosterFilter.insuranceMatchResult" clearable placeholder="全部" style="width: 100%" class="is-editable">
+                <el-option v-for="s in rosterInsuranceOptions" :key="s.value" :label="s.label" :value="s.value" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="退养资格">
+              <el-select v-model="rosterFilter.retireQualifyType" clearable placeholder="全部" style="width: 100%" class="is-editable">
+                <el-option v-for="s in rosterQualifyOptions" :key="s.value" :label="s.label" :value="s.value" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="来源">
+              <el-select v-model="rosterFilter.source" clearable filterable allow-create placeholder="全部" style="width: 100%" class="is-editable">
+                <el-option v-for="s in rosterSourceOptions" :key="s" :label="s" :value="s" />
+              </el-select>
+            </el-form-item>
+          </el-form>
+        </template>
+        <template #actions>
+          <el-button
+            type="primary"
+            :disabled="!selectedSubmittable.length"
+            :loading="qualifyBatching"
+            @click="onBatchSubmitQualify"
           >
-            <el-option v-for="s in rosterSourceOptions" :key="s" :label="s" :value="s" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label=" ">
-          <el-button type="primary" @click="applyRosterFilter">查询</el-button>
-          <el-button @click="resetRosterFilter">重置</el-button>
+            批量提交资格{{ selectedSubmittable.length ? `(${selectedSubmittable.length})` : '' }}
+          </el-button>
+          <el-button type="primary" @click="openPersonDialog">单人信息录入</el-button>
           <el-button :loading="rosterExporting" @click="onExportRoster">列表导出</el-button>
+          <el-dropdown trigger="click">
+            <el-button title="更多">
+              <el-icon><MoreFilled /></el-icon>
+            </el-button>
+            <template #dropdown>
+              <el-dropdown-menu>
+                <el-dropdown-item :disabled="templateDownloading" @click="onDownloadTemplate">
+                  下载导入模板
+                </el-dropdown-item>
+                <el-dropdown-item @click="triggerImportClick">Excel 导入</el-dropdown-item>
+                <el-dropdown-item @click="openImportHistory">导入历史</el-dropdown-item>
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
           <SensitiveRevealSwitch
             v-model="revealSensitive"
             class="base-info-view__reveal"
             :inactive-text="isVillage ? '显示身份证' : '显示身份证和银行卡号'"
             :active-text="isVillage ? '已显示身份证' : '已显示证件卡号'"
           />
-        </el-form-item>
-      </el-form>
+          <!-- 隐藏 upload,供更多菜单触发 -->
+          <el-upload
+            ref="importUploadRef"
+            class="base-info-view__import-upload is-hidden-upload"
+            :show-file-list="false"
+            :auto-upload="false"
+            accept=".xlsx,.xls,.csv"
+            :disabled="importing"
+            :on-change="onImportFileChange"
+          >
+            <span />
+          </el-upload>
+        </template>
+      </ListPageToolbar>
 
       <div class="page-fill__table">
         <el-table
@@ -540,6 +472,7 @@
       v-model="importHistoryVisible"
       title="人员导入历史"
       size="560px"
+      append-to-body
       destroy-on-close
     >
       <el-table
@@ -603,9 +536,11 @@
 
 <script setup lang="ts">
 import { computed, onMounted, reactive, ref, watch } from 'vue'
+import { MoreFilled } from '@element-plus/icons-vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import type { FormInstance, FormRules, UploadFile } from 'element-plus'
 import PagePanel from '@/components/common/PagePanel.vue'
+import ListPageToolbar from '@/components/common/ListPageToolbar.vue'
 import StatusTag from '@/components/common/StatusTag.vue'
 import TablePagination from '@/components/common/TablePagination.vue'
 import AlertLifecycleTimeline from '@/components/biz/AlertLifecycleTimeline.vue'
@@ -879,6 +814,17 @@ function resetRosterFilter() {
   void peoplePager.resetAndLoad()
 }
 
+function onToolbarRefresh() {
+  resetRosterFilter()
+  if (isAdmin.value) resetAdminScope()
+}
+
+function triggerImportClick() {
+  const input = document.querySelector('.base-info-view__import-upload input[type="file"]') as HTMLInputElement | null
+  input?.click()
+}
+
+
 async function onDownloadTemplate() {
   if (!hasVillageScope.value) {
     ElMessage.warning(
@@ -1323,6 +1269,15 @@ watch(
     max-height: 420px !important;
   }
 }
+
+.is-hidden-upload {
+  position: absolute;
+  width: 0;
+  height: 0;
+  overflow: hidden;
+  opacity: 0;
+  pointer-events: none;
+}
 </style>
 
 <style scoped lang="less">

+ 262 - 159
src/views/dashboard/DashboardView.vue

@@ -7,20 +7,44 @@
     }"
     v-loading="loading"
   >
-    <!-- 核心指标:在册总人数 ≠ 本月发放人数 -->
+    <!-- 风格 B:顶筛浮条(发放周期) -->
+    <section class="dashboard-filter-bar" aria-label="展板筛选">
+      <div class="dashboard-filter-bar__left">
+        <span class="dashboard-filter-bar__scope">{{ data?.batchInfo?.scope || '全区' }}</span>
+        <span class="dashboard-filter-bar__sep">·</span>
+        <span class="dashboard-filter-bar__tip">{{ roleTip }}</span>
+      </div>
+      <div class="dashboard-filter-bar__period">
+        <span class="dashboard-filter-bar__period-label">发放周期</span>
+        <el-date-picker
+          :model-value="userStore.currentMonth"
+          type="month"
+          value-format="YYYY-MM"
+          format="YYYY-MM"
+          size="small"
+          placeholder="选择月份"
+          :clearable="false"
+          class="dashboard-filter-bar__picker"
+          @update:model-value="onMonthChange"
+        />
+      </div>
+    </section>
+
+    <!-- 核心指标:多色 KPI(在册总人数 ≠ 本月发放人数) -->
     <StatCards :items="statItems" />
+    <!-- 说明 tip 由全局 .dashboard-stat-note { display:none } 隐藏,勿删 -->
     <p class="dashboard-stat-note">
       <strong>在册总人数</strong>是当前角色范围内的人员档案底数(含暂停、死亡等仍在库人员),
       <strong>不是</strong>本月实际发放人数。
       本月发放人数与金额按当前发放周期的月补清单口径统计,见下方对照表。
     </p>
 
-    <!-- 5.1.1.1 近期告警滚动;点击跳转告警页 -->
+    <!-- 轻量告警条:浮层白卡 + 横向滚动 -->
     <section class="alert-ticker" aria-label="近期告警">
       <div class="alert-ticker__label">
         <el-icon><Bell /></el-icon>
         <span>近期告警</span>
-        <em v-if="alertTickerItems.length">未关闭 {{ alertTickerItems.length }}</em>
+        <em v-if="alertTickerItems.length">{{ alertTickerItems.length }}</em>
       </div>
       <div class="alert-ticker__track-wrap" @mouseenter="tickerPaused = true" @mouseleave="tickerPaused = false">
         <div
@@ -50,7 +74,7 @@
       </el-button>
     </section>
 
-    <!-- 本批次发放概览 -->
+    <!-- 本批次发放概览(浮层白卡) -->
     <section class="batch-overview">
       <div class="batch-overview__head">
         <div class="batch-overview__title">
@@ -61,30 +85,14 @@
               <template v-if="isVillage || isTown">
                 统计范围
                 <em>{{ data?.batchInfo.scope || '-' }}</em>
-                · {{ roleTip }}
               </template>
               <template v-else>
                 全区月批 {{ data?.batchInfo.batchNo || '-' }} · 统计范围
                 <em>{{ data?.batchInfo.scope || '-' }}</em>
-                · {{ roleTip }}
               </template>
             </p>
           </div>
         </div>
-        <div class="batch-overview__period">
-          <span class="batch-overview__period-label">发放周期</span>
-          <el-date-picker
-            :model-value="userStore.currentMonth"
-            type="month"
-            value-format="YYYY-MM"
-            format="YYYY-MM"
-            size="small"
-            placeholder="选择月份"
-            :clearable="false"
-            class="batch-overview__period-picker"
-            @update:model-value="onMonthChange"
-          />
-        </div>
       </div>
 
       <div class="batch-overview__metrics">
@@ -134,14 +142,14 @@
           </template>
         </el-table-column>
         <el-table-column prop="node" label="当前节点" width="150" show-overflow-tooltip />
-        <el-table-column label="状态" width="80" align="center">
+        <el-table-column label="状态" width="80" align="center" fixed="right">
           <template #default="{ row }">
             <el-tag size="small" :type="(row as TodoItem).read ? 'info' : 'warning'" effect="plain">
               {{ (row as TodoItem).read ? '已读' : '未读' }}
             </el-tag>
           </template>
         </el-table-column>
-        <el-table-column label="操作" width="100" align="center">
+        <el-table-column label="操作" width="100" align="center" fixed="right">
           <template #default="{ row }">
             <el-button
               v-if="canHandleTodo(row as TodoItem)"
@@ -168,23 +176,17 @@
     <!-- 村 / 镇:发放表与状态饼图并排;待办已在上方单独一行 -->
     <div v-if="isTownLevelBoard" class="village-workspace">
       <div class="village-workspace__main" :class="{ 'has-pie': sidePieItems.length > 0 }">
-        <PieMixPanel
-          v-if="sidePieItems.length > 0"
-          class="village-workspace__pie"
-          :title="sidePieTitle"
-          :items="sidePieItems"
-        />
-
-        <PagePanel :title="payTableTitle" class="village-workspace__table-panel village-workspace__pay">
+        <!-- 左右同为 PagePanel 根节点,标题行结构一致、顶边对齐 -->
+        <PagePanel :title="payTableTitle" class="village-workspace__pay village-workspace__table-panel">
           <el-table
             v-loading="payTableLoading"
             :data="payTableRows"
             border
             stripe
             size="small"
-            max-height="300"
+            max-height="280"
           >
-            <el-table-column prop="name" :label="payTableNameLabel" min-width="110" />
+            <el-table-column prop="name" :label="payTableNameLabel" min-width="110" show-overflow-tooltip />
             <el-table-column label="发放人数" width="100" align="right">
               <template #default="{ row }">{{ formatHeadcount(row.people) }}</template>
             </el-table-column>
@@ -193,6 +195,13 @@
             </el-table-column>
           </el-table>
         </PagePanel>
+        <PagePanel
+          v-if="sidePieItems.length > 0"
+          :title="sidePieTitle"
+          class="village-workspace__pie"
+        >
+          <PieMixPanel :items="sidePieItems" />
+        </PagePanel>
       </div>
 
       <div class="village-workspace__trend">
@@ -413,9 +422,9 @@ let refreshTimer: ReturnType<typeof setInterval> | null = null
 const DASHBOARD_REFRESH_MS = 120_000
 
 /** 图表主色:政务蓝行政台 */
-const CHART_COLOR = '#2B73F8'
-const CHART_COLORS = ['#2B73F8', '#27C98E', '#FA8C16', '#722ED1', '#1890FF']
-const CHART_COLOR_GREEN = '#27C98E'
+const CHART_COLOR = '#2f54eb'
+const CHART_COLORS = ['#2f54eb', '#13c2c2', '#faad14', '#722ed1', '#52c41a']
+const CHART_COLOR_GREEN = '#23B7A4'
 
 const userStore = useUserStore()
 /** shallowRef:整包替换工作台数据,避免深层响应拖慢图表重算 */
@@ -537,25 +546,25 @@ const REGISTERED_COUNT_TIP =
 const MONTH_PAY_COUNT_TIP = '当前发放周期纳入月补清单的人数,与在册总人数口径不同。'
 const MONTH_AMOUNT_TIP = '当前发放周期月补清单应发合计。'
 
-/** 核心指标:在册总人数与本月发放人数分列,避免混用 */
+/** 核心指标:在册总人数与本月发放人数分列;色调对齐风格 B 多色 KPI */
 const statItems = computed(() => {
   const s = data.value?.stats
   const payPeople = data.value?.batchInfo?.peopleCount
   if (!s) {
     return [
-      { label: '在册总人数', value: '-', unit: '人', tip: REGISTERED_COUNT_TIP },
-      { label: '本月发放人数', value: '-', unit: '人', tip: MONTH_PAY_COUNT_TIP },
-      { label: '本月新增', value: '-', unit: '人', tip: '本月新纳入在册的人数。' },
-      { label: '本月减少', value: '-', unit: '人', tip: '本月因暂停/停止/死亡等退出发放的人数。' },
-      { label: '本月发放金额', value: '-', unit: '元', tip: MONTH_AMOUNT_TIP },
+      { label: '在册总人数', value: '-', unit: '人', tip: REGISTERED_COUNT_TIP, tone: 'blue' as const },
+      { label: '本月发放人数', value: '-', unit: '人', tip: MONTH_PAY_COUNT_TIP, tone: 'green' as const },
+      { label: '本月新增', value: '-', unit: '人', tip: '本月新纳入在册的人数。', tone: 'purple' as const },
+      { label: '本月减少', value: '-', unit: '人', tip: '本月因暂停/停止/死亡等退出发放的人数。', tone: 'orange' as const },
+      { label: '本月发放金额', value: '-', unit: '元', tip: MONTH_AMOUNT_TIP, tone: 'cyan' as const },
     ]
   }
   return [
-    { label: '在册总人数', value: s.normal, unit: '人', tip: REGISTERED_COUNT_TIP },
-    { label: '本月发放人数', value: payPeople ?? 0, unit: '人', tip: MONTH_PAY_COUNT_TIP },
-    { label: '本月新增', value: s.monthIncrease ?? 0, unit: '人', tip: '本月新纳入在册的人数。' },
-    { label: '本月减少', value: s.monthDecrease ?? 0, unit: '人', tip: '本月因暂停/停止/死亡等退出发放的人数。' },
-    { label: '本月发放金额', value: formatAmount(s.amount), unit: '元', tip: MONTH_AMOUNT_TIP },
+    { label: '在册总人数', value: s.normal, unit: '人', tip: REGISTERED_COUNT_TIP, tone: 'blue' as const },
+    { label: '本月发放人数', value: payPeople ?? 0, unit: '人', tip: MONTH_PAY_COUNT_TIP, tone: 'green' as const },
+    { label: '本月新增', value: s.monthIncrease ?? 0, unit: '人', tip: '本月新纳入在册的人数。', tone: 'purple' as const },
+    { label: '本月减少', value: s.monthDecrease ?? 0, unit: '人', tip: '本月因暂停/停止/死亡等退出发放的人数。', tone: 'orange' as const },
+    { label: '本月发放金额', value: formatAmount(s.amount), unit: '元', tip: MONTH_AMOUNT_TIP, tone: 'cyan' as const },
   ]
 })
 
@@ -600,14 +609,14 @@ const batchMetrics = computed(() => {
         value: s?.monthDecrease ?? 0,
         unit: '人',
         icon: 'Bell',
-        tone: 'tone-gold',
+        tone: 'tone-primary',
       },
       {
         label: '预计发放',
         value: s ? formatAmount(s.amount) : info ? formatAmount(info.amount) : '-',
         unit: '元',
         icon: 'Wallet',
-        tone: 'tone-gold',
+        tone: 'tone-primary',
       },
     ]
   }
@@ -618,7 +627,7 @@ const batchMetrics = computed(() => {
         label: '发放月份',
         value: userStore.currentMonth || '-',
         icon: 'Calendar',
-        tone: 'tone-gold',
+        tone: 'tone-primary',
       },
       {
         label: '待资格审批',
@@ -639,7 +648,7 @@ const batchMetrics = computed(() => {
         value: review?.withoutLandCount ?? 0,
         unit: '人',
         icon: 'Wallet',
-        tone: 'tone-gold',
+        tone: 'tone-primary',
       },
     ]
   }
@@ -665,14 +674,14 @@ const batchMetrics = computed(() => {
         value: sw?.diskPendingCount ?? 0,
         unit: '批',
         icon: 'Ticket',
-        tone: 'tone-gold',
+        tone: 'tone-primary',
       },
       {
         label: '未关闭告警',
         value: sw?.alertCount ?? 0,
         unit: '条',
         icon: 'Bell',
-        tone: 'tone-gold',
+        tone: 'tone-primary',
       },
     ]
   }
@@ -698,14 +707,14 @@ const batchMetrics = computed(() => {
         value: lw?.issueReadyCount ?? 0,
         unit: '批',
         icon: 'Ticket',
-        tone: 'tone-gold',
+        tone: 'tone-primary',
       },
       {
         label: '未关闭告警',
         value: lw?.alertCount ?? 0,
         unit: '条',
         icon: 'Bell',
-        tone: 'tone-gold',
+        tone: 'tone-primary',
       },
     ]
   }
@@ -716,7 +725,7 @@ const batchMetrics = computed(() => {
         label: '全区月批',
         value: info?.batchNo ?? '-',
         icon: 'Ticket',
-        tone: 'tone-gold',
+        tone: 'tone-primary',
       },
       {
         label: '覆盖街镇/村',
@@ -736,7 +745,7 @@ const batchMetrics = computed(() => {
         value: d?.withoutLandCount ?? 0,
         unit: '人',
         icon: 'Checked',
-        tone: 'tone-gold',
+        tone: 'tone-primary',
       },
     ]
   }
@@ -747,7 +756,7 @@ const batchMetrics = computed(() => {
         label: '全区月批',
         value: info?.batchNo ?? '-',
         icon: 'Ticket',
-        tone: 'tone-gold',
+        tone: 'tone-primary',
       },
       {
         label: '覆盖街镇/村',
@@ -767,7 +776,7 @@ const batchMetrics = computed(() => {
         value: d ? formatAmount(d.amount) : '-',
         unit: '元',
         icon: 'Wallet',
-        tone: 'tone-gold',
+        tone: 'tone-primary',
       },
     ]
   }
@@ -776,7 +785,7 @@ const batchMetrics = computed(() => {
       label: '全区月批',
       value: info?.batchNo ?? '-',
       icon: 'Ticket',
-      tone: 'tone-gold',
+      tone: 'tone-primary',
     },
     {
       label: '统计范围',
@@ -796,7 +805,7 @@ const batchMetrics = computed(() => {
       value: info ? formatAmount(info.amount) : '-',
       unit: '元',
       icon: 'Wallet',
-      tone: 'tone-gold',
+      tone: 'tone-primary',
     },
   ]
 })
@@ -808,7 +817,7 @@ function buildBarOption(
   valueFormatter?: (v: unknown) => string,
   barColorStops = [
     { offset: 0, color: '#4A8AFF' },
-    { offset: 1, color: '#2B73F8' },
+    { offset: 1, color: '#2f54eb' },
   ],
 ): EChartsCoreOption {
   const names = list.map((i) => i.name)
@@ -847,7 +856,7 @@ function buildBarOption(
     xAxis: {
       type: 'category',
       data: names,
-      axisLine: { lineStyle: { color: '#e0d6c8' } },
+      axisLine: { lineStyle: { color: '#e8eef5' } },
       axisLabel: {
         color: '#8c8c8c',
         fontSize: many ? 10 : 12,
@@ -862,7 +871,7 @@ function buildBarOption(
       name: yName,
       nameTextStyle: { color: '#8c8c8c', fontSize: 11 },
       axisLabel: { color: '#8c8c8c', fontSize: 11 },
-      splitLine: { lineStyle: { color: '#f0ebe3', type: 'dashed' } },
+      splitLine: { lineStyle: { color: '#e8eef5', type: 'dashed' } },
       axisLine: { show: false },
       minInterval: yName.includes('人') ? 1 : undefined,
     },
@@ -912,7 +921,7 @@ function mergePayRows(people: ChartBarItem[] = [], amounts: ChartBarItem[] = [])
 
 function buildPayCombineOption(list: Array<{ name: string; people: number; amount: number }>): EChartsCoreOption {
   return {
-    color: [CHART_COLOR, '#1A56C4'],
+    color: [CHART_COLOR, '#225DCB'],
     tooltip: {
       trigger: 'axis',
       backgroundColor: 'rgba(255,255,255,0.96)',
@@ -933,7 +942,7 @@ function buildPayCombineOption(list: Array<{ name: string; people: number; amoun
     xAxis: {
       type: 'category',
       data: list.map((i) => i.name),
-      axisLine: { lineStyle: { color: '#e0d6c8' } },
+      axisLine: { lineStyle: { color: '#e8eef5' } },
       axisLabel: { color: '#8c8c8c', fontSize: 11 },
       axisTick: { show: false },
     },
@@ -948,7 +957,7 @@ function buildPayCombineOption(list: Array<{ name: string; people: number; amoun
           fontSize: 11,
           formatter: (v: number) => (Math.abs(v) >= 10000 ? `${v / 10000}万` : String(v)),
         },
-        splitLine: { lineStyle: { color: '#f0ebe3', type: 'dashed' } },
+        splitLine: { lineStyle: { color: '#e8eef5', type: 'dashed' } },
         axisLine: { show: false },
       },
       {
@@ -976,7 +985,7 @@ function buildPayCombineOption(list: Array<{ name: string; people: number; amoun
             y2: 1,
             colorStops: [
               { offset: 0, color: '#4A8AFF' },
-              { offset: 1, color: '#2B73F8' },
+              { offset: 1, color: '#2f54eb' },
             ],
           },
         },
@@ -1050,7 +1059,7 @@ const roleBarOption = computed(() =>
 const batchCompareOption = computed<EChartsCoreOption>(() => {
   const list = data.value?.batchCompareChart ?? []
   return {
-    color: [CHART_COLOR, '#1A56C4'],
+    color: [CHART_COLOR, '#225DCB'],
     tooltip: {
       trigger: 'axis',
       backgroundColor: 'rgba(255,255,255,0.96)',
@@ -1071,7 +1080,7 @@ const batchCompareOption = computed<EChartsCoreOption>(() => {
     xAxis: {
       type: 'category',
       data: list.map((i) => i.name),
-      axisLine: { lineStyle: { color: '#e0d6c8' } },
+      axisLine: { lineStyle: { color: '#e8eef5' } },
       axisLabel: { color: '#8c8c8c', fontSize: 12 },
       axisTick: { show: false },
     },
@@ -1086,7 +1095,7 @@ const batchCompareOption = computed<EChartsCoreOption>(() => {
           fontSize: 11,
           formatter: (v: number) => (Math.abs(v) >= 10000 ? `${v / 10000}万` : String(v)),
         },
-        splitLine: { lineStyle: { color: '#f0ebe3', type: 'dashed' } },
+        splitLine: { lineStyle: { color: '#e8eef5', type: 'dashed' } },
         axisLine: { show: false },
       },
       {
@@ -1114,7 +1123,7 @@ const batchCompareOption = computed<EChartsCoreOption>(() => {
             y2: 1,
             colorStops: [
               { offset: 0, color: '#4A8AFF' },
-              { offset: 1, color: '#2B73F8' },
+              { offset: 1, color: '#2f54eb' },
             ],
           },
         },
@@ -1184,7 +1193,7 @@ function buildPieOption(
         itemStyle: { borderColor: '#fff', borderWidth: 2 },
         emphasis: {
           scaleSize: 4,
-          itemStyle: { shadowBlur: 8, shadowColor: 'rgba(43,115,248,0.35)' },
+          itemStyle: { shadowBlur: 8, shadowColor: 'rgba(34,93,203,0.35)' },
         },
       },
     ],
@@ -1232,7 +1241,7 @@ const districtWithoutLandOption = computed(() =>
     (v) => `${Number(v).toLocaleString('zh-CN')} 人`,
     [
       { offset: 0, color: '#4A8AFF' },
-      { offset: 1, color: '#2B73F8' },
+      { offset: 1, color: '#2f54eb' },
     ],
   ),
 )
@@ -1387,23 +1396,105 @@ watch(
 .dashboard-view {
   display: flex;
   flex-direction: column;
+  gap: 14px;
+}
+
+/* 风格 B:顶筛条(白壳内轻边框) */
+.dashboard-filter-bar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
   gap: 12px;
+  min-height: 48px;
+  padding: 8px 16px;
+  background: #fafafa;
+  border: 1px solid @border-light;
+  border-radius: 8px;
+  box-shadow: none;
+
+  &__left {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    min-width: 0;
+    font-size: 13px;
+    color: #595959;
+  }
+
+  &__scope {
+    font-weight: 700;
+    color: #262626;
+  }
+
+  &__sep {
+    color: #bfbfbf;
+  }
+
+  &__tip {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  &__period {
+    flex-shrink: 0;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    padding: 2px 6px 2px 12px;
+    background: #f5f7ff;
+    border: 1px solid rgba(47, 84, 235, 0.16);
+    border-radius: 8px;
+  }
+
+  &__period-label {
+    font-size: 12px;
+    color: #595959;
+    white-space: nowrap;
+  }
+
+  &__picker {
+    width: 124px;
+
+    :deep(.el-input__wrapper) {
+      min-height: 32px !important;
+      height: 32px !important;
+      background: #fff;
+      box-shadow: none;
+      border: 1px solid @border-color;
+    }
+
+    :deep(.el-input__inner) {
+      font-size: 12px;
+      font-weight: 600;
+      color: #262626;
+    }
+  }
 }
 
 .dashboard-todos-panel__count {
   font-size: 12px;
-  color: #8a7a66;
+  color: #8a9099;
 }
 
 .dashboard-todos-table {
   cursor: pointer;
 
+  :deep(th.el-table__cell),
+  :deep(td.el-table__cell) {
+    text-align: left;
+  }
+
+  :deep(.el-table__cell .cell) {
+    text-align: left;
+  }
+
   :deep(.is-todo-read) {
-    color: #9a8b7a;
+    color: #8a9099;
   }
 
   .text-muted {
-    color: #b5a898;
+    color: #c0c4cc;
   }
 }
 
@@ -1411,11 +1502,13 @@ watch(
   display: flex;
   align-items: center;
   gap: 10px;
-  min-height: 40px;
-  padding: 6px 10px;
-  background: #fff8ef;
+  min-height: 44px;
+  padding: 8px 16px;
+  margin-bottom: 0;
+  background: #fff;
   border: 1px solid @border-light;
-  border-left: 3px solid #2B73F8;
+  border-radius: 8px;
+  box-shadow: none;
 
   &__label {
     display: inline-flex;
@@ -1424,7 +1517,7 @@ watch(
     flex-shrink: 0;
     font-size: 13px;
     font-weight: 600;
-    color: @primary-dark;
+    color: #262626;
 
     em {
       font-style: normal;
@@ -1432,7 +1525,7 @@ watch(
       height: 18px;
       padding: 0 5px;
       border-radius: 9px;
-      background: #c45c26;
+      background: #ff7875;
       color: #fff;
       font-size: 11px;
       line-height: 18px;
@@ -1479,7 +1572,7 @@ watch(
 
   &__type {
     font-size: 12px;
-    color: #8a7350;
+    color: #8a9099;
   }
 
   &__text {
@@ -1530,12 +1623,12 @@ watch(
   min-height: 0;
   display: flex;
   flex-direction: column;
-  gap: 12px;
+  gap: 14px;
 
   &__charts {
     display: grid;
     grid-template-columns: repeat(3, minmax(0, 1fr));
-    gap: 12px;
+    gap: 14px;
 
     @media (max-width: 1200px) {
       grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -1575,7 +1668,7 @@ watch(
   }
 
   :deep(.el-table__header th.el-table__cell) {
-    background: #f5f2eb;
+    background: #fafafa;
     color: @text-regular;
     font-weight: 600;
   }
@@ -1586,21 +1679,22 @@ watch(
   }
 
   :deep(.el-table__row:hover > td.el-table__cell) {
-    background: #faf6ee !important;
+    background: #f0f7ff !important;
   }
 }
 
 .village-workspace {
   display: flex;
   flex-direction: column;
-  gap: 12px;
+  gap: 14px;
 
   &__main {
     display: grid;
     grid-template-columns: minmax(0, 1fr);
     grid-template-areas: 'pay';
-    gap: 12px;
-    align-items: start;
+    gap: 14px;
+    /* 右侧跟左侧同高(拉高右侧),不改左侧宽度 */
+    align-items: stretch;
 
     &.has-pie {
       grid-template-columns: minmax(0, 1.55fr) minmax(300px, 0.9fr);
@@ -1610,6 +1704,7 @@ watch(
     @media (max-width: 1100px) {
       grid-template-columns: 1fr;
       grid-template-areas: 'pay';
+      align-items: start;
 
       &.has-pie {
         grid-template-areas:
@@ -1618,26 +1713,30 @@ watch(
       }
     }
 
-    :deep(.content-panel) {
+    > .content-panel {
       margin-bottom: 0;
       min-height: 0;
-      overflow: visible;
+      height: 100%;
       display: flex;
       flex-direction: column;
     }
+  }
 
-    :deep(.page-panel__title),
-    :deep(.block-title) {
-      flex-shrink: 0;
-    }
+  &__pay {
+    grid-area: pay;
+    min-width: 0;
   }
 
   &__pie {
     grid-area: pie;
-  }
+    min-width: 0;
 
-  &__pay {
-    grid-area: pay;
+    /* 标题下内容区吃满剩余高度,供饼图垂直居中 */
+    :deep(.pie-mix),
+    :deep(.pie-mix__empty) {
+      flex: 1;
+      min-height: 0;
+    }
   }
 
   &__table-panel {
@@ -1646,10 +1745,26 @@ watch(
     }
   }
 
+  &__pay,
+  &__pie {
+    :deep(.page-panel__title),
+    :deep(.block-title) {
+      flex-shrink: 0;
+      margin-bottom: 12px;
+      min-height: 36px;
+    }
+
+    :deep(.page-panel__text) {
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+  }
+
   &__trend {
     display: grid;
     grid-template-columns: repeat(2, minmax(0, 1fr));
-    gap: 12px;
+    gap: 14px;
 
     @media (max-width: 1100px) {
       grid-template-columns: 1fr;
@@ -1666,18 +1781,23 @@ watch(
 }
 
 .batch-overview {
-  background: @block-bg;
+  background: #fff;
   border: 1px solid @border-light;
+  border-radius: 8px;
+  box-shadow: none;
   overflow: hidden;
+  margin-bottom: 0;
 
   &__head {
     display: flex;
     align-items: center;
     justify-content: space-between;
     gap: 12px;
+    box-sizing: border-box;
+    min-height: 56px;
     padding: 12px 16px;
-    background: linear-gradient(90deg, #eef4ff 0%, #f7f9fc 55%, #fff 100%);
-    border-bottom: 1px solid #d6e4ff;
+    background: #fff;
+    border-bottom: 1px solid @border-light;
   }
 
   &__title {
@@ -1688,67 +1808,28 @@ watch(
 
     h3 {
       margin: 0;
-      font-size: 15px;
+      font-size: 16px;
       font-weight: 700;
-      color: @text-primary;
+      color: #262626;
       line-height: 1.3;
     }
 
     p {
       margin: 4px 0 0;
       font-size: 12px;
-      color: @text-secondary;
+      color: #8c8c8c;
       line-height: 1.4;
 
       em {
         font-style: normal;
         font-weight: 600;
-        color: @primary-dark;
+        color: #262626;
       }
     }
   }
 
   &__bar {
-    flex-shrink: 0;
-    width: 3px;
-    height: 28px;
-    margin-top: 2px;
-    background: linear-gradient(180deg, @brand-gold-deep, @primary-color);
-    border-radius: 1px;
-  }
-
-  &__period {
-    flex-shrink: 0;
-    display: flex;
-    align-items: center;
-    gap: 8px;
-    padding: 2px 6px 2px 10px;
-    font-size: 12px;
-    color: @primary-dark;
-    background: rgba(43, 115, 248, 0.12);
-    border: 1px solid rgba(43, 115, 248, 0.35);
-    border-radius: @radius-sm;
-  }
-
-  &__period-label {
-    flex-shrink: 0;
-    white-space: nowrap;
-  }
-
-  &__period-picker {
-    width: 120px;
-
-    :deep(.el-input__wrapper) {
-      background: #fff;
-      box-shadow: none;
-      border: 1px solid rgba(43, 115, 248, 0.45);
-    }
-
-    :deep(.el-input__inner) {
-      font-size: 12px;
-      color: @primary-dark;
-      font-weight: 600;
-    }
+    display: none;
   }
 
   &__metrics {
@@ -1788,7 +1869,7 @@ watch(
   }
 
   &:hover {
-    background: @el-primary-light-9;
+    background: #fafafa;
   }
 
   &__icon {
@@ -1800,8 +1881,8 @@ watch(
     justify-content: center;
     border-radius: @radius-sm;
     font-size: 18px;
-    background: @el-primary-light-9;
-    color: @primary-color;
+    background: #f5f5f5;
+    color: #595959;
   }
 
   &__body {
@@ -1810,7 +1891,7 @@ watch(
 
   &__label {
     font-size: 12px;
-    color: @text-secondary;
+    color: #8c8c8c;
     margin-bottom: 4px;
     white-space: nowrap;
   }
@@ -1818,7 +1899,7 @@ watch(
   &__value {
     font-size: 18px;
     font-weight: 700;
-    color: @text-primary;
+    color: #262626;
     line-height: 1.2;
     word-break: break-all;
 
@@ -1827,13 +1908,13 @@ watch(
       font-size: 12px;
       font-style: normal;
       font-weight: 400;
-      color: @text-secondary;
+      color: #8c8c8c;
     }
   }
 
-  &.tone-gold .metric-item__icon {
-    background: #e8f1ff;
-    color: @primary-dark;
+  &.tone-primary .metric-item__icon {
+    background: @el-primary-light-9;
+    color: @primary-color;
   }
 
   &.tone-green .metric-item__icon {
@@ -1852,8 +1933,8 @@ watch(
   }
 
   &.tone-plain .metric-item__icon {
-    background: #f3f3f3;
-    color: @text-regular;
+    background: #f5f5f5;
+    color: #595959;
   }
 }
 
@@ -1861,7 +1942,7 @@ watch(
 .dashboard-view__tables {
   display: grid;
   grid-template-columns: repeat(2, 1fr);
-  gap: 12px;
+  gap: 14px;
 
   @media (max-width: 1100px) {
     grid-template-columns: 1fr;
@@ -1870,6 +1951,28 @@ watch(
   :deep(.content-panel) {
     margin-bottom: 0;
     height: 100%;
+    border: 1px solid @border-light;
+    border-radius: 8px;
+    box-shadow: none;
+    background: #fff;
+  }
+}
+
+/* 展板内嵌套 PagePanel:白壳内轻边框卡 */
+.village-workspace,
+.district-workspace {
+  :deep(.content-panel) {
+    border: 1px solid @border-light;
+    border-radius: 8px;
+    box-shadow: none;
+    background: #fff;
   }
 }
+
+.dashboard-todos-panel {
+  border: 1px solid @border-light !important;
+  border-radius: 8px !important;
+  box-shadow: none !important;
+  background: #fff !important;
+}
 </style>

+ 50 - 28
src/views/disk/DiskReturnView.vue

@@ -1,10 +1,6 @@
 <template>
   <div class="page-fill disk-return-view">
     <PagePanel title="回盘信息确认">
-      <template #extra>
-        <el-button type="primary" size="small" @click="openUploadDialog">回盘文件上传</el-button>
-      </template>
-
       <p class="page-tip">
         <template v-if="isAdmin">
           查看各镇回盘与归档。出盘、回盘、归档由对应镇社区事务办理。
@@ -13,23 +9,28 @@
           分管领导通过并出盘后,在此上传回盘并确认;完成后点「归档」闭环。失败人员可查看原因并处理告警。
         </template>
       </p>
-      <div v-if="isAdmin" class="query-panel disk-return-view__filter">
-        <el-form :inline="true" size="small">
-          <el-form-item label="街镇">
-            <el-select v-model="filterTown" placeholder="全部" clearable style="width: 140px">
-              <el-option v-for="t in townOptions" :key="t" :label="t" :value="t" />
-            </el-select>
-          </el-form-item>
-          <el-form-item>
-            <el-button type="primary" @click="applyFilter">查询</el-button>
-            <el-button @click="resetFilter">重置</el-button>
-          </el-form-item>
-        </el-form>
-      </div>
+
+      <ListPageToolbar
+        v-model:keyword="filterBatchNo"
+        keyword-placeholder="请输入批次号查询"
+        :actions="toolbarActions"
+        @search="applyFilter"
+        @refresh="onToolbarRefresh"
+      >
+        <template v-if="isAdmin" #filters>
+          <el-form label-position="top">
+            <el-form-item label="街镇">
+              <el-select v-model="filterTown" placeholder="全部" clearable style="width: 100%">
+                <el-option v-for="t in townOptions" :key="t" :label="t" :value="t" />
+              </el-select>
+            </el-form-item>
+          </el-form>
+        </template>
+      </ListPageToolbar>
 
       <div class="page-fill__table">
-      <el-table v-loading="loading" :data="pagedList" border stripe size="small" height="100%">
-        <el-table-column prop="batchNo" label="批次号" width="100" align="center" />
+      <el-table v-loading="loading" :data="displayList" border stripe size="small" height="100%">
+        <el-table-column prop="batchNo" label="批次号" width="100" />
         <el-table-column prop="town" label="街镇" width="90" />
         <el-table-column prop="village" label="村居" width="110" show-overflow-tooltip>
           <template #default="{ row }">{{ row.village || '—' }}</template>
@@ -40,24 +41,22 @@
             {{ row.returnFileName || '—' }}
           </template>
         </el-table-column>
-        <el-table-column prop="peopleCount" label="人数" width="72" align="center" />
+        <el-table-column prop="peopleCount" label="人数" width="72" />
         <el-table-column label="金额(元)" width="120" align="right">
           <template #default="{ row }">{{ formatAmount(row.amount) }}</template>
         </el-table-column>
-        <el-table-column label="清单状态" width="100" align="center">
+        <el-table-column label="清单状态" width="100" fixed="right">
           <template #default="{ row }">
             <StatusTag :status="row.status" />
           </template>
         </el-table-column>
-        <el-table-column label="回盘确认" width="100" align="center">
+        <el-table-column label="回盘确认" width="100" fixed="right">
           <template #default="{ row }">
             <StatusTag :status="row.returnStatus" />
           </template>
         </el-table-column>
-        <el-table-column label="失败人数" width="88" align="center">
-          <template #default="{ row }">{{ row.failCount ?? 0 }}</template>
-        </el-table-column>
-        <el-table-column label="操作" width="180" align="center" fixed="right">
+        <el-table-column label="失败人数" width="88" />
+        <el-table-column label="操作" width="180" fixed="right">
           <template #default="{ row }">
             <el-button
               v-if="row.returnStatus === '部分失败'"
@@ -209,6 +208,9 @@ import { computed, onMounted, ref, watch } from 'vue'
 import type { UploadFile, UploadInstance, UploadRawFile } from 'element-plus'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import PagePanel from '@/components/common/PagePanel.vue'
+import ListPageToolbar, {
+  type ListToolbarAction,
+} from '@/components/common/ListPageToolbar.vue'
 import StatusTag from '@/components/common/StatusTag.vue'
 import TablePagination from '@/components/common/TablePagination.vue'
 import {
@@ -227,6 +229,27 @@ import type { DiskFailPerson, DiskRecord } from '@/types/biz'
 
 const userStore = useUserStore()
 const isAdmin = computed(() => userStore.roleInfo?.key === 'admin')
+const filterBatchNo = ref('')
+
+const toolbarActions = computed<ListToolbarAction[]>(() => [
+  {
+    key: 'upload',
+    label: '回盘文件上传',
+    type: 'primary',
+    onClick: () => openUploadDialog(),
+  },
+])
+
+const displayList = computed(() => {
+  const kw = filterBatchNo.value.trim()
+  if (!kw) return pagedList.value
+  return pagedList.value.filter((r) => String(r.batchNo || '').includes(kw))
+})
+
+function onToolbarRefresh() {
+  filterBatchNo.value = ''
+  resetFilter()
+}
 const { filterTown, townOptions, resetTownVillage } = useTownVillageOptions()
 
 const archivingId = ref('')
@@ -414,8 +437,7 @@ watch(
 
 <style scoped lang="less">
 .disk-return-view__filter {
-  margin-bottom: 10px;
-  padding: 8px 12px 0;
+  /* 样式走全局 .content-panel .query-panel */
 }
 
 .disk-return-view__dialog-meta {

+ 113 - 77
src/views/insurance/InsuranceMatchView.vue

@@ -1,32 +1,6 @@
 <template>
   <div class="page-fill insurance-match-view">
     <PagePanel :title="panelTitle">
-      <template #extra>
-        <el-button
-          :loading="exporting"
-          :disabled="!canExport"
-          @click="onExport"
-        >
-          导出结果
-        </el-button>
-        <el-button
-          v-if="!isHistoryTab"
-          :disabled="!canAddPeople || addingPerson"
-          :loading="addingPerson"
-          @click="openAddPerson()"
-        >
-          新增人员并匹配
-        </el-button>
-        <el-button
-          v-if="!isHistoryTab"
-          type="primary"
-          :loading="matching"
-          :disabled="!selectedBatches.length || matching"
-          @click="onBatchMatch"
-        >
-          批量匹配
-        </el-button>
-      </template>
       <p class="page-tip">
         <template v-if="isAdmin && isHistoryTab">
           已提交领导及之后的各镇居保结果,只读。请按街镇筛选。
@@ -41,48 +15,63 @@
           核验全区当月批次的本镇人员。失败须在本页人工确认后,才能提交月度清单;已匹配完成的不会被全选勾中。测算锁定前可用「新增人员并匹配」把本镇人员补进当月批并单人匹配。
         </template>
       </p>
-      <el-tabs v-model="activeTab" @tab-change="onTabChange">
-        <el-tab-pane label="待办理" name="todo" />
-        <el-tab-pane label="历史记录" name="history" />
-      </el-tabs>
-      <div class="query-panel insurance-match-view__filter">
-        <el-form :inline="true" size="small">
-          <el-form-item v-if="isAdmin" label="街镇">
-            <el-select
-              v-model="filterTown"
-              placeholder="全部"
-              clearable
-              style="width: 140px"
-              @change="onTownChange"
-            >
-              <el-option v-for="t in townOptions" :key="t" :label="t" :value="t" />
-            </el-select>
-          </el-form-item>
-          <el-form-item label="村居">
-            <el-select v-model="filterVillage" placeholder="全部" clearable style="width: 140px">
-              <el-option v-for="v in villageOptions" :key="v" :label="v" :value="v" />
-            </el-select>
-          </el-form-item>
-          <el-form-item label="批次号">
-            <el-input
-              v-model="filterBatchNo"
-              placeholder="如 202608"
-              clearable
-              style="width: 130px"
-              @keyup.enter="applyFilter"
-            />
-          </el-form-item>
-          <el-form-item label="流转状态">
-            <el-select v-model="filterStage" placeholder="全部" clearable style="width: 150px">
-              <el-option v-for="s in stageOptions" :key="s" :label="s" :value="s" />
-            </el-select>
-          </el-form-item>
-          <el-form-item>
-            <el-button type="primary" @click="applyFilter">查询</el-button>
-            <el-button @click="resetFilter">重置</el-button>
-          </el-form-item>
-        </el-form>
+
+      <!-- 蓝湖风格 A:关键词 + 查询 + 多条件 + 刷新 | 操作最多 3 个 -->
+      <ListPageToolbar
+        v-model:keyword="filterBatchNo"
+        keyword-placeholder="请输入批次号查询"
+        :actions="toolbarActions"
+        @search="applyFilter"
+        @refresh="resetFilter"
+      >
+        <template #filters>
+          <el-form label-position="top" size="default" class="insurance-match-view__more-form">
+            <el-form-item v-if="isAdmin" label="街镇">
+              <el-select
+                v-model="filterTown"
+                placeholder="全部"
+                clearable
+                style="width: 100%"
+                @change="onTownChange"
+              >
+                <el-option v-for="t in townOptions" :key="t" :label="t" :value="t" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="村居">
+              <el-select v-model="filterVillage" placeholder="全部" clearable style="width: 100%">
+                <el-option v-for="v in villageOptions" :key="v" :label="v" :value="v" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="流转状态">
+              <el-select v-model="filterStage" placeholder="全部" clearable style="width: 100%">
+                <el-option v-for="s in stageOptions" :key="s" :label="s" :value="s" />
+              </el-select>
+            </el-form-item>
+          </el-form>
+        </template>
+      </ListPageToolbar>
+
+      <div class="style-a-tabs" role="tablist">
+        <button
+          type="button"
+          class="style-a-tab"
+          :class="{ 'is-active': activeTab === 'todo' }"
+          role="tab"
+          @click="switchTab('todo')"
+        >
+          待办理
+        </button>
+        <button
+          type="button"
+          class="style-a-tab"
+          :class="{ 'is-active': activeTab === 'history' }"
+          role="tab"
+          @click="switchTab('history')"
+        >
+          历史记录
+        </button>
       </div>
+
       <div class="page-fill__table">
       <el-table
         ref="tableRef"
@@ -102,7 +91,7 @@
           align="center"
           :selectable="isBatchSelectable"
         />
-        <el-table-column prop="batchNo" label="统一批次号" width="100" align="center" />
+        <el-table-column prop="batchNo" label="统一批次号" width="110" />
         <el-table-column prop="town" label="街镇" width="90" />
         <el-table-column label="范围" width="110">
           <template #default="{ row }">
@@ -117,18 +106,18 @@
             <span v-else>{{ row.village }}</span>
           </template>
         </el-table-column>
-        <el-table-column prop="peopleCount" label="人数" width="72" align="center" />
+        <el-table-column prop="peopleCount" label="人数" width="72" />
         <el-table-column label="预计金额(元)" width="120" align="right">
           <template #default="{ row }">{{ formatAmount(row.totalAmount) }}</template>
         </el-table-column>
-        <el-table-column label="流转状态" width="130" align="center">
+        <el-table-column label="流转状态" width="130" fixed="right">
           <template #default="{ row }">
             <StatusTag :status="row.stage" />
           </template>
         </el-table-column>
         <el-table-column prop="submitter" label="提交人" width="110" />
         <el-table-column prop="submittedAt" label="提交时间" min-width="140" />
-        <el-table-column label="操作" width="380" align="center" fixed="right">
+        <el-table-column label="操作" width="280" fixed="right">
           <template #default="{ row }">
             <el-button
               link
@@ -149,7 +138,7 @@
             <el-button
               v-if="row.stage === '居保状态匹配'"
               link
-              type="success"
+              type="primary"
               size="small"
               :loading="matching"
               :disabled="matching"
@@ -160,7 +149,7 @@
             <el-button
               v-else-if="canViewMatchResult(row as VillageBatch)"
               link
-              type="success"
+              type="primary"
               size="small"
               @click="openVillageResult(row as VillageBatch)"
             >
@@ -199,7 +188,8 @@
 
     <!-- 可研 5.1.4.1:任务态 / 失败清单 / 执行日志 -->
     <PagePanel v-if="lastTaskSummary || taskLogs.length" title="居保匹配任务执行情况">
-      <p class="insurance-match-view__tip">
+      <!-- 说明 tip 由全局 .page-tip { display:none } 隐藏,勿删 -->
+      <p class="page-tip insurance-match-view__tip">
         最近一次匹配的人数汇总、待人工处理名单与执行日志。
       </p>
       <div v-if="lastTaskSummary" class="insurance-match-view__task-summary">
@@ -392,6 +382,9 @@ import { computed, onMounted, ref, watch } from 'vue'
 import { ElMessage } from 'element-plus'
 import type { TableInstance } from 'element-plus'
 import PagePanel from '@/components/common/PagePanel.vue'
+import ListPageToolbar, {
+  type ListToolbarAction,
+} from '@/components/common/ListPageToolbar.vue'
 import StatusTag from '@/components/common/StatusTag.vue'
 import TablePagination from '@/components/common/TablePagination.vue'
 import {
@@ -439,6 +432,42 @@ const filterStage = ref('')
 const panelTitle = computed(() =>
   isHistoryTab.value ? '居保匹配历史记录' : '全区月批居保匹配(本镇切片)',
 )
+
+/** 风格 A:主区最多 3 个操作,超出进「…」 */
+const toolbarActions = computed<ListToolbarAction[]>(() => {
+  const list: ListToolbarAction[] = []
+  if (!isHistoryTab.value) {
+    list.push({
+      key: 'batch-match',
+      label: '批量匹配',
+      type: 'primary',
+      loading: matching.value,
+      disabled: !selectedBatches.value.length || matching.value,
+      onClick: () => void onBatchMatch(),
+    })
+    list.push({
+      key: 'add-person',
+      label: '新增人员并匹配',
+      loading: addingPerson.value,
+      disabled: !canAddPeople.value || addingPerson.value,
+      onClick: () => openAddPerson(),
+    })
+  }
+  list.push({
+    key: 'export',
+    label: '导出结果',
+    loading: exporting.value,
+    disabled: !canExport.value,
+    onClick: () => void onExport(),
+  })
+  return list
+})
+
+function switchTab(name: string) {
+  if (activeTab.value === name) return
+  activeTab.value = name
+  onTabChange()
+}
 const stageOptions = computed(() =>
   isHistoryTab.value
     ? ['分管领导审批', '待线下发放', '回盘信息确认', '已回盘确认', '已归档']
@@ -893,15 +922,22 @@ watch(
 }
 
 .insurance-match-view__filter {
-  margin-bottom: 10px;
-  padding: 8px 12px 0;
+  /* 样式走全局 .content-panel .query-panel */
 }
 
 .insurance-match-view__result-toolbar {
   display: flex;
   align-items: center;
-  gap: 12px;
+  flex-wrap: wrap;
+  gap: 8px 12px;
+  min-height: 36px;
   margin-bottom: 10px;
+
+  :deep(.el-button) {
+    min-height: 36px;
+    height: 36px;
+    padding: 0 16px;
+  }
 }
 
 .muted {

+ 72 - 46
src/views/monthly/MonthlyPaymentView.vue

@@ -1,11 +1,6 @@
 <template>
   <div class="page-fill monthly-payment-view">
     <PagePanel title="月补贴清单">
-      <template #extra>
-        <el-button size="small" :disabled="!calcBatchId" @click="onShowCalcRounds">
-          核算版本列表
-        </el-button>
-      </template>
       <p class="page-tip">
         <template v-if="isAdmin">
           查看各镇月补贴进度(镇统计,挂全区月批)。提交领导、出盘由对应镇社区事务办理;该镇特殊业务未齐时不可提交。
@@ -14,37 +9,43 @@
           明细为全区月批的本镇切片。须居保全部通过(或人工确认)且本镇当月特殊业务全部通过后,才可提交镇社区受理中心分管领导;通过后可出盘。
         </template>
       </p>
-      <div v-if="isAdmin" class="query-panel monthly-payment-view__filter">
-        <el-form :inline="true" size="small">
-          <el-form-item label="街镇">
-            <el-select
-              v-model="filterTown"
-              placeholder="全部"
-              clearable
-              style="width: 140px"
-              @change="onTownChange"
-            >
-              <el-option v-for="t in townOptions" :key="t" :label="t" :value="t" />
-            </el-select>
-          </el-form-item>
-          <el-form-item label="村居">
-            <el-select v-model="filterVillage" placeholder="全部" clearable style="width: 140px">
-              <el-option v-for="v in villageOptions" :key="v" :label="v" :value="v" />
-            </el-select>
-          </el-form-item>
-          <el-form-item>
-            <el-button type="primary" @click="applyFilter">查询</el-button>
-            <el-button @click="resetFilter">重置</el-button>
-          </el-form-item>
-        </el-form>
-      </div>
+
+      <ListPageToolbar
+        v-model:keyword="filterBatchNo"
+        keyword-placeholder="请输入批次号查询"
+        :actions="toolbarActions"
+        @search="applyFilter"
+        @refresh="onToolbarRefresh"
+      >
+        <template v-if="isAdmin" #filters>
+          <el-form label-position="top" class="monthly-payment-view__more-form">
+            <el-form-item label="街镇">
+              <el-select
+                v-model="filterTown"
+                placeholder="全部"
+                clearable
+                style="width: 100%"
+                @change="onTownChange"
+              >
+                <el-option v-for="t in townOptions" :key="t" :label="t" :value="t" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="村居">
+              <el-select v-model="filterVillage" placeholder="全部" clearable style="width: 100%">
+                <el-option v-for="v in villageOptions" :key="v" :label="v" :value="v" />
+              </el-select>
+            </el-form-item>
+          </el-form>
+        </template>
+      </ListPageToolbar>
+
       <div class="page-fill__table">
-      <el-table v-loading="loading" :data="pagedList" border stripe size="small" height="100%">
-        <el-table-column prop="batchNo" label="批次号" width="88" align="center" />
-        <el-table-column prop="month" label="发放月份" width="100" align="center" />
+      <el-table v-loading="loading" :data="displayList" border stripe size="small" height="100%">
+        <el-table-column prop="batchNo" label="批次号" width="88" />
+        <el-table-column prop="month" label="发放月份" width="100" />
         <el-table-column prop="town" label="街镇" width="90" />
         <el-table-column prop="village" label="范围" width="90" />
-        <el-table-column prop="peopleCount" label="人数" width="72" align="center" />
+        <el-table-column prop="peopleCount" label="人数" width="72" />
         <el-table-column label="正常金额" width="110" align="right">
           <template #default="{ row }">{{ formatAmount(row.normalAmount) }}</template>
         </el-table-column>
@@ -60,14 +61,15 @@
         <el-table-column label="应发合计" width="110" align="right">
           <template #default="{ row }">{{ formatAmount(row.totalAmount) }}</template>
         </el-table-column>
-        <el-table-column label="状态" width="100" align="center">
+        <el-table-column label="状态" width="100" fixed="right">
           <template #default="{ row }">
             <StatusTag :status="row.status" />
           </template>
         </el-table-column>
         <el-table-column prop="submittedAt" label="提交时间" min-width="140" />
-        <el-table-column label="操作" width="380" align="center" fixed="right">
+        <el-table-column label="操作" width="360" fixed="right">
           <template #default="{ row }">
+            <div class="table-ops">
             <el-button
               v-if="['待提交', '待审核', '已审核'].includes(row.status)"
               link
@@ -90,7 +92,7 @@
             <el-button
               v-if="['待提交', '待审核', '已审核'].includes(row.status)"
               link
-              :type="calcLockedMap[row.id] ? 'warning' : 'success'"
+              type="primary"
               size="small"
               @click="onToggleCalcLock(row as VillageMonthlyBatch)"
             >
@@ -115,11 +117,10 @@
             <el-button link type="primary" size="small" @click="openDetail(row as VillageMonthlyBatch)">
               人员明细
             </el-button>
-            <!-- 本批次待提交:提交分管领导审核 -->
             <el-button
               v-if="isCurrentBatch(row as VillageMonthlyBatch) && row.status === '待提交'"
               link
-              type="success"
+              type="primary"
               size="small"
               :disabled="!canSubmitLeader(row as VillageMonthlyBatch)"
               :loading="actingId === row.id"
@@ -139,25 +140,22 @@
             >
               本镇尚有 {{ pendingSpecialCount }} 笔特殊业务待审
             </span>
-            <!-- 本批次待审核 -->
             <span
               v-else-if="isCurrentBatch(row as VillageMonthlyBatch) && row.status === '待审核'"
               class="muted"
             >
               待分管领导审核
             </span>
-            <!-- 已审核:出盘 -->
             <el-button
               v-else-if="row.status === '已审核'"
               link
-              type="success"
+              type="primary"
               size="small"
               :loading="actingId === row.id"
               @click="onIssueDisk(row as VillageMonthlyBatch)"
             >
               出盘
             </el-button>
-            <!-- 已出盘:导出清单 -->
             <el-button
               v-else-if="row.status === '已出盘'"
               link
@@ -169,8 +167,8 @@
             >
               导出清单
             </el-button>
-            <!-- 已回盘 -->
             <span v-else-if="row.status === '已回盘'" class="muted">已闭环</span>
+            </div>
           </template>
         </el-table-column>
       </el-table>
@@ -407,6 +405,9 @@
 import { computed, onMounted, ref, watch } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import PagePanel from '@/components/common/PagePanel.vue'
+import ListPageToolbar, {
+  type ListToolbarAction,
+} from '@/components/common/ListPageToolbar.vue'
 import StatusTag from '@/components/common/StatusTag.vue'
 import TablePagination from '@/components/common/TablePagination.vue'
 import BatchDetailDrawer from '@/components/biz/BatchDetailDrawer.vue'
@@ -447,6 +448,7 @@ const {
   resetTownVillage,
 } = useTownVillageOptions()
 
+const filterBatchNo = ref('')
 const actingId = ref('')
 const pendingSpecialCount = ref(0)
 /** 待提交批次:居保未闭环人数(按批次 id) */
@@ -586,6 +588,32 @@ const {
 })
 const list = pagedList
 
+/** 关键词批次号:当前页内筛选(风格 A 主搜索) */
+const displayList = computed(() => {
+  const kw = filterBatchNo.value.trim()
+  if (!kw) return pagedList.value
+  return pagedList.value.filter(
+    (r) =>
+      String(r.batchNo || '').includes(kw) ||
+      String(r.month || '').includes(kw),
+  )
+})
+
+const toolbarActions = computed<ListToolbarAction[]>(() => [
+  {
+    key: 'calc-rounds',
+    label: '核算版本列表',
+    type: 'primary',
+    disabled: !calcBatchId.value,
+    onClick: () => void onShowCalcRounds(),
+  },
+])
+
+function onToolbarRefresh() {
+  filterBatchNo.value = ''
+  resetFilter()
+}
+
 const {
   page: detailPage,
   pageSize: detailPageSize,
@@ -856,8 +884,7 @@ watch(
   }
 
   &__filter {
-    margin-bottom: 10px;
-    padding: 8px 12px 0;
+    /* 样式走全局 .content-panel .query-panel */
   }
 
   &__sub-tip {
@@ -872,7 +899,6 @@ watch(
 }
 
 .muted {
-  margin-left: 6px;
   color: @text-secondary;
   font-size: 12px;
 }

+ 58 - 9
src/views/reports/ReportsView.vue

@@ -1,13 +1,14 @@
 <template>
   <div class="reports-view page-stack" v-loading="pageLoading">
     <div class="reports-view__toolbar">
+      <!-- 说明 tip 由全局 .page-tip { display:none } 隐藏,勿删 -->
       <p class="page-tip">{{ scopeTip }}</p>
-      <div class="reports-view__actions">
-        <el-button type="primary" :loading="exporting" @click="onExportSummary">导出月度汇总</el-button>
-        <el-button @click="onPrint">打印本页</el-button>
-        <el-button v-if="!isTown" :loading="topicLoading" @click="resetTopicPage">刷新专题</el-button>
-        <el-button v-if="isDistrict" :loading="yoyLoading" @click="loadYoy">刷新同比环比</el-button>
-      </div>
+      <ListPageToolbar
+        :show-keyword="false"
+        :actions="reportToolbarActions"
+        @search="resetTopicPage"
+        @refresh="resetTopicPage"
+      />
     </div>
 
     <!-- 可研 5.1.9.4:区级同比/环比 -->
@@ -27,7 +28,8 @@
         <el-tab-pane label="人员变动" name="personnel-change" />
         <el-tab-pane label="特殊业务" name="special-biz" />
       </el-tabs>
-      <p class="reports-view__yoy-meta">
+      <!-- 说明 tip 由全局 .page-tip { display:none } 隐藏,勿删 -->
+      <p class="page-tip">
         专题用于业务排查:发放异常看未关闭告警;人员变动看本月状态变化;特殊业务看审批与退款进度。街镇/状态均已转中文。
       </p>
       <el-table v-loading="topicLoading" :data="topicRows" border stripe size="small" max-height="280">
@@ -683,6 +685,9 @@ import { computed, onMounted, reactive, ref, toRef, watch } from 'vue'
 import { ElMessage } from 'element-plus'
 import StatCards from '@/components/common/StatCards.vue'
 import PagePanel from '@/components/common/PagePanel.vue'
+import ListPageToolbar, {
+  type ListToolbarAction,
+} from '@/components/common/ListPageToolbar.vue'
 import StatusTag from '@/components/common/StatusTag.vue'
 import TablePagination from '@/components/common/TablePagination.vue'
 import {
@@ -743,6 +748,41 @@ const peopleLoading = ref(false)
 const pageLoading = ref(false)
 const exporting = ref(false)
 const ledgerExporting = ref(false)
+
+const reportToolbarActions = computed<ListToolbarAction[]>(() => {
+  const list: ListToolbarAction[] = [
+    {
+      key: 'export',
+      label: '导出月度汇总',
+      type: 'primary',
+      loading: exporting.value,
+      onClick: () => void onExportSummary(),
+    },
+    {
+      key: 'print',
+      label: '打印本页',
+      onClick: () => onPrint(),
+    },
+  ]
+  if (!isTown.value) {
+    list.push({
+      key: 'topic',
+      label: '刷新专题',
+      loading: topicLoading.value,
+      onClick: () => resetTopicPage(),
+    })
+  }
+  if (isDistrict.value) {
+    list.push({
+      key: 'yoy',
+      label: '刷新同比环比',
+      loading: yoyLoading.value,
+      onClick: () => void loadYoy(),
+    })
+  }
+  return list
+})
+
 const revealSensitive = ref(false)
 const yoyLoading = ref(false)
 const topicTab = ref<ReportTopicType>('pay-abnormal')
@@ -1597,9 +1637,10 @@ watch(
 
 .reports-view__toolbar {
   display: flex;
-  align-items: flex-start;
-  justify-content: space-between;
+  align-items: center;
+  justify-content: flex-end;
   gap: 12px;
+  min-height: 36px;
   margin-bottom: 12px;
 
   .page-tip {
@@ -1612,8 +1653,16 @@ watch(
 .reports-view__actions {
   display: flex;
   flex-wrap: wrap;
+  align-items: center;
   gap: 8px;
   flex-shrink: 0;
+  min-height: 36px;
+
+  :deep(.el-button) {
+    min-height: 36px;
+    height: 36px;
+    padding: 0 16px;
+  }
 }
 
 .reports-view__yoy-meta {

+ 82 - 74
src/views/special/SpecialBusinessView.vue

@@ -14,7 +14,7 @@
         <!-- 人员状态维护:按村 Tab + 行内操作 -->
         <el-tab-pane label="人员状态维护" name="status">
           <div v-if="isAdmin" class="query-panel special-business-view__filter">
-            <el-form :inline="true" size="small">
+            <el-form :inline="true">
               <el-form-item label="街镇" required>
                 <el-select
                   v-model="adminTown"
@@ -39,26 +39,27 @@
             @tab-change="onVillageTabChange"
           >
             <el-tab-pane v-for="v in villageTabs" :key="v" :label="v" :name="v">
-              <div v-if="activeVillage === v" class="special-business-view__status-search">
-                <el-input
-                  v-model="statusSearchName"
-                  placeholder="姓名"
-                  clearable
-                  size="small"
-                  style="width: 120px"
-                  @keyup.enter="onStatusSearch"
-                />
-                <el-input
-                  v-model="statusSearchIdCard"
-                  placeholder="身份证(可后几位)"
-                  clearable
-                  size="small"
-                  style="width: 160px"
-                  @keyup.enter="onStatusSearch"
-                />
-                <el-button size="small" type="primary" @click="onStatusSearch">查询</el-button>
-                <el-button size="small" @click="resetStatusSearch">重置</el-button>
-              </div>
+              <ListPageToolbar
+                v-if="activeVillage === v"
+                v-model:keyword="statusSearchName"
+                keyword-placeholder="请输入姓名查询"
+                class="special-business-view__status-search"
+                @search="onStatusSearch"
+                @refresh="resetStatusSearch"
+              >
+                <template #filters>
+                  <el-form label-position="top">
+                    <el-form-item label="身份证">
+                      <el-input
+                        v-model="statusSearchIdCard"
+                        placeholder="可后几位"
+                        clearable
+                        style="width: 100%"
+                      />
+                    </el-form-item>
+                  </el-form>
+                </template>
+              </ListPageToolbar>
               <div v-if="activeVillage === v" class="page-fill__table">
               <el-table
                 v-loading="peopleLoading"
@@ -99,46 +100,48 @@
                 <el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
                 <el-table-column label="操作" width="260" align="center" fixed="right">
                   <template #default="{ row }">
-                    <el-button link type="primary" size="small" @click="openPersonDetail(row as Person)">
-                      详情
-                    </el-button>
-                    <template v-if="canOperate && canMaintainStatus(row as Person)">
-                      <el-button
-                        v-if="canStopPerson(row as Person)"
-                        link
-                        type="warning"
-                        size="small"
-                        @click="openSpecialDialog('暂停发放', row as Person)"
-                      >
-                        暂停
-                      </el-button>
-                      <el-button
-                        v-if="canStopPerson(row as Person)"
-                        link
-                        type="warning"
-                        size="small"
-                        @click="onPersonAction(row as Person, '停止')"
-                      >
-                        停止
-                      </el-button>
-                      <el-button
-                        v-if="canResumePerson(row as Person)"
-                        link
-                        type="success"
-                        size="small"
-                        @click="onPersonAction(row as Person, '恢复')"
-                      >
-                        恢复
+                    <div class="table-ops">
+                      <el-button link type="primary" size="small" @click="openPersonDetail(row as Person)">
+                        详情
                       </el-button>
-                      <el-button
-                        link
-                        type="primary"
-                        size="small"
-                        @click="openReissueDialog(row as Person)"
-                      >
-                        补发
-                      </el-button>
-                    </template>
+                      <template v-if="canOperate && canMaintainStatus(row as Person)">
+                        <el-button
+                          v-if="canStopPerson(row as Person)"
+                          link
+                          type="warning"
+                          size="small"
+                          @click="openSpecialDialog('暂停发放', row as Person)"
+                        >
+                          暂停
+                        </el-button>
+                        <el-button
+                          v-if="canStopPerson(row as Person)"
+                          link
+                          type="warning"
+                          size="small"
+                          @click="onPersonAction(row as Person, '停止')"
+                        >
+                          停止
+                        </el-button>
+                        <el-button
+                          v-if="canResumePerson(row as Person)"
+                          link
+                          type="success"
+                          size="small"
+                          @click="onPersonAction(row as Person, '恢复')"
+                        >
+                          恢复
+                        </el-button>
+                        <el-button
+                          link
+                          type="primary"
+                          size="small"
+                          @click="openReissueDialog(row as Person)"
+                        >
+                          补发
+                        </el-button>
+                      </template>
+                    </div>
                   </template>
                 </el-table-column>
               </el-table>
@@ -157,7 +160,7 @@
         <!-- 死亡人员录入 + 死亡状态更新 -->
         <el-tab-pane label="死亡人员录入" name="death">
           <div v-if="isAdmin" class="query-panel special-business-view__filter">
-            <el-form :inline="true" size="small">
+            <el-form :inline="true">
               <el-form-item label="街镇" required>
                 <el-select
                   v-model="adminTown"
@@ -216,7 +219,7 @@
         <!-- 丧葬补贴 -->
         <el-tab-pane label="丧葬补贴申请" name="funeral">
           <div v-if="isAdmin" class="query-panel special-business-view__filter">
-            <el-form :inline="true" size="small">
+            <el-form :inline="true">
               <el-form-item label="街镇">
                 <el-select
                   v-model="adminTown"
@@ -331,7 +334,7 @@
         <!-- 调标仅区人社:街镇不再发起。管理员可查看历史补发名单。 -->
         <el-tab-pane v-if="isAdmin" label="调标补发" name="adjust">
           <div v-if="isAdmin" class="query-panel special-business-view__filter">
-            <el-form :inline="true" size="small">
+            <el-form :inline="true">
               <el-form-item label="街镇" required>
                 <el-select
                   v-model="adminTown"
@@ -395,6 +398,7 @@
       v-model="deathDialogVisible"
       title="死亡人员信息录入"
       width="640px"
+      align-center
       destroy-on-close
       @closed="onDeathDialogClosed"
     >
@@ -939,6 +943,7 @@
 import { computed, onMounted, reactive, ref, watch } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import PagePanel from '@/components/common/PagePanel.vue'
+import ListPageToolbar from '@/components/common/ListPageToolbar.vue'
 import StatusTag from '@/components/common/StatusTag.vue'
 import TablePagination from '@/components/common/TablePagination.vue'
 import FlowAttachments from '@/components/biz/FlowAttachments.vue'
@@ -2241,19 +2246,20 @@ watch(
   .refund-stat-card {
     min-width: 110px;
     padding: 10px 14px;
-    background: #f7f3eb;
-    border: 1px solid #e8dfd0;
+    background: @el-primary-light-9;
+    border: 1px solid @border-color;
+    border-radius: @radius-md;
 
     &__label {
       font-size: 12px;
-      color: #8a7a66;
+      color: #8a9099;
     }
 
     &__value {
       margin-top: 4px;
       font-size: 20px;
       font-weight: 600;
-      color: #5c4a32;
+      color: #1f2329;
     }
   }
 
@@ -2265,8 +2271,7 @@ watch(
   }
 
   &__filter {
-    margin-bottom: 10px;
-    padding: 8px 12px 0;
+    /* 样式走全局 .content-panel .query-panel */
     flex-shrink: 0;
   }
 
@@ -2281,17 +2286,20 @@ watch(
   &__toolbar {
     display: flex;
     flex-wrap: wrap;
+    align-items: center;
     gap: 8px;
+    min-height: 36px;
     margin-bottom: 10px;
     flex-shrink: 0;
+
+    :deep(.el-button) {
+      min-height: 36px;
+      height: 36px;
+      padding: 0 16px;
+    }
   }
 
   &__status-search {
-    display: flex;
-    flex-wrap: wrap;
-    align-items: center;
-    gap: 8px;
-    margin-bottom: 8px;
     flex-shrink: 0;
   }
 

+ 57 - 28
src/views/status/StatusMaintenanceView.vue

@@ -1,27 +1,6 @@
 <template>
   <div class="page-fill status-maintenance-view">
     <PagePanel title="补助标准管理">
-      <template #extra>
-        <el-button
-          v-if="canDistrictAdjust"
-          type="warning"
-          size="small"
-          @click="openDistrictAdjust"
-        >
-          全区调标
-        </el-button>
-        <el-button
-          v-if="canMutateStandard"
-          type="primary"
-          size="small"
-          :disabled="!canOpenCreate"
-          :title="createBlockedTip || undefined"
-          @click="openCreateDialog"
-        >
-          新增标准
-        </el-button>
-      </template>
-
       <el-alert
         type="info"
         :closable="false"
@@ -38,18 +17,27 @@
         :title="pendingTip"
       />
 
+      <ListPageToolbar
+        v-model:keyword="filterYear"
+        keyword-placeholder="请输入年度查询"
+        :actions="toolbarActions"
+        :show-keyword="true"
+        @search="applyYearFilter"
+        @refresh="onToolbarRefresh"
+      />
+
       <div class="page-fill__table">
-      <el-table v-loading="standardLoading" :data="pagedStandards" border stripe size="small" height="100%">
-        <el-table-column prop="year" label="年度" width="80" align="center" />
+      <el-table v-loading="standardLoading" :data="displayStandards" border stripe size="small" height="100%">
+        <el-table-column prop="year" label="年度" width="80" />
         <el-table-column label="月补助(元)" width="110" align="right">
           <template #default="{ row }">{{ formatAmount(row.monthly) }}</template>
         </el-table-column>
         <el-table-column label="丧葬补助(元)" width="120" align="right">
           <template #default="{ row }">{{ formatAmount(row.funeral) }}</template>
         </el-table-column>
-        <el-table-column prop="date" label="生效日期" width="110" align="center" />
+        <el-table-column prop="date" label="生效日期" width="110" />
         <el-table-column prop="reason" label="调整原因" min-width="160" show-overflow-tooltip />
-        <el-table-column label="当前" width="72" align="center">
+        <el-table-column label="当前" width="72" fixed="right">
           <template #default="{ row }">
             <el-tag v-if="isCurrentStandard(row as SubsidyStandard)" size="small" type="success">
               有效
@@ -57,7 +45,7 @@
             <span v-else class="muted">历史</span>
           </template>
         </el-table-column>
-        <el-table-column label="操作" width="200" align="center" fixed="right">
+        <el-table-column label="操作" width="200" fixed="right">
           <template #default="{ row }">
             <template v-if="isCurrentStandard(row as SubsidyStandard)">
               <el-button
@@ -257,6 +245,9 @@ import { computed, onMounted, reactive, ref, watch } from 'vue'
 import type { FormInstance, FormRules } from 'element-plus'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import PagePanel from '@/components/common/PagePanel.vue'
+import ListPageToolbar, {
+  type ListToolbarAction,
+} from '@/components/common/ListPageToolbar.vue'
 import TablePagination from '@/components/common/TablePagination.vue'
 import {
   createStandard,
@@ -278,6 +269,45 @@ const isAdmin = computed(() => userStore.roleInfo?.key === 'admin')
 const isHr = computed(() => userStore.roleInfo?.key === 'hr')
 const canMutateStandard = computed(() => isAdmin.value)
 const canDistrictAdjust = computed(() => isHr.value || isAdmin.value)
+const filterYear = ref('')
+const yearKeyword = ref('')
+
+const toolbarActions = computed<ListToolbarAction[]>(() => {
+  const list: ListToolbarAction[] = []
+  if (canMutateStandard.value) {
+    list.push({
+      key: 'create',
+      label: '新增标准',
+      type: 'primary',
+      disabled: !canOpenCreate.value,
+      onClick: () => openCreateDialog(),
+    })
+  }
+  if (canDistrictAdjust.value) {
+    list.push({
+      key: 'adjust',
+      label: '全区调标',
+      type: 'warning',
+      onClick: () => openDistrictAdjust(),
+    })
+  }
+  return list
+})
+
+const displayStandards = computed(() => {
+  const kw = yearKeyword.value.trim()
+  if (!kw) return pagedStandards.value
+  return pagedStandards.value.filter((r) => String(r.year || '').includes(kw))
+})
+
+function applyYearFilter() {
+  yearKeyword.value = filterYear.value
+}
+
+function onToolbarRefresh() {
+  filterYear.value = ''
+  yearKeyword.value = ''
+}
 const standardAlertTitle = computed(() => {
   if (isHr.value) {
     return '补助标准全区统一,不按街镇分别维护。调标仅由区人社办理;确认后更新全区标准并按各镇生成补发批次。'
@@ -580,8 +610,7 @@ watch(
   }
 
   &__filter {
-    margin-bottom: 10px;
-    padding: 8px 12px 0;
+    /* 样式走全局 .content-panel .query-panel */
   }
 
   &__field-tip {

+ 4 - 4
src/views/system/SystemAdminView.vue

@@ -29,13 +29,13 @@
           <template #default="{ row }">{{ roleLabel(row.roleKey) }}</template>
         </el-table-column>
         <el-table-column prop="org" label="所属机构" min-width="140" show-overflow-tooltip />
-        <el-table-column label="状态" width="88" align="center">
+        <el-table-column label="状态" width="88" fixed="right">
           <template #default="{ row }">
             <StatusTag :status="row.status" />
           </template>
         </el-table-column>
         <el-table-column prop="lastLogin" label="最近登录" width="150" />
-        <el-table-column label="操作" width="220" align="center" fixed="right">
+        <el-table-column label="操作" width="220" fixed="right">
           <template #default="{ row }">
             <el-button link type="primary" size="small" @click="openAccountDialog(row as AccountItem)">
               编辑
@@ -656,7 +656,7 @@ onMounted(() => {
 .system-admin-view__hint {
   margin-left: 10px;
   font-size: 12px;
-  color: #8a7a66;
+  color: #8a9099;
 }
 
 .system-admin-view__reset-text {
@@ -676,7 +676,7 @@ onMounted(() => {
 
   em {
     font-style: normal;
-    color: #b45309;
+    color: #fa8c16;
     font-weight: 600;
   }
 }