优化: AI Chat 渲染与无障碍 + 文件查看 + 同步 IO 收口
- 渲染: 结果/过程分离(气泡只放最终结果,过程轮叙述+工具卡收进轨迹卡默认收起可展开——治『流式后正文消失』且不再 N 气泡打扰) - 渲染 P0: v-memo 定向 dep(只有真正渲染流式块的 item 失效,其余取占位) + renderItems 桶 memo(签名覆盖全消费输入,item 引用稳定) + 指纹 watch 降为 O(1) toolEpoch - TopBar 三处 O(n) messages computed 合并单次遍历;md 缓存改有界 LRU(除满即全清抖动);useTurnTrace 补 never 穷尽 + default;MessageItem 模板去重 - 无障碍(四批累计): 可点容器键盘化(含 role/tabindex/keydown/aria-expanded) + icon-only aria-label 约 60 处 + 装饰图标 aria-hidden + 52 个滚动容器 overscroll-behavior:contain + 设置开关聚焦可见环 + outline 零补偿站点补 :focus-visible - UX 走查项: 灵感 IDEA-10/11、知识 KNOW-6/7/9、项目详情 O8、审计 SA-4、设置 SA-6/26/27/28(脏表单/冗余 save/clamp/删除二次确认) - 文件查看专项: 切工程状态残留 F5/F6、FileTree 去重与展开反馈 F7/F15/F17、切文件头部残留 F18、R 状态着色 F19;FilePreview diff 文件头(---/+++)误当增删渲染修复 + 大纲行高亮死样式修复 + 图片失败文案 i18n 化 - 同步 IO 收口(异步上下文内阻塞): unattended 看板读写改 tokio::fs、fetch_url 探活改 spawn_blocking、http/generate_image 清理改 tokio::fs - 子代理 P3 批量: 任务书钉扎上限/截断文案区分父子语境/环境画像日期/temperature 可覆盖/model 回退上报/lang 透传 - crate 清理: df-execute·df-types 注释与格式、df-ideas 过时注释、df-nodes subflow 夸大描述、df-project 死变量
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
//! (含流式分块、工具卡片、token 用量、错误重试、压缩摘要置顶等聚合逻辑),
|
||||
//! 本组件专注 user,不做 AI 消息渲染,避免两套渲染入口并存导致的死代码与职责混乱。
|
||||
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { AiMessage, ContentPart, MentionSpan } from '../../api/types'
|
||||
import { useAiStore } from '../../stores/ai'
|
||||
@@ -30,7 +31,7 @@ function leadingSkill(msg: AiMessage): { name: string; rest: string } | null {
|
||||
return { name: m[1], rest: m[2] ?? '' }
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
msg: AiMessage
|
||||
/** 是否显示编辑按钮(父合成:末条 user + 非生成中 + 非编辑中) */
|
||||
canEdit?: boolean
|
||||
@@ -92,6 +93,12 @@ function imageSrc(img: ImageContentPart): string {
|
||||
function imgAlt(img: ImageContentPart): string {
|
||||
return img.alt || t('aiChat.imagePreviewAlt')
|
||||
}
|
||||
|
||||
// 模板中同一渲染内多次引用的纯派生值 → computed 缓存一次求值
|
||||
// (纯函数仅依赖 props.msg / store.state.skills 等响应式源,缓存语义与原先逐次调用等价)
|
||||
const skill = computed(() => leadingSkill(props.msg))
|
||||
const segments = computed(() => segmentUserContent(props.msg))
|
||||
const imageParts = computed(() => msgImageParts(props.msg))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -107,19 +114,19 @@ function imgAlt(img: ImageContentPart): string {
|
||||
</div>
|
||||
<div class="ai-msg-bubble ai-msg-bubble--user">
|
||||
<!-- 问题3 技能调用留痕:前导 /技能名 命中已安装技能 → 紫色 chip 标识,替代原文前缀显示 -->
|
||||
<template v-if="leadingSkill(msg)">
|
||||
<span class="ai-msg-chip ai-msg-chip--skill" :title="t('aiChat.skillBadgeTip', { name: leadingSkill(msg)!.name })">⚡ /{{ leadingSkill(msg)!.name }}</span>
|
||||
<template v-if="leadingSkill(msg)!.rest">{{ ' ' + leadingSkill(msg)!.rest }}</template>
|
||||
<template v-if="skill">
|
||||
<span class="ai-msg-chip ai-msg-chip--skill" :title="t('aiChat.skillBadgeTip', { name: skill!.name })">⚡ /{{ skill!.name }}</span>
|
||||
<template v-if="skill!.rest">{{ ' ' + skill!.rest }}</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-for="(seg, idx) in segmentUserContent(msg)" :key="msg.id + '-seg-' + idx">
|
||||
<template v-for="(seg, idx) in segments" :key="msg.id + '-seg-' + idx">
|
||||
<span v-if="seg.type === 'text'">{{ seg.text }}</span>
|
||||
<span v-else class="ai-msg-chip" :class="'ai-msg-chip--' + seg.span.kind">{{ seg.span.label }}</span>
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="msgImageParts(msg).length">
|
||||
<template v-if="imageParts.length">
|
||||
<div class="ai-msg-images">
|
||||
<img v-for="(img, idx) in msgImageParts(msg)" :key="msg.id + '-img-' + idx" :src="imageSrc(img)" :alt="imgAlt(img)" class="ai-msg-image" loading="lazy" />
|
||||
<img v-for="(img, idx) in imageParts" :key="msg.id + '-img-' + idx" :src="imageSrc(img)" :alt="imgAlt(img)" class="ai-msg-image" loading="lazy" />
|
||||
</div>
|
||||
</template>
|
||||
<!-- UX-09:末条 user 消息编辑按钮(hover 显示),点击经 start-edit 链回填输入框编辑重发 -->
|
||||
|
||||
Reference in New Issue
Block a user