|
@@ -259,19 +259,19 @@
|
|
|
|
|
|
|
|
<!-- 右侧要素面板:仅在选中文档时显示 -->
|
|
<!-- 右侧要素面板:仅在选中文档时显示 -->
|
|
|
<div v-if="hasActiveDocument" class="right-panel" :style="{ width: rightPanelWidth + 'px' }">
|
|
<div v-if="hasActiveDocument" class="right-panel" :style="{ width: rightPanelWidth + 'px' }">
|
|
|
- <!-- 要素管理(展示文档中识别的实体) -->
|
|
|
|
|
|
|
+ <!-- 我的要素(已确认的实体) -->
|
|
|
<div class="element-section">
|
|
<div class="element-section">
|
|
|
<div class="element-header">
|
|
<div class="element-header">
|
|
|
<span class="element-title">
|
|
<span class="element-title">
|
|
|
- 🏷️ 要素管理
|
|
|
|
|
- <span class="element-count">({{ allFilteredEntities?.length || 0 }}/{{ entities?.length || 0 }})</span>
|
|
|
|
|
|
|
+ 🏷️ 我的要素
|
|
|
|
|
+ <span class="element-count">({{ myEntities?.length || 0 }})</span>
|
|
|
</span>
|
|
</span>
|
|
|
<el-button size="small" :icon="Plus" @click="showAddVariableDialog = true">
|
|
<el-button size="small" :icon="Plus" @click="showAddVariableDialog = true">
|
|
|
添加
|
|
添加
|
|
|
</el-button>
|
|
</el-button>
|
|
|
</div>
|
|
</div>
|
|
|
<!-- 搜索和筛选 -->
|
|
<!-- 搜索和筛选 -->
|
|
|
- <div class="element-filter" v-if="entities && entities.length > 0">
|
|
|
|
|
|
|
+ <div class="element-filter" v-if="myEntities && myEntities.length > 0">
|
|
|
<el-input
|
|
<el-input
|
|
|
v-model="entitySearchKeyword"
|
|
v-model="entitySearchKeyword"
|
|
|
placeholder="搜索要素..."
|
|
placeholder="搜索要素..."
|
|
@@ -282,7 +282,7 @@
|
|
|
/>
|
|
/>
|
|
|
<div class="entity-type-filter">
|
|
<div class="entity-type-filter">
|
|
|
<el-tag
|
|
<el-tag
|
|
|
- v-for="(count, type) in entityTypeCounts"
|
|
|
|
|
|
|
+ v-for="(count, type) in myEntityTypeCounts"
|
|
|
:key="type"
|
|
:key="type"
|
|
|
:class="['filter-tag', { active: entityTypeFilter === type }]"
|
|
:class="['filter-tag', { active: entityTypeFilter === type }]"
|
|
|
size="small"
|
|
size="small"
|
|
@@ -301,61 +301,53 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="element-body">
|
|
<div class="element-body">
|
|
|
- <!-- 我的要素(已确认) -->
|
|
|
|
|
- <div class="entity-group my-entities" v-if="myEntities.length > 0">
|
|
|
|
|
- <div class="entity-group-header">
|
|
|
|
|
- <span class="group-title">✓ 我的要素</span>
|
|
|
|
|
- <span class="group-count">{{ myEntities.length }}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="element-tags-wrap">
|
|
|
|
|
- <div
|
|
|
|
|
- v-for="entity in myEntities"
|
|
|
|
|
- :key="entity.id"
|
|
|
|
|
- class="var-tag confirmed"
|
|
|
|
|
- :class="[getEntityTypeClass(entity.type)]"
|
|
|
|
|
- :title="`${getEntityTypeName(entity.type)}: ${entity.text}`"
|
|
|
|
|
- @click="scrollToEntity(entity.id)"
|
|
|
|
|
- @dblclick="openEntityEditModal(entity)"
|
|
|
|
|
- >
|
|
|
|
|
- <span class="tag-icon">{{ getEntityTypeIcon(entity.type) }}</span>
|
|
|
|
|
- <span class="tag-name">{{ entity.text }}</span>
|
|
|
|
|
- <span class="tag-status">✓</span>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <div class="element-tags-wrap" v-if="myEntities.length > 0">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="entity in myEntities"
|
|
|
|
|
+ :key="entity.id"
|
|
|
|
|
+ class="var-tag confirmed"
|
|
|
|
|
+ :class="[getEntityTypeClass(entity.type)]"
|
|
|
|
|
+ :title="`${getEntityTypeName(entity.type)}: ${entity.text}`"
|
|
|
|
|
+ @click="scrollToEntity(entity.id)"
|
|
|
|
|
+ @dblclick="openEntityEditModal(entity)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span class="tag-icon">{{ getEntityTypeIcon(entity.type) }}</span>
|
|
|
|
|
+ <span class="tag-name">{{ entity.text }}</span>
|
|
|
|
|
+ <span class="tag-status">✓</span>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
-
|
|
|
|
|
- <!-- AI 识别(待采纳) -->
|
|
|
|
|
- <div class="entity-group ai-suggestions" v-if="aiSuggestedEntities.length > 0">
|
|
|
|
|
- <div class="entity-group-header">
|
|
|
|
|
- <span class="group-title">💡 AI 识别</span>
|
|
|
|
|
- <span class="group-count">{{ aiSuggestedEntities.length }}</span>
|
|
|
|
|
- <div class="group-actions">
|
|
|
|
|
- <el-button size="small" text type="primary" @click="adoptAllAiSuggestions">全部采纳</el-button>
|
|
|
|
|
- <el-button size="small" text @click="ignoreAllAiSuggestions">全部忽略</el-button>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="element-tags-wrap ai-tags">
|
|
|
|
|
- <div
|
|
|
|
|
- v-for="entity in aiSuggestedEntities"
|
|
|
|
|
- :key="entity.id"
|
|
|
|
|
- class="var-tag ai-suggestion"
|
|
|
|
|
- :class="[getEntityTypeClass(entity.type)]"
|
|
|
|
|
- :title="`${getEntityTypeName(entity.type)}: ${entity.text} - 点击采纳`"
|
|
|
|
|
- @click.stop="adoptEntity(entity)"
|
|
|
|
|
- >
|
|
|
|
|
- <span class="tag-icon">{{ getEntityTypeIcon(entity.type) }}</span>
|
|
|
|
|
- <span class="tag-name">{{ entity.text }}</span>
|
|
|
|
|
- <span class="tag-action">+</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <div class="element-hint" v-else>
|
|
|
|
|
+ 选中文本后右键标记为实体,或从 AI 建议中采纳
|
|
|
</div>
|
|
</div>
|
|
|
-
|
|
|
|
|
- <!-- 空状态提示 -->
|
|
|
|
|
- <div class="element-hint" v-if="!entities || entities.length === 0">
|
|
|
|
|
- 选中文本后右键标记为实体
|
|
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- AI 建议(待采纳的实体) -->
|
|
|
|
|
+ <div class="element-section ai-section" v-if="aiSuggestedEntities.length > 0">
|
|
|
|
|
+ <div class="element-header">
|
|
|
|
|
+ <span class="element-title">
|
|
|
|
|
+ 💡 AI 建议
|
|
|
|
|
+ <span class="element-count">({{ aiSuggestedEntities?.length || 0 }})</span>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <div class="header-actions">
|
|
|
|
|
+ <el-button size="small" text type="primary" @click="adoptAllAiSuggestions">全部采纳</el-button>
|
|
|
|
|
+ <el-button size="small" text @click="ignoreAllAiSuggestions">忽略全部</el-button>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="element-hint" v-else-if="!filteredEntities || filteredEntities.length === 0">
|
|
|
|
|
- 没有匹配的要素
|
|
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="element-body">
|
|
|
|
|
+ <div class="element-tags-wrap ai-tags">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="entity in aiSuggestedEntities"
|
|
|
|
|
+ :key="entity.id"
|
|
|
|
|
+ class="var-tag ai-suggestion"
|
|
|
|
|
+ :class="[getEntityTypeClass(entity.type)]"
|
|
|
|
|
+ :title="`${getEntityTypeName(entity.type)}: ${entity.text} - 点击采纳`"
|
|
|
|
|
+ @click.stop="adoptEntity(entity)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span class="tag-icon">{{ getEntityTypeIcon(entity.type) }}</span>
|
|
|
|
|
+ <span class="tag-name">{{ entity.text }}</span>
|
|
|
|
|
+ <span class="tag-action">+</span>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -1151,7 +1143,7 @@ const entities = ref([])
|
|
|
const entitySearchKeyword = ref('')
|
|
const entitySearchKeyword = ref('')
|
|
|
const entityTypeFilter = ref('')
|
|
const entityTypeFilter = ref('')
|
|
|
|
|
|
|
|
-// 计算属性:按类型统计要素数量
|
|
|
|
|
|
|
+// 计算属性:按类型统计要素数量(全部)
|
|
|
const entityTypeCounts = computed(() => {
|
|
const entityTypeCounts = computed(() => {
|
|
|
const counts = {}
|
|
const counts = {}
|
|
|
if (!entities.value || !Array.isArray(entities.value)) return counts
|
|
if (!entities.value || !Array.isArray(entities.value)) return counts
|
|
@@ -1163,6 +1155,17 @@ const entityTypeCounts = computed(() => {
|
|
|
return counts
|
|
return counts
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+// 计算属性:按类型统计已确认的要素数量
|
|
|
|
|
+const myEntityTypeCounts = computed(() => {
|
|
|
|
|
+ const counts = {}
|
|
|
|
|
+ if (!entities.value || !Array.isArray(entities.value)) return counts
|
|
|
|
|
+ entities.value.filter(e => e && e.confirmed).forEach(entity => {
|
|
|
|
|
+ const type = entity.type || 'default'
|
|
|
|
|
+ counts[type] = (counts[type] || 0) + 1
|
|
|
|
|
+ })
|
|
|
|
|
+ return counts
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
// 实体显示数量限制(性能优化)
|
|
// 实体显示数量限制(性能优化)
|
|
|
|
|
|
|
|
// 计算属性:筛选后的要素列表(全部)
|
|
// 计算属性:筛选后的要素列表(全部)
|
|
@@ -3932,6 +3935,33 @@ onUnmounted(() => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ .header-actions {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 4px;
|
|
|
|
|
+
|
|
|
|
|
+ .el-button {
|
|
|
|
|
+ padding: 4px 8px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // AI 建议区块特殊样式
|
|
|
|
|
+ &.ai-section {
|
|
|
|
|
+ background: var(--bg);
|
|
|
|
|
+ border-bottom: none;
|
|
|
|
|
+
|
|
|
|
|
+ .element-header {
|
|
|
|
|
+ .element-title {
|
|
|
|
|
+ color: var(--text-2);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .element-tags-wrap {
|
|
|
|
|
+ max-height: 300px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 要素 Tab 切换 - V2 风格
|
|
// 要素 Tab 切换 - V2 风格
|
|
|
.element-tabs {
|
|
.element-tabs {
|
|
|
display: flex;
|
|
display: flex;
|
|
@@ -4023,53 +4053,6 @@ onUnmounted(() => {
|
|
|
gap: 16px;
|
|
gap: 16px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 实体分组
|
|
|
|
|
- .entity-group {
|
|
|
|
|
- .entity-group-header {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 8px;
|
|
|
|
|
- margin-bottom: 10px;
|
|
|
|
|
- padding-bottom: 8px;
|
|
|
|
|
- border-bottom: 1px solid var(--border);
|
|
|
|
|
-
|
|
|
|
|
- .group-title {
|
|
|
|
|
- font-size: 13px;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- color: var(--text-1);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .group-count {
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- color: var(--text-3);
|
|
|
|
|
- background: var(--bg);
|
|
|
|
|
- padding: 2px 8px;
|
|
|
|
|
- border-radius: 10px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .group-actions {
|
|
|
|
|
- margin-left: auto;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- gap: 4px;
|
|
|
|
|
-
|
|
|
|
|
- .el-button {
|
|
|
|
|
- padding: 4px 8px;
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &.ai-suggestions {
|
|
|
|
|
- .entity-group-header {
|
|
|
|
|
- border-bottom-style: dashed;
|
|
|
|
|
-
|
|
|
|
|
- .group-title {
|
|
|
|
|
- color: var(--text-2);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
// 要素标签容器 - V2 风格
|
|
// 要素标签容器 - V2 风格
|
|
|
.element-tags-wrap {
|
|
.element-tags-wrap {
|
|
|
display: flex;
|
|
display: flex;
|