Bläddra i källkod

feat(frontend): 案件管理页卡片化改版

- 满宽工具栏与内容卡,案件卡片加高并在卡片内滚动
- 案件类型标签去除红色,图标提示改为白底黑字,删除操作默认红色
cc 1 dag sedan
förälder
incheckning
e903a4029a
1 ändrade filer med 711 tillägg och 207 borttagningar
  1. 711 207
      ai-electron/frontend/src/plat/views/case/case.vue

+ 711 - 207
ai-electron/frontend/src/plat/views/case/case.vue

@@ -1,109 +1,135 @@
 <template>
-  <div :class="prefixCls" class="relative flex h-full min-h-0 w-full flex-col overflow-hidden bg-light-400">
+  <div :class="prefixCls" class="case-page">
     <AppHeader>
       <template #actions>
         <UserDropdown />
       </template>
     </AppHeader>
-    <div class="container relative mx-auto flex h-full min-h-0 flex-1 flex-col py-2">
-      <div class="flex h-full min-h-0 flex-col">
-        <div class="flex items-start justify-between gap-4">
-          <div>
-            <h1 class="text-2xl">案件管理</h1>
-            <p class="mt-2 text-gray-600 dark:text-gray-400">管理您的所有案件</p>
+
+    <main class="case-page__body">
+      <div class="case-page__inner">
+        <!-- 页头:标题、说明与案件总数 -->
+        <header class="case-page__head">
+          <div class="case-page__title-wrap">
+            <h1 class="case-page__title">案件管理</h1>
+            <p class="case-page__desc">管理您的所有案件,打开案件后即可开始数据研判。</p>
           </div>
-        </div>
-
-        <div class="mb-6">
-          <Input
-            v-model:value="searchKeyword"
-            placeholder="请输入案件名称进行搜索"
-            size="large"
-            allowClear
-            autocomplete="off"
-            @change="handleSearch"
-          >
-            <template #prefix>
-              <SearchOutlined />
-            </template>
-          </Input>
-        </div>
-
-        <div class="min-h-0 flex-1 overflow-y-auto overflow-x-hidden pt-4">
-          <div class="grid grid-cols-1 gap-6 pb-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
-            <Card
-              class="case-card add-card hover:shadow-lg transition-shadow duration-300 cursor-pointer border-dashed"
-              @click="handleAdd"
+          <span class="case-page__total">共 {{ caseList.length }} 个案件</span>
+        </header>
+
+        <!-- 工具栏卡:搜索 + 刷新 + 新建 -->
+        <section class="case-page__toolbar">
+          <label class="case-page__search">
+            <SearchOutlined />
+            <input v-model="searchKeyword" type="text" placeholder="请输入案件名称进行搜索" autocomplete="off" />
+            <button
+              v-if="searchKeyword"
+              class="case-page__search-clear"
+              type="button"
+              aria-label="清空搜索"
+              @click="searchKeyword = ''"
             >
-              <div class="flex flex-col items-center justify-center py-8">
-                <PlusOutlined class="text-4xl text-gray-400 mb-4" />
-                <span class="text-gray-500 text-lg">点击创建案件</span>
-              </div>
-            </Card>
+              <CloseCircleOutlined />
+            </button>
+          </label>
+          <span class="case-page__spacer"></span>
+          <div class="case-page__actions">
+            <Button :loading="loading" @click="loadCaseList">
+              <template #icon><ReloadOutlined /></template>
+              刷新
+            </Button>
+            <Button type="primary" @click="handleAdd">
+              <template #icon><PlusOutlined /></template>
+              新建案件
+            </Button>
+          </div>
+        </section>
+
+        <!-- 内容卡:案件卡片网格 -->
+        <section class="case-page__panel">
+          <div class="case-page__panel-head">
+            <span class="case-page__panel-title">案件列表</span>
+            <span class="case-page__panel-desc">按创建时间倒序排列,点击卡片打开案件。</span>
+            <span class="case-page__spacer"></span>
+            <span class="case-page__panel-count">共 {{ filteredCaseList.length }} 个</span>
+          </div>
 
-            <Card
-              size="small"
-              v-for="item in filteredCaseList"
-              :key="item.id"
-              class="case-card hover:shadow-lg transition-shadow duration-300"
-            >
-              <template #title>
-                <div class="flex items-center justify-between">
-                  <span class="font-semibold text-lg truncate flex-1">{{ item.name }}</span>
-                  <div class="flex items-center">
-                    <Button type="link" size="small" color="primary" :skill="true" @click="handleEdit(item)">
-                      <template #icon>
+          <div class="case-page__panel-body">
+            <Spin :spinning="loading">
+              <div v-if="filteredCaseList.length" class="case-page__grid">
+                <!-- 新建案件卡 -->
+                <button class="case-card case-card--add" type="button" @click="handleAdd">
+                  <span class="case-card__add-icon"><PlusOutlined /></span>
+                  <span class="case-card__add-title">新建案件</span>
+                  <span class="case-card__add-desc">创建新的案件研判空间</span>
+                </button>
+
+                <article v-for="item in filteredCaseList" :key="item.id" class="case-card">
+                  <div class="case-card__head">
+                    <span class="case-card__icon">{{ (item.name || '?').charAt(0) }}</span>
+                    <div class="case-card__heading">
+                      <div class="case-card__name-row">
+                        <span class="case-card__name" :title="item.name">{{ item.name }}</span>
+                        <span class="case-card__type" :class="getTypeClass(item.type)">
+                          {{ item.type || '未分类' }}
+                        </span>
+                      </div>
+                      <span class="case-card__time">创建于 {{ item.createTime || '—' }}</span>
+                    </div>
+                  </div>
+
+                  <p class="case-card__remark" :title="item.remark || '-'">
+                    {{ item.remark || '暂无描述' }}
+                  </p>
+
+                  <div class="case-card__ops">
+                    <button class="case-card__open" type="button" @click="handleOpen(item.id!)">
+                      <FolderOpenOutlined />
+                      打开案件
+                    </button>
+                    <span class="case-card__ops-spacer"></span>
+                    <Tooltip title="编辑案件" overlay-class-name="case-card-tooltip">
+                      <button class="case-card__iconbtn" type="button" aria-label="编辑案件" @click="handleEdit(item)">
                         <EditOutlined />
-                      </template>
-                    </Button>
-                    <Button type="link" size="small" danger :skill="true" @click="handleDelete(item.id!)">
-                      <template #icon>
+                      </button>
+                    </Tooltip>
+                    <Tooltip title="删除案件" overlay-class-name="case-card-tooltip">
+                      <button
+                        class="case-card__iconbtn is-danger"
+                        type="button"
+                        aria-label="删除案件"
+                        @click="handleDelete(item.id!)"
+                      >
                         <DeleteOutlined />
-                      </template>
-                    </Button>
+                      </button>
+                    </Tooltip>
                   </div>
-                </div>
-              </template>
-
-              <div>
-                <div class="mb-3">
-                  <span class="text-gray-500 dark:text-gray-400 text-sm">案件类型:</span>
-                  <Tag :color="getTypeColor(item.type)">{{ item.type }}</Tag>
-                </div>
-                <div class="mb-3">
-                  <span class="text-gray-500 dark:text-gray-400 text-sm">创建时间:</span>
-                  <span class="text-gray-700 dark:text-gray-300 text-sm">{{ item.createTime }}</span>
-                </div>
-                <div>
-                  <span class="text-gray-500 dark:text-gray-400 text-sm">描述:</span>
-                  <span class="case-card__remark text-gray-700 dark:text-gray-300 text-sm" :title="item.remark || '-'">
-                    {{ item.remark || '-' }}
-                  </span>
-                </div>
+                </article>
               </div>
 
-              <template #actions>
-                <div class="flex justify-center">
-                  <Button type="primary" :skill="true" @click="handleOpen(item.id!)">
-                    <template #icon>
-                      <FolderOpenOutlined />
-                    </template>
-                    打开案件
-                  </Button>
-                </div>
-              </template>
-            </Card>
+              <EmptyState
+                v-else-if="!loading"
+                :title="searchKeyword.trim() ? '没有匹配的案件' : '还没有案件'"
+                :description="
+                  searchKeyword.trim()
+                    ? '换个案件名称关键词试试,或清空搜索条件查看全部案件。'
+                    : '点击右上角「新建案件」创建第一个案件。'
+                "
+                :min-height="320"
+                description-width="min(100%, 520px)"
+              />
+            </Spin>
           </div>
-        </div>
+        </section>
       </div>
-    </div>
+    </main>
 
-    <div class="case-footer flex-shrink-0 border-t border-gray-200 bg-white px-6 py-2.5">
-      <div class="mx-auto flex max-w-7xl items-center justify-between text-xs text-gray-400">
+    <footer class="case-page__footer">
+      <div class="case-page__footer-inner">
         <span>© 2026 中数矩阵(成都)科技有限公司</span>
         <span>联系电话:18908093240</span>
       </div>
-    </div>
+    </footer>
 
     <CaseForm @register="registerModal" @success="handleSuccess" @openCase="handleOpenCreatedCase" />
 
@@ -115,41 +141,41 @@
       @ok="confirmDelete"
       @cancel="cancelDelete"
     >
-      <div class="py-4">
-        <p class="m-0 text-sm text-gray-500">
-          删除后该案件的数据库文件与工作空间会被一并清理,且不可恢复。
-        </p>
-      </div>
+      <div class="case-page__delete-tip"> 删除后该案件的数据库文件与工作空间会被一并清理,且不可恢复。 </div>
     </Modal>
   </div>
 </template>
 
 <script lang="ts" setup>
   import { computed, onMounted, ref, toRaw } from 'vue';
-  import { Card, Button, Tag, Input, Modal, Tooltip } from 'ant-design-vue';
+  import { Button, Modal, Spin, Tooltip } from 'ant-design-vue';
   import {
+    CloseCircleOutlined,
     DeleteOutlined,
     FolderOpenOutlined,
     SearchOutlined,
     PlusOutlined,
     EditOutlined,
+    ReloadOutlined,
   } from '@ant-design/icons-vue';
   import { useDesign } from '@/core/hooks/web/useDesign';
   import { useMessage } from '@/core/hooks/web/useMessage';
   import { useUserStore } from '@/core/store/modules/case';
   import { useModal } from '@/core/components/Modal';
+  import { EmptyState } from '@/core/components/EmptyState';
   import { AppHeader } from '@/core/components/Application';
   import { UserDropdown } from '@/core/layouts/default/header/components';
   import { list, deleteById } from '@/plat/api/case/caseApi';
   import type { CaseInfo } from '@/plat/api/case/caseApi';
   import CaseForm from './CaseForm.vue';
 
-  const { showMessage, notification } = useMessage();
+  const { showMessage } = useMessage();
   const { prefixCls } = useDesign('case-list');
   const userStore = useUserStore();
 
   const caseList = ref<CaseInfo[]>([]);
   const searchKeyword = ref('');
+  const loading = ref(false);
   const deleteModalVisible = ref(false);
   const deleteLoading = ref(false);
   const currentDeleteId = ref<number>(0);
@@ -157,8 +183,13 @@
   const [registerModal, { openModal }] = useModal();
 
   async function loadCaseList() {
-    const data = await list();
-    caseList.value = data || [];
+    loading.value = true;
+    try {
+      const data = await list();
+      caseList.value = data || [];
+    } finally {
+      loading.value = false;
+    }
   }
 
   onMounted(() => {
@@ -171,9 +202,6 @@
     const timestamp = new Date(rawTime.replace(/-/g, '/')).getTime();
     return Number.isNaN(timestamp) ? 0 : timestamp;
   }
-  const getLogoWidth = computed(() => {
-    return {};
-  });
   const filteredCaseList = computed(() => {
     const sortedList = [...caseList.value].sort((left, right) => {
       const timeDiff = getCaseSortTime(right) - getCaseSortTime(left);
@@ -188,20 +216,20 @@
     return sortedList.filter((item) => item.name.toLowerCase().includes(keyword));
   });
 
-  function handleSearch() {
-    console.log('搜索关键词:', searchKeyword.value);
-  }
-
-  function getTypeColor(type: string): string {
-    const colorMap: Record<string, string> = {
-      刑事案件: 'red',
-      民事案件: 'blue',
-      行政案件: 'orange',
-      经济案件: 'green',
-      知识产权: 'purple',
-      劳动争议: 'cyan',
+  function getTypeClass(type?: string): string {
+    const classMap: Record<string, string> = {
+      刑事案件: 'is-criminal',
+      贪污: 'is-criminal',
+      受贿: 'is-criminal',
+      行贿: 'is-criminal',
+      诈骗: 'is-criminal',
+      民事案件: 'is-civil',
+      行政案件: 'is-admin',
+      经济案件: 'is-economic',
+      知识产权: 'is-ip',
+      劳动争议: 'is-labor',
     };
-    return colorMap[type] || 'default';
+    return classMap[type || ''] || '';
   }
 
   async function handleDelete(id: number) {
@@ -284,121 +312,597 @@
   }
 </script>
 
-<style lang="less">
-  @prefix-cls: ~'jeesite-case-list';
+<style lang="less" scoped>
+  .case-page {
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+    height: 100%;
+    min-height: 0;
+    overflow: hidden;
+    background: #f4f6fa;
+  }
 
-  .@{prefix-cls} {
-    min-height: 100%;
+  .case-page__body {
     display: flex;
+    flex: 1 1 auto;
     flex-direction: column;
+    min-height: 0;
+    padding: 8px 10px;
+    overflow: hidden;
+  }
 
-    .case-card {
-      border-radius: 12px;
-      transition: all 0.3s ease;
+  .case-page__inner {
+    display: flex;
+    flex: 1 1 auto;
+    flex-direction: column;
+    gap: 8px;
+    width: 100%;
+    min-height: 0;
+  }
 
-      &:hover {
-        transform: translateY(-4px);
-        box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
-      }
+  .case-page__head {
+    display: flex;
+    align-items: flex-start;
+    gap: 16px;
+    padding: 2px 2px 0;
+  }
 
-      &.add-card {
-        border: 2px dashed #d9d9d9;
-        background: #fafafa;
-        min-height: 229px;
-        display: flex;
-        align-items: center;
-        justify-content: center;
-
-        .ant-card-body {
-          padding: 0;
-          width: 100%;
-          display: flex;
-          align-items: center;
-          justify-content: center;
-        }
-
-        &:hover {
-          border-color: #3f61ee;
-          background: #e6f7ff;
-
-          .anticon-plus {
-            color: #3f61ee;
-          }
-        }
-      }
+  .case-page__title-wrap {
+    flex: 1 1 auto;
+    min-width: 0;
+  }
+
+  .case-page__title {
+    margin: 0;
+    color: #111827;
+    font-size: 22px;
+    font-weight: 600;
+    line-height: 1.35;
+  }
+
+  .case-page__desc {
+    margin: 4px 0 0;
+    color: #5b6577;
+    font-size: 12.5px;
+    line-height: 1.7;
+  }
 
-      .ant-card-head {
-        border-bottom: 1px solid #f0f0f0;
-        padding: 16px;
+  .case-page__total {
+    flex: 0 0 auto;
+    height: 24px;
+    padding: 0 10px;
+    color: #2a50ec;
+    font-size: 11.5px;
+    font-weight: 500;
+    line-height: 22px;
+    white-space: nowrap;
+    background: rgb(42 80 236 / 7%);
+    border: 1px solid rgb(42 80 236 / 16%);
+    border-radius: 999px;
+  }
+
+  .case-page__toolbar {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    min-height: 48px;
+    padding: 8px 12px;
+    background: #fff;
+    border: 1px solid #e7ebf3;
+    border-radius: 14px;
+    box-shadow: 0 1px 2px rgb(16 24 40 / 4%);
+    box-sizing: border-box;
+  }
 
-        .ant-card-head-title {
-          padding: 0;
-        }
+  .case-page__search {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    width: 340px;
+    height: 32px;
+    padding: 0 11px;
+    color: #98a2b3;
+    background: #fff;
+    border: 1px solid #e7ebf3;
+    border-radius: 8px;
+    cursor: text;
+    transition:
+      border-color 0.15s ease,
+      box-shadow 0.15s ease;
+
+    &:focus-within {
+      border-color: #2a50ec;
+      box-shadow: 0 0 0 2px rgb(42 80 236 / 12%);
+    }
+
+    input {
+      flex: 1;
+      min-width: 0;
+      color: #111827;
+      font-size: 12px;
+      font-family: inherit;
+      background: transparent;
+      border: 0;
+      outline: none;
+
+      &::placeholder {
+        color: #98a2b3;
       }
+    }
+  }
+
+  .case-page__search-clear {
+    display: inline-flex;
+    flex: 0 0 auto;
+    align-items: center;
+    justify-content: center;
+    width: 18px;
+    height: 18px;
+    padding: 0;
+    color: #98a2b3;
+    background: transparent;
+    border: 0;
+    border-radius: 50%;
+    cursor: pointer;
+
+    &:hover {
+      color: #5b6577;
+      background: #f1f5f9;
+    }
+  }
+
+  .case-page__spacer {
+    flex: 1 1 auto;
+  }
 
-      .ant-card-body {
-        padding: 10px;
+  .case-page__actions {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+
+    :deep(.ant-btn) {
+      height: 30px;
+      padding: 0 14px;
+      color: #5b6577;
+      font-size: 12px;
+      font-weight: 500;
+      background: #fff;
+      border: 1px solid #e7ebf3;
+      border-radius: 8px;
+      box-shadow: none;
+
+      &:hover,
+      &:focus {
+        color: #2a50ec;
+        background: #f8faff;
+        border-color: #bfcdfb;
       }
+    }
+
+    :deep(.ant-btn-primary) {
+      color: #fff;
+      background: #2a50ec;
+      border-color: #2a50ec;
 
-      .case-card__remark {
-        display: inline-block;
-        max-width: calc(100% - 52px);
-        overflow: hidden;
-        text-overflow: ellipsis;
-        white-space: nowrap;
-        vertical-align: bottom;
+      &:hover,
+      &:focus {
+        color: #fff;
+        background: #4b6bf0;
+        border-color: #4b6bf0;
       }
 
-      .ant-card-actions {
-        border-top: 1px solid #f0f0f0;
-        background: #fafafa;
+      &:active {
+        background: #1e3ed0;
+        border-color: #1e3ed0;
       }
     }
   }
 
-  html[data-theme='dark'] {
-    .@{prefix-cls} {
-      background-color: #141414;
-
-      .case-card {
-        background: #1f1f1f;
-        border-color: #303030;
-
-        &.add-card {
-          border-color: #434343;
-          background: #1f1f1f;
-
-          .ant-card-body {
-            padding: 0;
-            width: 100%;
-            display: flex;
-            align-items: center;
-            justify-content: center;
-          }
-
-          &:hover {
-            border-color: #177ddc;
-            background: #111b26;
-          }
-        }
-
-        .ant-card-head {
-          border-bottom-color: #303030;
-        }
-
-        .ant-card-body {
-          color: rgba(255, 255, 255, 0.85);
-        }
-
-        .ant-card-actions {
-          border-top-color: #303030;
-          background: #141414;
-        }
-
-        &:hover {
-          box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
-        }
+  .case-page__panel {
+    display: flex;
+    flex: 1 1 auto;
+    flex-direction: column;
+    min-height: 0;
+    background: #fff;
+    border: 1px solid #e7ebf3;
+    border-radius: 14px;
+    box-shadow: 0 1px 2px rgb(16 24 40 / 4%);
+    overflow: hidden;
+  }
+
+  .case-page__panel-head {
+    display: flex;
+    flex: 0 0 auto;
+    align-items: center;
+    gap: 10px;
+    min-height: 44px;
+    padding: 8px 12px;
+    border-bottom: 1px solid #eff2f7;
+    box-sizing: border-box;
+  }
+
+  .case-page__panel-title {
+    position: relative;
+    padding-left: 11px;
+    color: #111827;
+    font-size: 15px;
+    font-weight: 600;
+    line-height: 22px;
+    white-space: nowrap;
+
+    &::before {
+      content: '';
+      position: absolute;
+      top: 4px;
+      bottom: 4px;
+      left: 0;
+      width: 3px;
+      background: #2a50ec;
+      border-radius: 2px;
+    }
+  }
+
+  .case-page__panel-desc {
+    min-width: 0;
+    overflow: hidden;
+    color: #98a2b3;
+    font-size: 11.5px;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+  }
+
+  .case-page__panel-count {
+    color: #98a2b3;
+    font-size: 11.5px;
+    white-space: nowrap;
+    font-variant-numeric: tabular-nums;
+  }
+
+  .case-page__panel-body {
+    flex: 1 1 auto;
+    min-height: 0;
+    padding: 12px;
+    overflow-y: auto;
+    overscroll-behavior: contain;
+    box-sizing: border-box;
+  }
+
+  .case-page__grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
+    gap: 10px;
+  }
+
+  .case-card {
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+    min-width: 0;
+    min-height: 210px;
+    padding: 14px;
+    font-family: inherit;
+    text-align: left;
+    background: #fff;
+    border: 1px solid #e7ebf3;
+    border-radius: 12px;
+    box-sizing: border-box;
+    transition:
+      border-color 0.15s ease,
+      box-shadow 0.15s ease;
+
+    &:hover {
+      border-color: #bfcdfb;
+      box-shadow: 0 4px 14px rgb(16 24 40 / 8%);
+    }
+  }
+
+  .case-card__head {
+    display: flex;
+    align-items: flex-start;
+    gap: 10px;
+    min-width: 0;
+  }
+
+  .case-card__icon {
+    display: inline-flex;
+    flex: 0 0 auto;
+    align-items: center;
+    justify-content: center;
+    width: 34px;
+    height: 34px;
+    color: #2a50ec;
+    font-size: 15px;
+    font-weight: 600;
+    background: rgb(42 80 236 / 8%);
+    border-radius: 9px;
+  }
+
+  .case-card__heading {
+    display: flex;
+    flex: 1 1 auto;
+    flex-direction: column;
+    gap: 4px;
+    min-width: 0;
+  }
+
+  .case-card__name-row {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    min-width: 0;
+  }
+
+  .case-card__name {
+    flex: 1 1 auto;
+    min-width: 0;
+    overflow: hidden;
+    color: #111827;
+    font-size: 14px;
+    font-weight: 600;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+  }
+
+  .case-card__time {
+    overflow: hidden;
+    color: #98a2b3;
+    font-size: 11.5px;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+  }
+
+  .case-card__type {
+    flex: 0 0 auto;
+    height: 20px;
+    padding: 0 7px;
+    color: #5b6577;
+    font-size: 11px;
+    font-weight: 500;
+    line-height: 18px;
+    white-space: nowrap;
+    background: #f4f6fa;
+    border: 1px solid #e7ebf3;
+    border-radius: 6px;
+
+    &.is-criminal {
+      color: #475569;
+      background: #eef2f7;
+      border-color: #d7e0ea;
+    }
+
+    &.is-civil {
+      color: #2a50ec;
+      background: rgb(42 80 236 / 7%);
+      border-color: rgb(42 80 236 / 18%);
+    }
+
+    &.is-admin {
+      color: #b7791f;
+      background: rgb(183 121 31 / 8%);
+      border-color: rgb(183 121 31 / 20%);
+    }
+
+    &.is-economic {
+      color: #12805c;
+      background: rgb(18 128 92 / 8%);
+      border-color: rgb(18 128 92 / 20%);
+    }
+
+    &.is-ip {
+      color: #7c3aed;
+      background: rgb(124 58 237 / 8%);
+      border-color: rgb(124 58 237 / 20%);
+    }
+
+    &.is-labor {
+      color: #0d7f8c;
+      background: rgb(13 127 140 / 8%);
+      border-color: rgb(13 127 140 / 20%);
+    }
+  }
+
+  .case-card__remark {
+    display: -webkit-box;
+    min-height: calc(12.5px * 1.7 * 2);
+    margin: 0;
+    overflow: hidden;
+    color: #5b6577;
+    font-size: 12.5px;
+    line-height: 1.7;
+    word-break: break-word;
+    -webkit-box-orient: vertical;
+    -webkit-line-clamp: 2;
+  }
+
+  .case-card__ops {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    margin-top: auto;
+    padding-top: 10px;
+    border-top: 1px solid #eff2f7;
+  }
+
+  .case-card__open {
+    display: inline-flex;
+    align-items: center;
+    gap: 5px;
+    height: 26px;
+    padding: 0 10px;
+    color: #fff;
+    font-size: 11.5px;
+    font-weight: 500;
+    font-family: inherit;
+    white-space: nowrap;
+    background: #2a50ec;
+    border: 1px solid #2a50ec;
+    border-radius: 6px;
+    cursor: pointer;
+    transition:
+      background-color 0.15s ease,
+      border-color 0.15s ease;
+
+    &:hover {
+      background: #4b6bf0;
+      border-color: #4b6bf0;
+    }
+  }
+
+  .case-card__ops-spacer {
+    flex: 1 1 auto;
+  }
+
+  .case-card__iconbtn {
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    width: 26px;
+    height: 26px;
+    padding: 0;
+    color: #98a2b3;
+    background: #fff;
+    border: 1px solid #e7ebf3;
+    border-radius: 6px;
+    cursor: pointer;
+    transition:
+      color 0.15s ease,
+      background-color 0.15s ease,
+      border-color 0.15s ease;
+
+    &:hover {
+      color: #2a50ec;
+      background: #f8faff;
+      border-color: #bfcdfb;
+    }
+
+    &.is-danger {
+      color: #e5484d;
+      background: #fff7f7;
+      border-color: rgb(229 72 77 / 28%);
+
+      &:hover {
+        color: #c92a2a;
+        background: #fff0f0;
+        border-color: rgb(229 72 77 / 48%);
       }
     }
   }
+
+  .case-card--add {
+    align-items: center;
+    justify-content: center;
+    gap: 6px;
+    min-height: 210px;
+    color: #5b6577;
+    background: #fafbfe;
+    border: 1px dashed #dbe2ee;
+    cursor: pointer;
+
+    &:hover {
+      color: #2a50ec;
+      background: #fff;
+      border-color: #bfcdfb;
+    }
+  }
+
+  .case-card__add-icon {
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    width: 40px;
+    height: 40px;
+    margin-bottom: 2px;
+    color: #2a50ec;
+    font-size: 20px;
+    background: rgb(42 80 236 / 8%);
+    border-radius: 10px;
+  }
+
+  .case-card__add-title {
+    color: inherit;
+    font-size: 13px;
+    font-weight: 600;
+  }
+
+  .case-card__add-desc {
+    color: #98a2b3;
+    font-size: 11.5px;
+    line-height: 1.6;
+  }
+
+  .case-page__footer {
+    flex: 0 0 auto;
+    background: #fff;
+    border-top: 1px solid #e7ebf3;
+  }
+
+  .case-page__footer-inner {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 10px 20px;
+    color: #98a2b3;
+    font-size: 11.5px;
+  }
+
+  .case-page__delete-tip {
+    padding: 8px 0;
+    color: #5b6577;
+    font-size: 13px;
+    line-height: 1.7;
+  }
+
+  @media (max-width: 900px) {
+    .case-page__body {
+      padding: 8px 10px;
+    }
+
+    .case-page__head {
+      flex-direction: column;
+    }
+
+    .case-page__toolbar {
+      flex-wrap: wrap;
+    }
+
+    .case-page__search {
+      width: 100%;
+    }
+
+    .case-page__actions {
+      width: 100%;
+      justify-content: flex-end;
+    }
+
+    .case-page__panel-desc {
+      display: none;
+    }
+
+    .case-page__footer-inner {
+      padding: 10px 12px;
+    }
+  }
+</style>
+
+<style lang="less">
+  /* 案件卡片图标提示:白底黑字,和菜单 tooltip 保持同一套浅色卡片样式 */
+  body .ant-tooltip.case-card-tooltip {
+    --antd-arrow-background-color: #fff;
+
+    .ant-tooltip-inner {
+      min-width: 0;
+      min-height: 0;
+      padding: 5px 10px;
+      color: #111827;
+      font-size: 12px;
+      font-weight: 500;
+      line-height: 18px;
+      background: #fff;
+      border-radius: 8px;
+      box-shadow:
+        0 8px 24px rgb(15 23 42 / 12%),
+        0 0 0 1px #e7ebf3;
+    }
+  }
 </style>