- useStreamRenderer.ts: 流式Markdown块级渲染提取为composable(220行) - EmptyState.vue: 空状态独立组件(无provider引导+示例问题) - MentionPopover.vue: @实体联想浮层独立组件 - EnrichmentPanel.vue: @项目enrichment预览面板独立组件 - MessageList.vue: 1386→1148行, ChatInput.vue: 1155→1007行 - AiChat进度条: completedTools计数器(AiToolCallCompleted+1,AiAgentRound重置) - i18n: 进度条文案加已完成工具数(中英文)
133 lines
4.5 KiB
Vue
133 lines
4.5 KiB
Vue
<script setup lang="ts">
|
|
//! @ 实体引用浮层 — 抽自 ChatInput.vue(零行为变更)。
|
|
//!
|
|
//! 职责:@ 触发的实体引用 popover(项目/任务/灵感联想候选列表)。
|
|
//! 架构复用技能联想 popover 的 .ai-skill-popover 容器样式,仅新增组标题/类型标签。
|
|
//!
|
|
//! 父组件注入:
|
|
//! - mentionItems:联想候选(项目 + 任务 + 灵感,已按查询过滤)
|
|
//! - mentionGroups:按类型分组的候选(模板按组渲染组标题)
|
|
//! - mentionTotal:未过滤候选总数(区分"无匹配" vs "空")
|
|
//! - mentionIndex:当前高亮项在扁平列表中的下标
|
|
//! 触发:
|
|
//! - select(item):选中实体 → 替换 @query 段为 [类型: 名] 标记
|
|
//! - update:mentionIndex:鼠标 hover 时同步高亮下标
|
|
|
|
import type { MentionItem } from './MentionItem'
|
|
|
|
defineProps<{
|
|
/** 联想候选(扁平,项目 + 任务 + 灵感) */
|
|
mentionItems: MentionItem[]
|
|
/** 按类型分组的候选(模板按组渲染组标题) */
|
|
mentionGroups: { key: string; label: string; items: MentionItem[] }[]
|
|
/** 未过滤候选总数(用于区分"无匹配" vs "空") */
|
|
mentionTotal: number
|
|
/** 当前高亮项在扁平列表中的下标 */
|
|
mentionIndex: number
|
|
/** 扁平下标查找函数(组内 item 在扁平列表中的位置,供高亮判定) */
|
|
mentionFlatIndex: (uid: string) => number
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
/** 选中实体 → 替换 @query 段为 [类型: 名] 标记 + 关闭浮层 */
|
|
(e: 'select', item: MentionItem): void
|
|
/** 鼠标 hover 时同步高亮下标 */
|
|
(e: 'update:mentionIndex', index: number): void
|
|
}>()
|
|
</script>
|
|
|
|
<template>
|
|
<!-- UX-10 §1.4: @ 实体引用浮层(@ 触发,选中插入 [类型:名] 标记,架构复用技能 popover) -->
|
|
<div class="ai-skill-popover ai-mention-popover">
|
|
<div v-if="!mentionItems.length" class="ai-skill-empty">
|
|
{{ mentionTotal ? $t('aiChat.mentionNoMatch') : $t('aiChat.mentionEmpty') }}
|
|
</div>
|
|
<div v-else>
|
|
<template v-for="g in mentionGroups" :key="g.key">
|
|
<div v-if="g.items.length" class="ai-mention-group">{{ g.label }}</div>
|
|
<div
|
|
v-for="item in g.items"
|
|
:key="item.uid"
|
|
class="ai-skill-item"
|
|
:class="{ 'ai-skill-item--active': mentionFlatIndex(item.uid) === mentionIndex }"
|
|
@click="emit('select', item)"
|
|
@mouseenter="emit('update:mentionIndex', mentionFlatIndex(item.uid))"
|
|
>
|
|
<div class="ai-skill-item-row">
|
|
<span class="ai-mention-item-type" :class="'ai-mention-item-type--' + item.type">{{ item.typeLabel }}</span>
|
|
<span class="ai-mention-item-name">{{ item.name }}</span>
|
|
<span class="ai-mention-item-desc">{{ item.desc }}</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* ── UX-10 §1.4: @ 实体引用浮层(复用 .ai-skill-popover 容器,仅新增组标题/类型标签) ── */
|
|
.ai-mention-popover {
|
|
position: absolute;
|
|
bottom: calc(100% + 4px);
|
|
left: 0;
|
|
right: 0;
|
|
max-height: 240px;
|
|
overflow-y: auto;
|
|
background: var(--df-bg-card);
|
|
border: 0.5px solid var(--df-border);
|
|
border-radius: var(--df-radius);
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
|
z-index: 20;
|
|
}
|
|
.ai-skill-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
padding: 6px 10px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
}
|
|
.ai-skill-item-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
.ai-skill-item--active,
|
|
.ai-skill-item:hover {
|
|
background: color-mix(in srgb, var(--df-accent) 14%, transparent);
|
|
}
|
|
.ai-skill-empty { padding: 8px 12px; color: var(--df-text-dim); opacity: 0.7; font-size: 12px; }
|
|
.ai-mention-group {
|
|
padding: 4px 10px 2px;
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
color: var(--df-text-dim);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
user-select: none;
|
|
}
|
|
.ai-mention-item-name { color: var(--df-text); font-weight: 500; flex-shrink: 0; }
|
|
.ai-mention-item-desc {
|
|
color: var(--df-text-dim);
|
|
flex: 1;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
/* 类型标签小色块(项目/任务区分) */
|
|
.ai-mention-item-type {
|
|
flex-shrink: 0;
|
|
padding: 1px 5px;
|
|
border-radius: var(--df-radius-sm);
|
|
font-size: 9px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.3px;
|
|
background: var(--df-bg-hover);
|
|
color: var(--df-text-dim);
|
|
}
|
|
.ai-mention-item-type--project {
|
|
background: color-mix(in srgb, var(--df-accent) 16%, transparent);
|
|
color: var(--df-accent);
|
|
}
|
|
.ai-mention-item-type--task {
|
|
background: color-mix(in srgb, var(--df-success) 16%, transparent);
|
|
color: var(--df-success);
|
|
}
|
|
</style>
|