소스 검색

feat(frontend): 完善规则管理弹窗 - 搜索/筛选/类型标签/展开详情/来源附件

何文松 14 시간 전
부모
커밋
7b640dcf90
1개의 변경된 파일277개의 추가작업 그리고 26개의 파일을 삭제
  1. 277 26
      frontend/vue-demo/src/views/Editor.vue

+ 277 - 26
frontend/vue-demo/src/views/Editor.vue

@@ -649,12 +649,20 @@
     <el-dialog
       v-model="showRuleDialog"
       title="⚙️ 规则管理"
-      width="640"
+      width="720"
       :close-on-click-modal="true"
-      class="floating-panel-dialog"
+      class="floating-panel-dialog rule-manage-dialog"
       align-center
     >
       <div class="fp-toolbar">
+        <el-input
+          v-model="ruleSearchQuery"
+          size="small"
+          placeholder="搜索规则名称 / 要素标识..."
+          clearable
+          style="width: 220px"
+          :prefix-icon="Search"
+        />
         <el-button size="small" :icon="Plus" @click="showNewRuleDialog = true">添加规则</el-button>
         <el-button
           v-if="rules.length > 0"
@@ -665,27 +673,92 @@
         >
           批量执行
         </el-button>
-        <span class="fp-count">共 {{ rules.length }} 条规则</span>
+        <span class="fp-count">{{ filteredRules.length }} / {{ rules.length }} 条</span>
+      </div>
+      <!-- 类型筛选标签栏 -->
+      <div class="rule-filter-bar">
+        <span
+          class="rule-filter-tab"
+          :class="{ active: ruleFilterType === 'all' }"
+          @click="ruleFilterType = 'all'"
+        >全部 <em>{{ ruleTypeStats.all || 0 }}</em></span>
+        <span
+          class="rule-filter-tab tab-summary"
+          :class="{ active: ruleFilterType === 'summary' }"
+          @click="ruleFilterType = 'summary'"
+        >AI总结 <em>{{ ruleTypeStats.summary || 0 }}</em></span>
+        <span
+          class="rule-filter-tab tab-ai_extract"
+          :class="{ active: ruleFilterType === 'ai_extract' }"
+          @click="ruleFilterType = 'ai_extract'"
+        >AI提取 <em>{{ ruleTypeStats.ai_extract || 0 }}</em></span>
+        <span
+          class="rule-filter-tab tab-table_extract"
+          :class="{ active: ruleFilterType === 'table_extract' }"
+          @click="ruleFilterType = 'table_extract'"
+        >表格提取 <em>{{ ruleTypeStats.table_extract || 0 }}</em></span>
+        <span
+          class="rule-filter-tab tab-quote"
+          :class="{ active: ruleFilterType === 'quote' }"
+          @click="ruleFilterType = 'quote'"
+        >引用 <em>{{ ruleTypeStats.quote || 0 }}</em></span>
+        <span
+          class="rule-filter-tab tab-use_entity_value"
+          :class="{ active: ruleFilterType === 'use_entity_value' }"
+          @click="ruleFilterType = 'use_entity_value'"
+        >人工录入 <em>{{ ruleTypeStats.use_entity_value || 0 }}</em></span>
       </div>
+      <!-- 规则列表 -->
       <div class="fp-list">
         <div
-          v-for="rule in rules"
+          v-for="rule in filteredRules"
           :key="rule.id"
           class="fp-rule-item"
+          :class="{ expanded: expandedRuleId === rule.id }"
+          @click="toggleRuleExpand(rule.id)"
         >
-          <span class="rule-icon">⚙️</span>
-          <div class="rule-info">
-            <div class="rule-name">{{ rule.ruleName }}</div>
-            <div class="rule-meta">
-              <el-tag size="small" :type="rule.lastRunStatus === 'success' ? 'success' : rule.lastRunStatus === 'failed' ? 'danger' : 'info'">
-                {{ rule.ruleType }}
+          <div class="rule-item-main">
+            <span class="rule-action-badge" :class="'action-' + rule.actionType">{{ ruleActionLabel(rule.actionType) }}</span>
+            <div class="rule-info">
+              <div class="rule-name-row">
+                <span class="rule-name">{{ rule.ruleName }}</span>
+                <el-tag size="small" type="info" effect="plain" class="rule-elem-key">{{ rule.elementKey }}</el-tag>
+              </div>
+              <div class="rule-desc" v-if="rule.description">{{ rule.description }}</div>
+            </div>
+            <div class="rule-actions">
+              <el-button size="small" type="primary" text @click.stop="handleExecuteRule(rule)" title="执行" :loading="rule._executing">▶</el-button>
+              <el-button size="small" type="danger" text :icon="Delete" @click.stop="handleDeleteRule(rule)" title="删除" />
+            </div>
+          </div>
+          <!-- 展开详情 -->
+          <div class="rule-detail" v-if="expandedRuleId === rule.id">
+            <div class="rule-detail-row" v-if="rule.dslContent">
+              <span class="rule-detail-label">取值规则</span>
+              <span class="rule-detail-value">{{ rule.dslContent }}</span>
+            </div>
+            <div class="rule-detail-row" v-if="rule.inputs && rule.inputs.length > 0">
+              <span class="rule-detail-label">输入来源</span>
+              <div class="rule-detail-inputs">
+                <span v-for="inp in rule.inputs" :key="inp.inputId" class="rule-input-chip">
+                  📎 {{ inp.inputName || inp.sourceName }}
+                </span>
+              </div>
+            </div>
+            <div class="rule-detail-row" v-if="rule.lastRunStatus">
+              <span class="rule-detail-label">上次执行</span>
+              <el-tag size="small" :type="rule.lastRunStatus === 'success' ? 'success' : 'danger'">
+                {{ rule.lastRunStatus === 'success' ? '成功' : '失败' }}
               </el-tag>
+              <span v-if="rule.lastRunTime" class="rule-detail-time">{{ rule.lastRunTime }}</span>
+            </div>
+            <div class="rule-detail-row" v-if="rule.lastRunError">
+              <span class="rule-detail-label">错误信息</span>
+              <span class="rule-detail-error">{{ rule.lastRunError }}</span>
             </div>
           </div>
-          <el-button size="small" type="primary" text @click.stop="handleExecuteRule(rule)" title="执行">▶</el-button>
-          <el-button size="small" :icon="Delete" circle @click.stop="handleDeleteRule(rule)" />
         </div>
-        <el-empty v-if="rules.length === 0" description="暂无规则,点击添加按钮创建" :image-size="80" />
+        <el-empty v-if="filteredRules.length === 0" :description="ruleSearchQuery || ruleFilterType !== 'all' ? '无匹配规则' : '暂无规则,点击添加按钮创建'" :image-size="80" />
       </div>
     </el-dialog>
 
@@ -878,6 +951,43 @@ const showAddElementDialog = ref(false)
 const showNewRuleDialog = ref(false)
 const showAttachmentDialog = ref(false)
 const showRuleDialog = ref(false)
+const ruleSearchQuery = ref('')
+const ruleFilterType = ref('all')
+const expandedRuleId = ref(null)
+
+const filteredRules = computed(() => {
+  let list = rules.value
+  if (ruleFilterType.value !== 'all') {
+    list = list.filter(r => r.actionType === ruleFilterType.value)
+  }
+  const q = ruleSearchQuery.value.trim().toLowerCase()
+  if (q) {
+    list = list.filter(r =>
+      (r.ruleName || '').toLowerCase().includes(q) ||
+      (r.elementKey || '').toLowerCase().includes(q) ||
+      (r.description || '').toLowerCase().includes(q)
+    )
+  }
+  return list
+})
+
+const ruleTypeStats = computed(() => {
+  const stats = { all: rules.value.length }
+  for (const r of rules.value) {
+    const t = r.actionType || 'unknown'
+    stats[t] = (stats[t] || 0) + 1
+  }
+  return stats
+})
+
+function toggleRuleExpand(ruleId) {
+  expandedRuleId.value = expandedRuleId.value === ruleId ? null : ruleId
+}
+
+function ruleInputSummary(rule) {
+  if (!rule.inputs || rule.inputs.length === 0) return ''
+  return rule.inputs.map(i => i.inputName || i.sourceName || '').filter(Boolean).join('、')
+}
 // 附件解析状态: { [attachmentId]: { status: 'idle'|'uploading'|'parsing'|'completed'|'failed', progress: '', markdown: '' } }
 const parseStates = reactive({})
 const showParseResultDialog = ref(false)
@@ -5926,6 +6036,11 @@ onMounted(async () => {
     padding: 8px 12px;
   }
 
+  &.rule-manage-dialog .fp-list {
+    max-height: 480px;
+    overflow-y: auto;
+  }
+
   // ---- 附件项 ----
   .fp-att-item {
     display: flex;
@@ -6180,36 +6295,172 @@ onMounted(async () => {
     }
   }
 
+  // ---- 规则筛选栏 ----
+  .rule-filter-bar {
+    display: flex;
+    gap: 4px;
+    padding: 0 16px 10px;
+    border-bottom: 1px solid #f0f0f0;
+    flex-wrap: wrap;
+
+    .rule-filter-tab {
+      font-size: 12px;
+      padding: 4px 10px;
+      border-radius: 14px;
+      cursor: pointer;
+      color: #666;
+      background: #f5f7fa;
+      transition: all 0.2s;
+      user-select: none;
+
+      em {
+        font-style: normal;
+        font-size: 11px;
+        opacity: 0.7;
+        margin-left: 2px;
+      }
+
+      &:hover { background: #e8eaed; }
+      &.active { background: #409eff; color: #fff; }
+      &.active em { opacity: 0.9; }
+
+      &.tab-summary.active        { background: #52c41a; }
+      &.tab-ai_extract.active     { background: #13c2c2; }
+      &.tab-table_extract.active  { background: #fa8c16; }
+      &.tab-quote.active          { background: #1677ff; }
+      &.tab-use_entity_value.active { background: #8c8c8c; }
+    }
+  }
+
   // ---- 规则项 ----
   .fp-rule-item {
-    display: flex;
-    align-items: center;
-    gap: 10px;
-    padding: 10px 12px;
     border-radius: 8px;
     transition: background 0.15s;
+    cursor: pointer;
 
-    &:hover {
-      background: #f5f7fa;
+    &:hover { background: #f5f7fa; }
+    &.expanded { background: #fafbfc; }
+
+    .rule-item-main {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      padding: 10px 12px;
     }
 
-    .rule-icon {
-      font-size: 20px;
+    .rule-action-badge {
       flex-shrink: 0;
+      font-size: 11px;
+      font-weight: 600;
+      padding: 3px 8px;
+      border-radius: 12px;
+      line-height: 16px;
+      white-space: nowrap;
+
+      &.action-quote           { background: #e6f4ff; color: #1677ff; }
+      &.action-summary         { background: #f6ffed; color: #52c41a; }
+      &.action-ai_extract      { background: #e6fffb; color: #13c2c2; }
+      &.action-table_extract   { background: #fff7e6; color: #fa8c16; }
+      &.action-use_entity_value { background: #f0f0f0; color: #666; }
     }
 
     .rule-info {
       flex: 1;
       min-width: 0;
 
-      .rule-name {
-        font-size: 13px;
-        font-weight: 500;
-        color: #1f2937;
+      .rule-name-row {
+        display: flex;
+        align-items: center;
+        gap: 6px;
+        flex-wrap: wrap;
+
+        .rule-name {
+          font-size: 13px;
+          font-weight: 500;
+          color: #1f2937;
+        }
+
+        .rule-elem-key {
+          font-size: 10px;
+          max-width: 200px;
+          overflow: hidden;
+          text-overflow: ellipsis;
+        }
       }
 
-      .rule-meta {
+      .rule-desc {
+        font-size: 11px;
+        color: #999;
         margin-top: 2px;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+      }
+    }
+
+    .rule-actions {
+      display: flex;
+      align-items: center;
+      gap: 2px;
+      flex-shrink: 0;
+    }
+
+    // 展开详情区域
+    .rule-detail {
+      padding: 0 12px 10px 12px;
+      border-top: 1px dashed #e8e8e8;
+      margin: 0 12px;
+
+      .rule-detail-row {
+        display: flex;
+        align-items: flex-start;
+        gap: 8px;
+        padding: 6px 0;
+        font-size: 12px;
+
+        &:not(:last-child) {
+          border-bottom: 1px solid #f5f5f5;
+        }
+      }
+
+      .rule-detail-label {
+        flex-shrink: 0;
+        font-weight: 600;
+        color: #666;
+        width: 60px;
+        text-align: right;
+      }
+
+      .rule-detail-value {
+        color: #333;
+        line-height: 1.5;
+        word-break: break-all;
+      }
+
+      .rule-detail-inputs {
+        display: flex;
+        flex-wrap: wrap;
+        gap: 4px;
+      }
+
+      .rule-input-chip {
+        font-size: 11px;
+        padding: 2px 8px;
+        background: #f0f5ff;
+        border-radius: 10px;
+        color: #1677ff;
+        white-space: nowrap;
+      }
+
+      .rule-detail-time {
+        font-size: 11px;
+        color: #999;
+        margin-left: 4px;
+      }
+
+      .rule-detail-error {
+        color: #ff4d4f;
+        line-height: 1.4;
       }
     }
   }