cc 2 ngày trước cách đây
mục cha
commit
1fcac406b6
1 tập tin đã thay đổi với 334 bổ sung và 83 xóa
  1. 334 83
      ai-electron/frontend/src/ai/views/aiExpert/index.vue

+ 334 - 83
ai-electron/frontend/src/ai/views/aiExpert/index.vue

@@ -1,63 +1,107 @@
 <template>
   <div class="ai-page ai-expert">
-    <!-- 顶栏:标题 + 摘要 + 操作 -->
-    <div class="ai-expert__bar">
-      <span class="ai-expert__title">专家</span>
-      <span class="ai-expert__meta">{{ summary }}</span>
-      <a-button size="small" :loading="loading" @click="load">
-        <template #icon><Icon icon="mdi:refresh" :size="14" /></template>
-        刷新
-      </a-button>
-      <a-button size="small" type="primary" @click="openCreate">
-        <template #icon><Icon icon="mdi:plus" :size="14" /></template>
-        新建专家
-      </a-button>
-    </div>
-
-    <div class="ai-expert__hint">
-      专家 = 一套「提示词 + 可用工具组 +
-      可用技能」的智能体。点「开始分析」进入新对话草稿,<b>发送第一条消息后才会保存会话</b>(发送时自动绑定该专家,绑定后不可更换)。
-    </div>
-
-    <a-spin :spinning="loading">
-      <!-- 卡片混合展示(内置在前),右上角标签区分内置 / 自定义 -->
-      <div v-if="experts.length" class="ai-expert__grid">
-        <article v-for="e in experts" :key="e.id" class="ai-expert__card">
-          <span class="ai-expert__badge" :class="e.builtin ? 'is-builtin' : 'is-custom'">
-            {{ e.builtin ? '内置' : '自定义' }}
-          </span>
-          <div class="ai-expert__card-head">
-            <span class="ai-expert__name" :title="e.name">{{ e.name }}</span>
-          </div>
-          <p class="ai-expert__desc">{{ e.description || '暂无描述' }}</p>
-          <div class="ai-expert__chips">
-            <span class="ai-expert__chip">{{ groupSummary(e) }}</span>
-            <span class="ai-expert__chip">{{ skillSummary(e) }}</span>
-          </div>
-          <div class="ai-expert__card-ops">
-            <a-button type="primary" size="small" @click="onChat(e)">开始分析</a-button>
-            <span class="ai-expert__card-ops-sp"></span>
-            <!-- 内置只可对话(编辑/删除入口根本不渲染,后端另有双重校验) -->
-            <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>
+    <!-- 数据来源入口(设计稿 hero 区):点击=专家页的「新建对话」逻辑进草稿,仅预填数据范围提示 -->
+    <section class="ai-expert__hero-grid">
+      <article class="ai-expert__hero">
+        <div class="ai-expert__hero-icon"><Icon icon="mdi:upload" :size="22" /></div>
+        <div class="ai-expert__hero-body">
+          <div class="ai-expert__hero-title">基于上传文件智能分析</div>
+          <p class="ai-expert__hero-desc">基于当前案件上传的所有文件,通过系统内置的分析专家进行分析。</p>
+        </div>
+        <button class="ai-expert__hero-start" type="button" @click="emit('start-analysis', 'file')">
+          开始分析
+          <Icon icon="mdi:arrow-right" :size="15" />
+        </button>
+      </article>
+
+      <article class="ai-expert__hero">
+        <div class="ai-expert__hero-icon"><Icon icon="mdi:filter-variant" :size="22" /></div>
+        <div class="ai-expert__hero-body">
+          <div class="ai-expert__hero-title">基于清洗结果智能分析</div>
+          <p class="ai-expert__hero-desc">
+            基于当前案件上传数据经模板库自动清洗治理后的结果,通过系统内置的分析专家进行分析。
+          </p>
+        </div>
+        <button class="ai-expert__hero-start" type="button" @click="emit('start-analysis', 'clean')">
+          开始分析
+          <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>
+        <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>
       </div>
 
-      <div v-if="!loading && !experts.length" class="ai-expert__empty">
-        还没有专家。点击右上角「新建专家」创建第一个。
-      </div>
-    </a-spin>
+      <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>
+              <!-- 内置只可对话(编辑/删除入口根本不渲染,后端另有双重校验) -->
+              <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 && !experts.length" class="ai-expert__empty">
+          还没有专家。点击右上角「新建专家」创建第一个。
+        </div>
+
+        <div v-else-if="!loading" class="ai-expert__empty"> 没有找到与「{{ keyword.trim() }}」匹配的专家。 </div>
+      </a-spin>
+    </section>
 
     <AgentFormModal v-model:open="formOpen" :record="editing" @saved="onSaved" />
   </div>
@@ -71,6 +115,7 @@
    * (后端过滤,内置在前),页面不再分组,只靠卡片角标区分来源。
    *
    * 操作语义:
+   *  - 数据来源入口「开始分析」:向上层 emit(工作台进入新对话草稿并预填数据范围提示,不预绑专家);
    *  - 开始分析:向上层 emit(工作台切到对话视图并预选该专家,下一条消息创建的会话即绑定它);
    *  - 编辑/删除:仅自建卡片出现,内置的按钮根本不渲染(后端另有双重校验)。
    */
@@ -87,7 +132,10 @@
   const ASpin = Spin;
   const ATooltip = Tooltip;
 
-  const emit = defineEmits<{ chat: [expert: AiExpert] }>();
+  const emit = defineEmits<{
+    chat: [expert: AiExpert];
+    'start-analysis': [scope: 'file' | 'clean'];
+  }>();
 
   const { showMessage } = useMessage();
 
@@ -96,6 +144,15 @@
   const formOpen = ref(false);
   const editing = ref<AiExpert | null>(null);
 
+  /** 专家名称搜索(纯前端过滤,不改接口与加载逻辑) */
+  const keyword = ref('');
+
+  const filteredExperts = computed(() => {
+    const k = keyword.value.trim().toLowerCase();
+    if (!k) return experts.value;
+    return experts.value.filter((e) => (e.name || '').toLowerCase().includes(k));
+  });
+
   const builtinExperts = computed(() => experts.value.filter((e) => e.builtin));
   const mineExperts = computed(() => experts.value.filter((e) => !e.builtin));
 
@@ -176,47 +233,191 @@
     display: flex;
     flex-direction: column;
     gap: var(--ai-sp-3);
-    /* 页级内边距与模型/插件页内容区同档(8px),四边不贴边 */
+    /* 页级内边距与现状一致(8px),四边不贴边 —— 不用设计稿的宽页边距 */
     padding: var(--ai-sp-2);
+    /* 设计稿是灰底白卡:工作台中栏直接给灰色画布,卡片才能像设计稿一样立起来 */
+    background: var(--ai-bg-page);
 
-    &__bar {
+    /* ── 数据来源入口(设计稿 hero 区) ── */
+    &__hero-grid {
+      display: grid;
+      grid-template-columns: 1fr 1fr;
+      gap: var(--ai-sp-3);
+      flex: 0 0 auto;
+    }
+
+    &__hero {
+      position: relative;
+      display: flex;
+      align-items: center;
+      gap: 14px;
+      padding: 18px 20px;
+      overflow: hidden;
+      background: var(--ai-bg-card, #fff);
+      border: 1px solid var(--ai-border);
+      border-radius: var(--ai-radius-lg);
+      box-shadow: var(--ai-shadow-sm);
+      transition:
+        box-shadow 0.15s ease,
+        transform 0.15s ease;
+
+      &:hover {
+        box-shadow: var(--ai-shadow);
+        transform: translateY(-1px);
+      }
+
+      /* 角部淡蓝光晕:只做一点品牌氛围,保持克制 */
+      &::after {
+        content: '';
+        position: absolute;
+        top: -60px;
+        right: -40px;
+        width: 220px;
+        height: 160px;
+        background: radial-gradient(closest-side, fade(@primary-color, 7%), transparent);
+        pointer-events: none;
+      }
+    }
+
+    &__hero-icon {
       display: flex;
       align-items: center;
-      gap: var(--ai-sp-2);
+      justify-content: center;
       flex: 0 0 auto;
+      width: 46px;
+      height: 46px;
+      color: var(--ai-primary);
+      background: var(--ai-primary-soft);
+      border-radius: 10px;
     }
 
-    &__title {
+    &__hero-body {
+      flex: 1 1 auto;
+      min-width: 0;
+    }
+
+    &__hero-title {
       font-size: 16px;
       font-weight: 700;
+      letter-spacing: 0.01em;
       color: var(--ai-text);
     }
 
-    &__meta {
-      flex: 1 1 auto;
+    &__hero-desc {
+      margin-top: 5px;
       font-size: 12.5px;
+      line-height: 1.7;
       color: var(--ai-text-2);
     }
 
-    &__hint {
+    &__hero-start {
+      position: relative;
+      z-index: 1;
+      display: inline-flex;
+      align-items: center;
+      gap: 7px;
       flex: 0 0 auto;
-      padding: 8px 12px;
-      font-size: 12px;
-      line-height: 1.7;
+      padding: 0 16px;
+      height: 36px;
+      font-size: 13px;
+      font-weight: 600;
+      font-family: inherit;
+      color: #fff;
+      background: var(--ai-primary);
+      border: none;
+      border-radius: var(--ai-radius);
+      box-shadow: 0 2px 6px fade(@primary-color, 28%);
+      cursor: pointer;
+      transition: background 0.15s ease;
+
+      &:hover {
+        background: darken(@primary-color, 8%);
+      }
+    }
+
+    /* ── 面板(设计稿 panel:白卡承载标题行 + 提示 + 卡片网格) ── */
+    &__panel {
+      display: flex;
+      flex-direction: column;
+      gap: var(--ai-sp-3);
+      flex: 1 1 auto;
+      min-height: 0;
+      padding: 12px;
+      background: var(--ai-bg-card, #fff);
+      border: 1px solid var(--ai-border);
+      border-radius: var(--ai-radius-lg);
+    }
+
+    &__bar {
+      display: flex;
+      align-items: center;
+      gap: var(--ai-sp-3);
+      flex: 0 0 auto;
+    }
+
+    &__title {
+      font-size: 15px;
+      font-weight: 700;
+      letter-spacing: 0.01em;
+      color: var(--ai-text);
+    }
+
+    /* 摘要胶囊(设计稿 panel-head 的 count pill 形态),文案仍是专家页原有 summary */
+    &__count {
+      padding: 2px 10px;
+      font-size: 11.5px;
+      font-weight: 500;
       color: var(--ai-text-2);
       background: var(--ai-bg-subtle);
-      border-radius: 8px;
+      border-radius: 99px;
+    }
 
-      b {
-        color: var(--ai-primary);
+    /* 专家名称搜索(设计稿 panel-head 的 search 形态) */
+    &__search {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      width: 240px;
+      height: 32px;
+      padding: 0 11px;
+      color: var(--ai-text-3);
+      background: var(--ai-bg-card, #fff);
+      border: 1px solid var(--ai-border-strong);
+      border-radius: var(--ai-radius);
+      cursor: text;
+      transition:
+        border-color 0.15s ease,
+        box-shadow 0.15s ease;
+
+      &:focus-within {
+        border-color: var(--ai-primary);
+        box-shadow: 0 0 0 3px var(--ai-primary-soft);
+      }
+
+      input {
+        flex: 1;
+        min-width: 0;
+        font-size: 12.5px;
+        font-family: inherit;
+        color: var(--ai-text);
+        background: transparent;
+        border: none;
+        outline: none;
+
+        &::placeholder {
+          color: var(--ai-text-3);
+        }
       }
     }
 
+    &__bar-sp {
+      flex: 1 1 auto;
+    }
+
     &__grid {
       display: grid;
       grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
-      gap: var(--ai-sp-2);
-      margin-top: var(--ai-sp-2);
+      gap: var(--ai-sp-3);
     }
 
     &__card {
@@ -224,15 +425,17 @@
       display: flex;
       flex-direction: column;
       gap: 8px;
-      padding: 14px;
+      padding: 14px 14px 10px;
       background: var(--ai-bg-card, #fff);
       border: 1px solid var(--ai-border);
-      border-radius: 10px;
+      border-radius: var(--ai-radius);
       transition:
         box-shadow 0.15s ease,
-        transform 0.15s ease;
+        transform 0.15s ease,
+        border-color 0.15s ease;
 
       &:hover {
+        border-color: fade(@primary-color, 35%);
         box-shadow: var(--ai-shadow, 0 4px 16px rgba(15, 23, 42, 0.08));
         transform: translateY(-1px);
       }
@@ -241,23 +444,41 @@
     &__card-head {
       display: flex;
       align-items: center;
-      gap: var(--ai-sp-2);
+      gap: 9px;
+      /* 给右上角「内置 / 自定义」角标让位 */
+      padding-right: 52px;
+    }
+
+    /* 设计稿卡片头的图标位:用专家名称第一个字作图标 */
+    &__card-icon {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      flex: 0 0 auto;
+      width: 30px;
+      height: 30px;
+      font-size: 15px;
+      font-weight: 600;
+      line-height: 1;
+      color: var(--ai-primary);
+      background: var(--ai-primary-soft);
+      border-radius: var(--ai-radius);
     }
 
     &__name {
       overflow: hidden;
-      padding-right: 64px;
-      font-size: 14px;
+      font-size: 13.5px;
       font-weight: 700;
       color: var(--ai-text);
       white-space: nowrap;
       text-overflow: ellipsis;
+      letter-spacing: 0.01em;
     }
 
     /* 右上角来源标签:内置(全局)/ 自定义(我的) */
     &__badge {
       position: absolute;
-      top: 10px;
+      top: 12px;
       right: 12px;
       padding: 0 7px;
       font-size: 11px;
@@ -276,10 +497,10 @@
     }
 
     &__desc {
-      min-height: 36px;
+      min-height: 54px;
       margin: 0;
-      font-size: 12.5px;
-      line-height: 1.6;
+      font-size: 12.25px;
+      line-height: 1.7;
       color: var(--ai-text-2);
       display: -webkit-box;
       -webkit-line-clamp: 2;
@@ -294,10 +515,13 @@
     }
 
     &__chip {
+      display: inline-flex;
+      align-items: center;
+      gap: 5px;
       max-width: 100%;
       padding: 1px 8px;
       overflow: hidden;
-      font-size: 11.5px;
+      font-size: 11px;
       color: var(--ai-text-2);
       white-space: nowrap;
       text-overflow: ellipsis;
@@ -305,6 +529,13 @@
       border-radius: 99px;
     }
 
+    &__chip-dot {
+      flex: 0 0 auto;
+      width: 5px;
+      height: 5px;
+      border-radius: 50%;
+    }
+
     &__card-ops {
       display: flex;
       align-items: center;
@@ -315,6 +546,26 @@
       border-top: 1px solid var(--ai-border);
     }
 
+    /* 「开始分析」=设计稿的主色文字按钮,行为不变(emit chat) */
+    &__run {
+      display: inline-flex;
+      align-items: center;
+      gap: 5px;
+      padding: 4px 8px;
+      font-size: 12.5px;
+      font-weight: 600;
+      font-family: inherit;
+      color: var(--ai-primary);
+      background: none;
+      border: none;
+      border-radius: 6px;
+      cursor: pointer;
+
+      &:hover {
+        background: var(--ai-primary-soft);
+      }
+    }
+
     &__card-ops-sp {
       flex: 1 1 auto;
     }