|
@@ -649,12 +649,20 @@
|
|
|
<el-dialog
|
|
<el-dialog
|
|
|
v-model="showRuleDialog"
|
|
v-model="showRuleDialog"
|
|
|
title="⚙️ 规则管理"
|
|
title="⚙️ 规则管理"
|
|
|
- width="640"
|
|
|
|
|
|
|
+ width="720"
|
|
|
:close-on-click-modal="true"
|
|
:close-on-click-modal="true"
|
|
|
- class="floating-panel-dialog"
|
|
|
|
|
|
|
+ class="floating-panel-dialog rule-manage-dialog"
|
|
|
align-center
|
|
align-center
|
|
|
>
|
|
>
|
|
|
<div class="fp-toolbar">
|
|
<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 size="small" :icon="Plus" @click="showNewRuleDialog = true">添加规则</el-button>
|
|
|
<el-button
|
|
<el-button
|
|
|
v-if="rules.length > 0"
|
|
v-if="rules.length > 0"
|
|
@@ -665,27 +673,92 @@
|
|
|
>
|
|
>
|
|
|
批量执行
|
|
批量执行
|
|
|
</el-button>
|
|
</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>
|
|
|
|
|
+ <!-- 规则列表 -->
|
|
|
<div class="fp-list">
|
|
<div class="fp-list">
|
|
|
<div
|
|
<div
|
|
|
- v-for="rule in rules"
|
|
|
|
|
|
|
+ v-for="rule in filteredRules"
|
|
|
:key="rule.id"
|
|
:key="rule.id"
|
|
|
class="fp-rule-item"
|
|
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>
|
|
</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>
|
|
|
</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>
|
|
</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>
|
|
</div>
|
|
|
</el-dialog>
|
|
</el-dialog>
|
|
|
|
|
|
|
@@ -878,6 +951,43 @@ const showAddElementDialog = ref(false)
|
|
|
const showNewRuleDialog = ref(false)
|
|
const showNewRuleDialog = ref(false)
|
|
|
const showAttachmentDialog = ref(false)
|
|
const showAttachmentDialog = ref(false)
|
|
|
const showRuleDialog = 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: '' } }
|
|
// 附件解析状态: { [attachmentId]: { status: 'idle'|'uploading'|'parsing'|'completed'|'failed', progress: '', markdown: '' } }
|
|
|
const parseStates = reactive({})
|
|
const parseStates = reactive({})
|
|
|
const showParseResultDialog = ref(false)
|
|
const showParseResultDialog = ref(false)
|
|
@@ -5926,6 +6036,11 @@ onMounted(async () => {
|
|
|
padding: 8px 12px;
|
|
padding: 8px 12px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ &.rule-manage-dialog .fp-list {
|
|
|
|
|
+ max-height: 480px;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// ---- 附件项 ----
|
|
// ---- 附件项 ----
|
|
|
.fp-att-item {
|
|
.fp-att-item {
|
|
|
display: flex;
|
|
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 {
|
|
.fp-rule-item {
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 10px;
|
|
|
|
|
- padding: 10px 12px;
|
|
|
|
|
border-radius: 8px;
|
|
border-radius: 8px;
|
|
|
transition: background 0.15s;
|
|
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;
|
|
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 {
|
|
.rule-info {
|
|
|
flex: 1;
|
|
flex: 1;
|
|
|
min-width: 0;
|
|
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;
|
|
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;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|