Sfoglia il codice sorgente

feat(frontend): 智能工作台研判方案页卡片化改版

- 入口卡、单一内容卡与方案卡片统一持续联系页视觉规范
- 工具栏合并进内容卡卡头,搜索/操作/数量同排,空状态改用 EmptyState
cc 3 giorni fa
parent
commit
df70ee2969
1 ha cambiato i file con 600 aggiunte e 78 eliminazioni
  1. 600 78
      ai-electron/frontend/src/ai/views/aiExpert/index.vue

+ 600 - 78
ai-electron/frontend/src/ai/views/aiExpert/index.vue

@@ -24,101 +24,115 @@
             >上传案件文件后自动执行清洗与治理,形成规范、可分析的数据,供后续智能研判使用。</p
           >
         </div>
-        <button class="ai-expert__hero-start" type="button" @click="onUploadFile">
+        <button class="ai-expert__hero-start is-secondary" type="button" @click="onUploadFile">
           上传文件
           <Icon icon="mdi:arrow-right" :size="15" />
         </button>
       </article>
     </section>
 
-    <!-- 面板:顶栏(标题 + 摘要胶囊 + 搜索 + 操作)+ 提示 + 研判方案卡片网格 -->
+    <!-- 内容卡:标题 + 摘要 + 搜索 + 操作 + 方案卡片网格(工具栏合并进卡头) -->
     <section class="ai-expert__panel">
-      <div class="ai-expert__bar">
-        <span class="ai-expert__title">研判方案</span>
-        <span class="ai-expert__count">{{ summary }}</span>
+      <div class="ai-expert__panel-head">
+        <span class="ai-expert__card-title">研判方案</span>
+        <span class="ai-expert__summary">{{ summary }}</span>
         <label class="ai-expert__search">
           <Icon icon="mdi:magnify" :size="14" />
           <input v-model="keyword" type="text" placeholder="输入方案名称搜索" />
         </label>
-        <span class="ai-expert__bar-sp"></span>
-        <a-button :loading="loading" @click="load">
-          <template #icon><Icon icon="mdi:refresh" :size="14" /></template>
-          刷新
-        </a-button>
-        <a-button type="primary" @click="openCreate">
-          <template #icon><Icon icon="mdi:plus" :size="14" /></template>
-          新建研判方案
-        </a-button>
+        <span class="ai-expert__spacer"></span>
+        <span class="ai-expert__panel-count">共 {{ filteredExperts.length }} 个</span>
+        <div class="ai-expert__actions">
+          <a-button :loading="loading" @click="load">
+            <template #icon><Icon icon="mdi:refresh" :size="14" /></template>
+            刷新
+          </a-button>
+          <a-button type="primary" @click="openCreate">
+            <template #icon><Icon icon="mdi:plus" :size="14" /></template>
+            新建研判方案
+          </a-button>
+        </div>
       </div>
 
-      <a-spin :spinning="loading">
-        <!-- 卡片混合展示(内置在前),右上角标签区分内置 / 自定义 -->
-        <div v-if="filteredExperts.length" class="ai-expert__grid">
-          <article v-for="e in filteredExperts" :key="e.id" class="ai-expert__card">
-            <div class="ai-expert__card-head">
-              <span class="ai-expert__card-icon">{{ (e.name || '?').charAt(0) }}</span>
-              <span class="ai-expert__name" :title="e.name">{{ e.name }}</span>
-            </div>
-            <span class="ai-expert__badge" :class="e.builtin ? 'is-builtin' : 'is-custom'">
-              {{ e.builtin ? '内置' : '自定义' }}
-            </span>
-            <p class="ai-expert__desc">{{ e.description || '暂无描述' }}</p>
-            <div class="ai-expert__chips">
-              <span class="ai-expert__chip">
-                <i class="ai-expert__chip-dot" style="background: #0969da"></i>
-                {{ groupSummary(e) }}
-              </span>
-              <span class="ai-expert__chip">
-                <i class="ai-expert__chip-dot" style="background: #8250df"></i>
-                {{ skillSummary(e) }}
-              </span>
-            </div>
-            <div class="ai-expert__card-ops">
-              <!-- 「开始分析」视觉取设计稿的主色文字按钮,点击行为不变(emit chat 进入新对话草稿) -->
-              <button class="ai-expert__run" type="button" @click="onChat(e)">
-                开始分析
-                <Icon icon="mdi:arrow-right" :size="13" />
-              </button>
-              <span class="ai-expert__card-ops-sp"></span>
-              <!-- 查看详情:内置/自定义都可看(只读弹框),放最左与编辑/删除分隔开 -->
-              <a-tooltip title="查看详情">
-                <button
-                  class="ai-expert__iconbtn"
-                  type="button"
-                  aria-label="查看方案详情"
-                  @click="openDetail(e)"
-                >
-                  <Icon icon="mdi:eye-outline" :size="15" />
+      <div class="ai-expert__panel-body">
+        <a-spin :spinning="loading">
+          <!-- 卡片混合展示(内置在前),卡片头标签区分内置 / 自定义 -->
+          <div v-if="filteredExperts.length" class="ai-expert__grid">
+            <article v-for="e in filteredExperts" :key="e.id" class="ai-expert__card">
+              <div class="ai-expert__card-head">
+                <span class="ai-expert__card-icon">{{ (e.name || '?').charAt(0) }}</span>
+                <div class="ai-expert__card-heading">
+                  <div class="ai-expert__name-row">
+                    <span class="ai-expert__name" :title="e.name">{{ e.name }}</span>
+                    <span class="ai-expert__chip" :class="e.builtin ? '' : 'is-primary'">
+                      {{ e.builtin ? '内置' : '自定义' }}
+                    </span>
+                  </div>
+                  <span class="ai-expert__card-sub">{{ e.builtin ? '系统内置研判方案' : '我的研判方案' }}</span>
+                </div>
+              </div>
+
+              <p class="ai-expert__desc">{{ e.description || '暂无描述' }}</p>
+
+              <div class="ai-expert__chips">
+                <span class="ai-expert__chip">
+                  <i class="ai-expert__chip-dot" style="background: #0969da"></i>
+                  {{ groupSummary(e) }}
+                </span>
+                <span class="ai-expert__chip">
+                  <i class="ai-expert__chip-dot" style="background: #8250df"></i>
+                  {{ skillSummary(e) }}
+                </span>
+              </div>
+
+              <div class="ai-expert__card-ops">
+                <!-- 「开始分析」= 主操作,点击行为不变(emit chat 进入新对话草稿) -->
+                <button class="ai-expert__run" type="button" @click="onChat(e)">
+                  开始分析
+                  <Icon icon="mdi:arrow-right" :size="13" />
                 </button>
-              </a-tooltip>
-              <!-- 内置只可对话(编辑/删除入口根本不渲染,后端另有双重校验) -->
-              <template v-if="!e.builtin">
-                <a-tooltip title="编辑研判方案">
-                  <button class="ai-expert__iconbtn" type="button" aria-label="编辑研判方案" @click="openEdit(e)">
-                    <Icon icon="mdi:pencil-outline" :size="15" />
-                  </button>
-                </a-tooltip>
-                <a-tooltip title="删除研判方案">
-                  <button
-                    class="ai-expert__iconbtn is-danger"
-                    type="button"
-                    aria-label="删除研判方案"
-                    @click="onDelete(e)"
-                  >
-                    <Icon icon="mdi:trash-can-outline" :size="15" />
+                <span class="ai-expert__card-ops-sp"></span>
+                <!-- 查看详情:内置/自定义都可看(只读弹框) -->
+                <a-tooltip title="查看详情">
+                  <button class="ai-expert__iconbtn" type="button" aria-label="查看方案详情" @click="openDetail(e)">
+                    <Icon icon="mdi:eye-outline" :size="15" />
                   </button>
                 </a-tooltip>
-              </template>
-            </div>
-          </article>
-        </div>
-
-        <div v-else-if="!loading && !experts.length" class="ai-expert__empty">
-          还没有研判方案。点击右上角「新建研判方案」创建第一个。
-        </div>
+                <!-- 内置只可对话(编辑/删除入口根本不渲染,后端另有双重校验) -->
+                <template v-if="!e.builtin">
+                  <a-tooltip title="编辑研判方案">
+                    <button class="ai-expert__iconbtn" type="button" aria-label="编辑研判方案" @click="openEdit(e)">
+                      <Icon icon="mdi:pencil-outline" :size="15" />
+                    </button>
+                  </a-tooltip>
+                  <a-tooltip title="删除研判方案">
+                    <button
+                      class="ai-expert__iconbtn is-danger"
+                      type="button"
+                      aria-label="删除研判方案"
+                      @click="onDelete(e)"
+                    >
+                      <Icon icon="mdi:trash-can-outline" :size="15" />
+                    </button>
+                  </a-tooltip>
+                </template>
+              </div>
+            </article>
+          </div>
 
-        <div v-else-if="!loading" class="ai-expert__empty"> 没有找到与「{{ keyword.trim() }}」匹配的研判方案。 </div>
-      </a-spin>
+          <EmptyState
+            v-else-if="!loading"
+            :title="experts.length ? '没有匹配的研判方案' : '还没有研判方案'"
+            :description="
+              experts.length
+                ? '换个方案名称试试,或清空搜索条件查看全部方案。'
+                : '点击右上角「新建研判方案」创建第一个。'
+            "
+            :min-height="300"
+            description-width="min(100%, 520px)"
+          />
+        </a-spin>
+      </div>
     </section>
 
     <!-- 新建/编辑:默认窗口弹框(可右上角全屏),内容为新建方案页设计版式 -->
@@ -187,6 +201,7 @@
   import Icon from '@/core/components/Icon/src/Icon.vue';
   import { useMessage } from '@/core/hooks/web/useMessage';
   import { BasicModal } from '@/core/components/Modal';
+  import { EmptyState } from '@/core/components/EmptyState';
   import * as agentApi from '../../api/agentApi';
   import * as toolPackApi from '../../api/toolPackApi';
   import type { AiExpert, ToolPack } from '../../api/types';
@@ -825,4 +840,511 @@
       color: var(--ai-text-2);
     }
   }
+
+  /* ======================================================================
+   * 卡片化改版:对齐自主模式「持续联系」页与插件 / 模型页的视觉规范
+   * 页面底 #f4f6fa · 白卡圆角 14 · 描边 #e7ebf3 · 标题蓝条 · 主按钮 #2a50ec
+   * ==================================================================== */
+  .ai-page.ai-expert {
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+    width: 100%;
+    height: 100%;
+    min-height: 0;
+    margin: 0;
+    padding: 8px 10px;
+    background: #f4f6fa;
+    border: 0;
+    border-radius: 0;
+    box-sizing: border-box;
+    overflow: hidden;
+  }
+
+  .ai-expert {
+    &__hero-grid {
+      display: grid;
+      grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
+      gap: 8px;
+    }
+
+    &__hero {
+      position: relative;
+      display: flex;
+      align-items: center;
+      gap: 14px;
+      padding: 16px 18px;
+      overflow: hidden;
+      background: #fff;
+      border: 1px solid #e7ebf3;
+      border-radius: 14px;
+      box-shadow: 0 1px 2px rgb(16 24 40 / 4%);
+      transition:
+        border-color 0.15s ease,
+        box-shadow 0.15s ease;
+
+      &::after {
+        display: none;
+      }
+
+      &:hover {
+        border-color: #bfcdfb;
+        box-shadow: 0 4px 14px rgb(16 24 40 / 8%);
+        transform: none;
+      }
+    }
+
+    &__hero-icon {
+      display: flex;
+      flex: 0 0 auto;
+      align-items: center;
+      justify-content: center;
+      width: 40px;
+      height: 40px;
+      color: #2a50ec;
+      background: rgb(42 80 236 / 8%);
+      border-radius: 10px;
+    }
+
+    /* 第二个入口是"上传/清洗",用紫色与"开始分析"区分 */
+    &__hero:nth-child(2) &__hero-icon {
+      color: #7c3aed;
+      background: rgb(124 58 237 / 8%);
+    }
+
+    &__hero-body {
+      flex: 1 1 auto;
+      min-width: 0;
+    }
+
+    &__hero-title {
+      color: #111827;
+      font-size: 14px;
+      font-weight: 600;
+      line-height: 1.5;
+      letter-spacing: 0;
+    }
+
+    &__hero-desc {
+      margin-top: 4px;
+      color: #5b6577;
+      font-size: 12px;
+      line-height: 1.7;
+    }
+
+    &__hero-start {
+      position: relative;
+      z-index: 1;
+      display: inline-flex;
+      flex: 0 0 auto;
+      align-items: center;
+      gap: 6px;
+      height: 30px;
+      padding: 0 14px;
+      color: #fff;
+      font-size: 12px;
+      font-weight: 500;
+      font-family: inherit;
+      background: #2a50ec;
+      border: 1px solid #2a50ec;
+      border-radius: 8px;
+      box-shadow: none;
+      cursor: pointer;
+      transition:
+        color 0.15s ease,
+        background-color 0.15s ease,
+        border-color 0.15s ease;
+
+      &:hover {
+        background: #4b6bf0;
+        border-color: #4b6bf0;
+      }
+
+      &.is-secondary {
+        color: #5b6577;
+        background: #fff;
+        border-color: #e7ebf3;
+      }
+
+      &.is-secondary:hover {
+        color: #2a50ec;
+        background: #f8faff;
+        border-color: #bfcdfb;
+      }
+    }
+
+    &__card-title {
+      position: relative;
+      flex: 0 0 auto;
+      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;
+      }
+    }
+
+    &__summary {
+      max-width: 200px;
+      overflow: hidden;
+      color: #98a2b3;
+      font-size: 11.5px;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+    }
+
+    &__search {
+      display: flex;
+      flex: 0 1 auto;
+      align-items: center;
+      gap: 8px;
+      width: 240px;
+      min-width: 160px;
+      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;
+        }
+      }
+    }
+
+    &__spacer {
+      flex: 1 1 auto;
+    }
+
+    &__actions {
+      display: flex;
+      flex: 0 0 auto;
+      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;
+
+        &:hover,
+        &:focus {
+          color: #fff;
+          background: #4b6bf0;
+          border-color: #4b6bf0;
+        }
+
+        &:active {
+          background: #1e3ed0;
+          border-color: #1e3ed0;
+        }
+      }
+
+      :deep(.ant-btn[disabled]) {
+        color: rgb(0 0 0 / 25%);
+        background: #f5f5f5;
+        border-color: #e7ebf3;
+      }
+    }
+
+    &__panel {
+      display: flex;
+      flex: 1 1 auto;
+      flex-direction: column;
+      gap: 0;
+      min-height: 0;
+      padding: 0;
+      background: #fff;
+      border: 1px solid #e7ebf3;
+      border-radius: 14px;
+      box-shadow: 0 1px 2px rgb(16 24 40 / 4%);
+      overflow: hidden;
+    }
+
+    &__panel-head {
+      display: flex;
+      flex: 0 0 auto;
+      align-items: center;
+      gap: 10px;
+      min-height: 52px;
+      padding: 10px 14px;
+      border-bottom: 1px solid #eff2f7;
+      box-sizing: border-box;
+    }
+
+    &__panel-count {
+      color: #98a2b3;
+      font-size: 11.5px;
+      white-space: nowrap;
+      font-variant-numeric: tabular-nums;
+    }
+
+    &__panel-body {
+      flex: 1 1 auto;
+      min-height: 0;
+      padding: 14px;
+      overflow-y: auto;
+      overscroll-behavior: contain;
+      box-sizing: border-box;
+    }
+
+    &__grid {
+      display: grid;
+      grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
+      gap: 12px;
+    }
+
+    &__card {
+      position: relative;
+      display: flex;
+      flex-direction: column;
+      gap: 10px;
+      padding: 14px;
+      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%);
+        transform: none;
+      }
+    }
+
+    &__card-head {
+      align-items: flex-start;
+      gap: 10px;
+      padding-right: 0;
+    }
+
+    &__card-icon {
+      display: flex;
+      flex: 0 0 auto;
+      align-items: center;
+      justify-content: center;
+      width: 34px;
+      height: 34px;
+      color: #2a50ec;
+      font-size: 15px;
+      font-weight: 600;
+      line-height: 1;
+      background: rgb(42 80 236 / 8%);
+      border-radius: 9px;
+    }
+
+    &__card-heading {
+      display: flex;
+      flex: 1 1 auto;
+      flex-direction: column;
+      gap: 4px;
+      min-width: 0;
+    }
+
+    &__name-row {
+      display: flex;
+      align-items: center;
+      gap: 6px;
+      min-width: 0;
+    }
+
+    &__name {
+      flex: 1 1 auto;
+      min-width: 0;
+      overflow: hidden;
+      color: #111827;
+      font-size: 14px;
+      font-weight: 600;
+      line-height: 1.5;
+      letter-spacing: 0;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+    }
+
+    &__card-sub {
+      overflow: hidden;
+      color: #98a2b3;
+      font-size: 11.5px;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+    }
+
+    &__chip {
+      display: inline-flex;
+      flex: 0 0 auto;
+      align-items: center;
+      gap: 5px;
+      max-width: 100%;
+      height: 20px;
+      padding: 0 7px;
+      overflow: hidden;
+      color: #5b6577;
+      font-size: 11px;
+      font-weight: 500;
+      line-height: 18px;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+      background: #f4f6fa;
+      border: 1px solid #e7ebf3;
+      border-radius: 6px;
+
+      &.is-primary {
+        color: #2a50ec;
+        background: rgb(42 80 236 / 7%);
+        border-color: rgb(42 80 236 / 18%);
+      }
+    }
+
+    &__chip-dot {
+      flex: 0 0 auto;
+      width: 5px;
+      height: 5px;
+      border-radius: 50%;
+    }
+
+    &__desc {
+      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;
+      -webkit-box-orient: vertical;
+      -webkit-line-clamp: 3;
+    }
+
+    &__chips {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 6px;
+    }
+
+    &__card-ops {
+      display: flex;
+      align-items: center;
+      gap: 6px;
+      margin-top: auto;
+      padding-top: 10px;
+      border-top: 1px solid #eff2f7;
+    }
+
+    &__card-ops-sp {
+      flex: 1 1 auto;
+    }
+
+    &__run {
+      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;
+      }
+    }
+
+    &__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:hover {
+        color: #e5484d;
+        background: #fff5f5;
+        border-color: rgb(229 72 77 / 35%);
+      }
+    }
+  }
+
+  @media (max-width: 1180px) {
+    .ai-expert__summary {
+      display: none;
+    }
+  }
 </style>