refactor: 统一 Mention 图标渲染逻辑

This commit is contained in:
Wenjie Zhang 2026-06-03 19:50:49 +08:00
parent 3b35493db3
commit 1842c8da85
4 changed files with 70 additions and 52 deletions

View File

@ -15,9 +15,7 @@
- [x] 移除 lightrag
- [x] 新增图谱构建,支持自定义 Schema、支持自定义并发数知识库与向量检索解耦
- [x] 知识图谱抽取要求支持并发处理
## 权限部分
- [ ] UV 的版本
- [x] Agent 的 user_id 使用的有歧义,从数据库表到代码中都修改为统一使用 yuxi_id 来代替
- [ ] 权限新增一个 guest 的预设字段,暂无任何权限
- [ ] rename database table name, such as skills -> agent_skills, subagents -> agent_subagents, mcp, tool_call, 等等

View File

@ -269,11 +269,27 @@
</template>
<script setup>
import { ref, computed, onMounted, nextTick, watch, onBeforeUnmount, useSlots } from 'vue'
import {
ref,
computed,
onMounted,
nextTick,
watch,
onBeforeUnmount,
useSlots,
h,
render
} from 'vue'
import { SendOutlined, ArrowUpOutlined, PauseOutlined, FolderFilled } from '@ant-design/icons-vue'
import { Paperclip } from 'lucide-vue-next'
import { searchMentionFiles } from '@/apis/mention_api'
import { getFileIcon, getFileIconColor } from '@/utils/file_utils'
import {
getMentionIconComponent,
getMentionIconStyle,
MENTION_ICON_SIZE,
MENTION_ICON_STROKE_WIDTH
} from '@/utils/mention_icon_utils'
import {
buildMentionDisplayLabels,
expandMentionDeletionRange,
@ -438,20 +454,10 @@ const serializeEditorNode = (node) => {
const serializeEditorContent = () => serializeEditorNode(inputRef.value)
const getEditorRawValue = () => (inputRef.value ? serializeEditorContent() : inputValue.value)
const getEditorMentionIconSvg = (type) => {
if (type === 'knowledge') {
return '<svg viewBox="0 0 24 24" width="15" height="15" stroke="currentColor" stroke-width="2.2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg>'
}
if (type === 'skill') {
return '<svg viewBox="0 0 24 24" width="15" height="15" stroke="currentColor" stroke-width="2.2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"/></svg>'
}
if (type === 'subagent') {
return '<svg viewBox="0 0 24 24" width="15" height="15" stroke="currentColor" stroke-width="2.2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg>'
}
if (type === 'mcp') {
return '<svg viewBox="0 0 24 24" width="15" height="15" stroke="currentColor" stroke-width="2.2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22v-5"/><path d="M9 8V2"/><path d="M15 8V2"/><path d="M18 8v5a6 6 0 0 1-12 0V8Z"/></svg>'
}
return '<svg viewBox="0 0 24 24" width="15" height="15" stroke="currentColor" stroke-width="2.2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/></svg>'
const unmountEditorMentionIcons = () => {
inputRef.value
?.querySelectorAll('.mention-ref-icon[data-vue-icon]')
.forEach((container) => render(null, container))
}
const createEditorMentionElement = (segment) => {
@ -465,10 +471,15 @@ const createEditorMentionElement = (segment) => {
const icon = document.createElement('span')
icon.className = 'mention-ref-icon'
icon.innerHTML = getEditorMentionIconSvg(segment.type)
if (segment.type === 'file') {
icon.style.color = segment.value.endsWith('/') ? '#ffa940' : getFileIconColor(segment.value)
}
icon.dataset.vueIcon = 'true'
Object.assign(icon.style, getMentionIconStyle(segment.type, segment.value) || {})
render(
h(getMentionIconComponent(segment.type, segment.value), {
size: MENTION_ICON_SIZE,
strokeWidth: MENTION_ICON_STROKE_WIDTH
}),
icon
)
token.appendChild(icon)
const label = document.createElement('span')
@ -487,6 +498,7 @@ const renderEditorContent = (raw = '') => {
const editor = inputRef.value
if (!editor) return
unmountEditorMentionIcons()
editor.replaceChildren()
parseMentionText(raw).forEach((segment) => {
editor.appendChild(
@ -1165,6 +1177,7 @@ onMounted(() => {
//
onBeforeUnmount(() => {
unmountEditorMentionIcons()
if (debounceTimer.value) {
clearTimeout(debounceTimer.value)
}

View File

@ -13,11 +13,11 @@
:title="segment.raw"
>
<component
:is="getTokenIcon(segment)"
:is="getMentionIconComponent(segment.type, segment.value)"
class="mention-ref-icon"
:style="getIconStyle(segment)"
:stroke-width="2.2"
:size="15"
:style="getMentionIconStyle(segment.type, segment.value)"
:stroke-width="MENTION_ICON_STROKE_WIDTH"
:size="MENTION_ICON_SIZE"
/>
<span class="mention-ref-label">{{ getTokenLabel(segment) }}</span>
</span>
@ -27,9 +27,12 @@
<script setup>
import { computed } from 'vue'
import { FolderFilled } from '@ant-design/icons-vue'
import { BookMarked, BookOpen, Bot, Plug } from 'lucide-vue-next'
import { getFileIcon, getFileIconColor } from '@/utils/file_utils'
import {
getMentionIconComponent,
getMentionIconStyle,
MENTION_ICON_SIZE,
MENTION_ICON_STROKE_WIDTH
} from '@/utils/mention_icon_utils'
import { getMentionDisplayLabel, parseMentionText } from '@/utils/mention_utils'
const props = defineProps({
@ -49,29 +52,6 @@ const props = defineProps({
const segments = computed(() => parseMentionText(props.content))
const typeIcons = {
knowledge: BookOpen,
skill: BookMarked,
mcp: Plug,
subagent: Bot
}
const getTokenIcon = (segment) => {
if (segment.type === 'file') {
return segment.value.endsWith('/') ? FolderFilled : getFileIcon(segment.value)
}
return typeIcons[segment.type] || Plug
}
const getIconStyle = (segment) => {
if (segment.type === 'file') {
return {
color: segment.value.endsWith('/') ? '#ffa940' : getFileIconColor(segment.value)
}
}
return null
}
const getTokenLabel = (segment) =>
getMentionDisplayLabel(segment.type, segment.value, props.displayLabels)
</script>

View File

@ -0,0 +1,27 @@
import { FolderFilled } from '@ant-design/icons-vue'
import { BookMarked, BookOpen, Bot, Plug } from 'lucide-vue-next'
import { getFileIcon, getFileIconColor } from '@/utils/file_utils'
export const MENTION_ICON_SIZE = 15
export const MENTION_ICON_STROKE_WIDTH = 2.2
const MENTION_TYPE_ICON_COMPONENTS = {
knowledge: BookOpen,
skill: BookMarked,
mcp: Plug,
subagent: Bot
}
export const getMentionIconComponent = (type, value = '') => {
if (type === 'file') {
return String(value || '').endsWith('/') ? FolderFilled : getFileIcon(value)
}
return MENTION_TYPE_ICON_COMPONENTS[type] || Plug
}
export const getMentionIconStyle = (type, value = '') => {
if (type !== 'file') return null
return {
color: String(value || '').endsWith('/') ? '#ffa940' : getFileIconColor(value)
}
}