|
|
@@ -121,16 +121,46 @@
|
|
|
<div class="element-header">
|
|
|
<span class="element-title">
|
|
|
🏷️ 要素管理
|
|
|
- <span class="element-count">({{ entities.length }})</span>
|
|
|
+ <span class="element-count">({{ filteredEntities.length }}/{{ entities.length }})</span>
|
|
|
</span>
|
|
|
<el-button size="small" :icon="Plus" @click="showAddVariableDialog = true">
|
|
|
添加
|
|
|
</el-button>
|
|
|
</div>
|
|
|
+ <!-- 搜索和筛选 -->
|
|
|
+ <div class="element-filter" v-if="entities.length > 0">
|
|
|
+ <el-input
|
|
|
+ v-model="entitySearchKeyword"
|
|
|
+ placeholder="搜索要素..."
|
|
|
+ size="small"
|
|
|
+ :prefix-icon="Search"
|
|
|
+ clearable
|
|
|
+ class="entity-search"
|
|
|
+ />
|
|
|
+ <div class="entity-type-filter">
|
|
|
+ <el-tag
|
|
|
+ v-for="(count, type) in entityTypeCounts"
|
|
|
+ :key="type"
|
|
|
+ :class="['filter-tag', { active: entityTypeFilter === type }]"
|
|
|
+ size="small"
|
|
|
+ @click="toggleEntityTypeFilter(type)"
|
|
|
+ >
|
|
|
+ {{ getEntityTypeIcon(type) }} {{ getEntityTypeName(type) }} ({{ count }})
|
|
|
+ </el-tag>
|
|
|
+ <el-tag
|
|
|
+ v-if="entityTypeFilter"
|
|
|
+ class="filter-tag clear"
|
|
|
+ size="small"
|
|
|
+ @click="entityTypeFilter = ''"
|
|
|
+ >
|
|
|
+ 清除筛选
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
<div class="element-body">
|
|
|
- <div class="element-tags-wrap">
|
|
|
+ <div class="element-tags-wrap" v-if="filteredEntities.length > 0">
|
|
|
<div
|
|
|
- v-for="entity in entities"
|
|
|
+ v-for="entity in filteredEntities"
|
|
|
:key="entity.id"
|
|
|
class="var-tag"
|
|
|
:class="getEntityTypeClass(entity.type)"
|
|
|
@@ -144,6 +174,9 @@
|
|
|
<div class="element-hint" v-if="entities.length === 0">
|
|
|
选中文本后右键标记为变量
|
|
|
</div>
|
|
|
+ <div class="element-hint" v-else-if="filteredEntities.length === 0">
|
|
|
+ 没有匹配的要素
|
|
|
+ </div>
|
|
|
<div class="element-hint" v-else>
|
|
|
点击标签定位到文档位置
|
|
|
</div>
|
|
|
@@ -319,7 +352,7 @@
|
|
|
import { ref, reactive, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
|
|
import { useRouter, useRoute } from 'vue-router'
|
|
|
import {
|
|
|
- ArrowLeft, Clock, Share, Check, Plus, Delete, Connection, Refresh
|
|
|
+ ArrowLeft, Clock, Share, Check, Plus, Delete, Connection, Refresh, Search
|
|
|
} from '@element-plus/icons-vue'
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
import { useTemplateStore } from '@/stores/template'
|
|
|
@@ -365,6 +398,69 @@ const variables = ref([])
|
|
|
// 文档中的实体(从 blocks 的 elements 中提取)
|
|
|
const entities = ref([])
|
|
|
|
|
|
+// 要素搜索和筛选
|
|
|
+const entitySearchKeyword = ref('')
|
|
|
+const entityTypeFilter = ref('')
|
|
|
+
|
|
|
+// 计算属性:按类型统计要素数量
|
|
|
+const entityTypeCounts = computed(() => {
|
|
|
+ const counts = {}
|
|
|
+ entities.value.forEach(entity => {
|
|
|
+ const type = entity.type || 'default'
|
|
|
+ counts[type] = (counts[type] || 0) + 1
|
|
|
+ })
|
|
|
+ return counts
|
|
|
+})
|
|
|
+
|
|
|
+// 计算属性:筛选后的要素列表
|
|
|
+const filteredEntities = computed(() => {
|
|
|
+ let result = entities.value
|
|
|
+
|
|
|
+ // 按类型筛选
|
|
|
+ if (entityTypeFilter.value) {
|
|
|
+ result = result.filter(e => e.type === entityTypeFilter.value)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按关键词搜索
|
|
|
+ if (entitySearchKeyword.value) {
|
|
|
+ const keyword = entitySearchKeyword.value.toLowerCase()
|
|
|
+ result = result.filter(e =>
|
|
|
+ e.text?.toLowerCase().includes(keyword) ||
|
|
|
+ e.type?.toLowerCase().includes(keyword)
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ return result
|
|
|
+})
|
|
|
+
|
|
|
+// 切换类型筛选
|
|
|
+function toggleEntityTypeFilter(type) {
|
|
|
+ if (entityTypeFilter.value === type) {
|
|
|
+ entityTypeFilter.value = ''
|
|
|
+ } else {
|
|
|
+ entityTypeFilter.value = type
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 获取实体类型名称
|
|
|
+function getEntityTypeName(type) {
|
|
|
+ const typeNames = {
|
|
|
+ 'entity': '实体',
|
|
|
+ 'concept': '概念',
|
|
|
+ 'data': '数据',
|
|
|
+ 'location': '地点',
|
|
|
+ 'asset': '资产',
|
|
|
+ 'person': '人物',
|
|
|
+ 'org': '组织',
|
|
|
+ 'date': '日期',
|
|
|
+ 'product': '产品',
|
|
|
+ 'event': '事件',
|
|
|
+ 'law': '法规',
|
|
|
+ 'default': '其他'
|
|
|
+ }
|
|
|
+ return typeNames[type] || type || '其他'
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* 从结构化文档的 blocks 中提取所有实体
|
|
|
*/
|
|
|
@@ -1872,6 +1968,53 @@ onUnmounted(() => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ .element-filter {
|
|
|
+ padding: 0 16px 12px;
|
|
|
+
|
|
|
+ .entity-search {
|
|
|
+ margin-bottom: 10px;
|
|
|
+
|
|
|
+ :deep(.el-input__wrapper) {
|
|
|
+ border-radius: 18px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .entity-type-filter {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 6px;
|
|
|
+
|
|
|
+ .filter-tag {
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s;
|
|
|
+ border-radius: 12px;
|
|
|
+ font-size: 11px;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ border-color: var(--primary);
|
|
|
+ color: var(--primary);
|
|
|
+ }
|
|
|
+
|
|
|
+ &.active {
|
|
|
+ background: var(--primary);
|
|
|
+ color: white;
|
|
|
+ border-color: var(--primary);
|
|
|
+ }
|
|
|
+
|
|
|
+ &.clear {
|
|
|
+ background: transparent;
|
|
|
+ border-style: dashed;
|
|
|
+ color: var(--text-3);
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ border-color: var(--danger);
|
|
|
+ color: var(--danger);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
.element-body {
|
|
|
padding: 0 16px 16px;
|
|
|
}
|
|
|
@@ -1880,7 +2023,7 @@ onUnmounted(() => {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
gap: 8px;
|
|
|
- max-height: 300px;
|
|
|
+ max-height: 280px;
|
|
|
overflow-y: auto;
|
|
|
}
|
|
|
|