优化: 对话交互修复与界面完善
- 修输入法组词回车直发/分离窗误关/出错后按钮死锁 - 新建任务失败弹窗内联提示,会话切换乐观高亮 - 状态多选联动刷新不再冲空可见行 - 测试连接保留已配开关与权重,防抖按提供商分键 - 代码块复制/长回复折叠带量/队列回填/排队时钟 - 子代理完成卡渲染,死链卡片与引用清理 - 截图渲染换 pro 版依赖,样式 token 化收尾
This commit is contained in:
@@ -40,6 +40,7 @@
|
||||
class="ai-conv-item"
|
||||
:class="{
|
||||
'ai-conv-item--active': conv.id === store.state.activeConversationId,
|
||||
'ai-conv-item--switching': conv.id === pendingSwitchId,
|
||||
'ai-conv-item--archived': conv.archived,
|
||||
'ai-conv-item--pinned': conv.pinned,
|
||||
}"
|
||||
@@ -49,6 +50,8 @@
|
||||
<span class="ai-conv-item-title" :title="conv.title || ''">
|
||||
{{ conv.title || t('aiChat.newConversation') }}
|
||||
<span v-if="store.isGenerating(conv.id)" class="ai-conv-gen-dot" :title="t('aiChat.generating')"></span>
|
||||
<!-- V1: 待审批徽标(橙点计数,复用既有 aiChat.pendingApprovalCount 文案) -->
|
||||
<span v-if="pendingCountOf(conv.id) > 0" class="ai-conv-approve-badge" :title="t('aiChat.pendingApprovalCount', { n: pendingCountOf(conv.id) })">{{ pendingCountOf(conv.id) }}</span>
|
||||
</span>
|
||||
<span class="ai-conv-item-time">{{ formatRelative(conv.updated_at) }}</span>
|
||||
</div>
|
||||
@@ -102,8 +105,8 @@
|
||||
:key="conv.id"
|
||||
:ref="(el) => setItemEl(conv.id, el)"
|
||||
class="ai-conv-item"
|
||||
:class="{ 'ai-conv-item--active': conv.id === store.state.activeConversationId, 'ai-conv-item--pinned': conv.pinned }"
|
||||
@click="store.switchConversation(conv.id)"
|
||||
:class="{ 'ai-conv-item--active': conv.id === store.state.activeConversationId, 'ai-conv-item--switching': conv.id === pendingSwitchId, 'ai-conv-item--pinned': conv.pinned }"
|
||||
@click="beginSwitch(conv.id)"
|
||||
>
|
||||
<div class="ai-conv-item-main">
|
||||
<input
|
||||
@@ -111,8 +114,7 @@
|
||||
:ref="setEditEl"
|
||||
v-model="editText"
|
||||
class="ai-conv-item-edit"
|
||||
@keyup.enter="commitRename(conv.id)"
|
||||
@keyup.esc="cancelRename"
|
||||
@keydown="onRenameKeydown($event, conv.id)"
|
||||
@blur="commitRename(conv.id)"
|
||||
@click.stop
|
||||
/>
|
||||
@@ -132,6 +134,8 @@
|
||||
><polyline points="20 6 9 17 4 12"/></svg>
|
||||
<!-- F-09: 多会话并发生成指示器(后台/当前会话生成中均显脉冲点,多会话可同时显示) -->
|
||||
<span v-if="store.isGenerating(conv.id)" class="ai-conv-gen-dot" :title="t('aiChat.generating')"></span>
|
||||
<!-- V1: 待审批徽标(橙点计数,复用既有 aiChat.pendingApprovalCount 文案) -->
|
||||
<span v-if="pendingCountOf(conv.id) > 0" class="ai-conv-approve-badge" :title="t('aiChat.pendingApprovalCount', { n: pendingCountOf(conv.id) })">{{ pendingCountOf(conv.id) }}</span>
|
||||
</span>
|
||||
<span class="ai-conv-item-time">{{ formatRelative(conv.updated_at) }}</span>
|
||||
</div>
|
||||
@@ -171,13 +175,15 @@
|
||||
:key="'a-' + conv.id"
|
||||
:ref="(el) => setItemEl(conv.id, el)"
|
||||
class="ai-conv-item ai-conv-item--archived"
|
||||
:class="{ 'ai-conv-item--active': conv.id === store.state.activeConversationId, 'ai-conv-item--pinned': conv.pinned }"
|
||||
@click="store.switchConversation(conv.id)"
|
||||
:class="{ 'ai-conv-item--active': conv.id === store.state.activeConversationId, 'ai-conv-item--switching': conv.id === pendingSwitchId, 'ai-conv-item--pinned': conv.pinned }"
|
||||
@click="beginSwitch(conv.id)"
|
||||
>
|
||||
<div class="ai-conv-item-main">
|
||||
<span class="ai-conv-item-title" :title="conv.title || ''">
|
||||
{{ conv.title || t('aiChat.newConversation') }}
|
||||
<span v-if="store.isGenerating(conv.id)" class="ai-conv-gen-dot" :title="t('aiChat.generating')"></span>
|
||||
<!-- V1: 待审批徽标(橙点计数,复用既有 aiChat.pendingApprovalCount 文案) -->
|
||||
<span v-if="pendingCountOf(conv.id) > 0" class="ai-conv-approve-badge" :title="t('aiChat.pendingApprovalCount', { n: pendingCountOf(conv.id) })">{{ pendingCountOf(conv.id) }}</span>
|
||||
</span>
|
||||
<span class="ai-conv-item-time">{{ formatRelative(conv.updated_at) }}</span>
|
||||
</div>
|
||||
@@ -235,6 +241,8 @@
|
||||
import { ref, computed, nextTick, watch, onMounted, onBeforeUnmount, type ComponentPublicInstance } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAiStore } from '../../stores/ai'
|
||||
// V1 只读消费:跨会话待审批真相源(state.pendingApprovals 仅是活跃会话投影,后台会话挂起不进投影)
|
||||
import { convPendingApprovals } from '../../composables/ai/aiShared'
|
||||
import { formatRelative } from '../../utils/time'
|
||||
import type { AiConversationSummary } from '../../api/types'
|
||||
|
||||
@@ -302,6 +310,20 @@ function cancelRename() {
|
||||
editingConvId.value = null
|
||||
}
|
||||
|
||||
/** P1-1 重命名框键盘处理:输入法组词中(isComposing/229)的 Enter/Esc 属 IME 选词/取消组词,
|
||||
* 不当重命名提交/取消处理,防标题被半截拼音覆盖。原 keyup.enter/.esc 改 keydown 统一挂守卫:
|
||||
* 组词上屏的回车到 keyup 阶段 isComposing 已复位守不住,keydown 才拦得到。 */
|
||||
function onRenameKeydown(e: KeyboardEvent, convId: string) {
|
||||
if (e.isComposing || e.keyCode === 229) return
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
commitRename(convId)
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
cancelRename()
|
||||
}
|
||||
}
|
||||
|
||||
// ── 会话项「更多」操作菜单(置顶/归档/删除)──
|
||||
const actionsOpenId = ref<string | null>(null)
|
||||
|
||||
@@ -321,13 +343,31 @@ function onActionsOutsideClick(e: MouseEvent) {
|
||||
actionsOpenId.value = null
|
||||
}
|
||||
|
||||
// ── 会话切换过渡态(UX走查 2026-10-03 P1):点击即预高亮目标项(乐观),IPC 返回后随
|
||||
// activeConversationId 落定接管高亮;失败/过期时 finally 清 pending,高亮自动回滚到原活跃项。
|
||||
// 治:大历史会话 IPC 数百 ms 期间点击项不高亮、原高亮不动、无任何进行时反馈,快速连点像失灵。
|
||||
const pendingSwitchId = ref<string | null>(null)
|
||||
function beginSwitch(id: string) {
|
||||
// 已活跃会话:switchConversation 内部同判短路,无过渡期,不挂 pending
|
||||
if (id === store.state.activeConversationId) {
|
||||
void store.switchConversation(id)
|
||||
return
|
||||
}
|
||||
pendingSwitchId.value = id
|
||||
void store.switchConversation(id).finally(() => {
|
||||
// 仅当 pending 仍是本次点击的 id 才清:快速连点 B 覆盖 A 后,A 的迟到返回不得清掉 B 的过渡态
|
||||
if (pendingSwitchId.value === id) pendingSwitchId.value = null
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索结果点击切对话后清空 searchQuery,退出搜索模式回原时间分组视图。
|
||||
* M3:命中项可能在折叠的时间分组或折叠的归档分组内,选中态不可见。切回分组视图后展开所在分组
|
||||
* 并 scrollIntoView,确保选中项立即可见(原仅切 searchQuery,选中项落在折叠组里看不到高亮)。
|
||||
*/
|
||||
function onSelectSearchResult(id: string) {
|
||||
void store.switchConversation(id)
|
||||
// 走 beginSwitch 挂切换过渡态(乐观预高亮),与分组视图点击口径一致
|
||||
beginSwitch(id)
|
||||
store.state.searchQuery = ''
|
||||
// nextTick 后视图已切回分组态;ensureVisible 展开所在组 + 滚动定位
|
||||
void nextTick(() => ensureActiveVisible())
|
||||
@@ -370,6 +410,30 @@ function ensureActiveVisible(): void {
|
||||
})
|
||||
}
|
||||
|
||||
// ── V1: 跨会话待审批徽标 ──
|
||||
// convPendingApprovals Map 本体非响应式(aiShared 注明渲染只依赖投影数组),故 computed 读 Map 的
|
||||
// 同时锚定三类响应式源触发重算:
|
||||
// - state.pendingApprovals(活跃会话投影,审批增删/切换恢复整体替换时必变)→ 活跃会话即时刷新;
|
||||
// - conversations 列表(新建/删除/排序刷新);
|
||||
// - pendingTick 2.5s 轮询兜底:后台会话新增/解除挂起不触碰任何响应式源(aiShared 只写裸 Map),
|
||||
// 轻量周期重算补偿(徽标属弱实时信息,秒级延迟可接受)。
|
||||
const pendingTick = ref(0)
|
||||
let pendingPollTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const pendingCountByConv = computed(() => {
|
||||
void store.state.pendingApprovals.length
|
||||
void store.state.conversations.length
|
||||
void pendingTick.value
|
||||
const out = new Map<string, number>()
|
||||
for (const [convId, arr] of convPendingApprovals) {
|
||||
if (arr.length > 0) out.set(convId, arr.length)
|
||||
}
|
||||
return out
|
||||
})
|
||||
function pendingCountOf(convId: string): number {
|
||||
return pendingCountByConv.value.get(convId) ?? 0
|
||||
}
|
||||
|
||||
// ── 对话分组:今天 / 昨天 / 更早 ──
|
||||
const bucketLabels = computed<Record<string, string>>(() => ({
|
||||
today: t('aiChat.today'),
|
||||
@@ -447,10 +511,14 @@ function onResizeStart(e: MouseEvent) {
|
||||
onMounted(() => {
|
||||
// 点击外部关闭更多操作菜单
|
||||
document.addEventListener('click', onActionsOutsideClick)
|
||||
// V1:后台会话待审批变化兜底轮询(见 pendingCountByConv 注)
|
||||
pendingPollTimer = setInterval(() => { pendingTick.value++ }, 2500)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', onActionsOutsideClick)
|
||||
// 释放待审批徽标轮询计时器
|
||||
if (pendingPollTimer) { clearInterval(pendingPollTimer); pendingPollTimer = null }
|
||||
// 拖拽中途卸载兜底摘临时 document listener(原 AiChat 同段迁移)
|
||||
document.removeEventListener('mousemove', onResizeMove)
|
||||
document.removeEventListener('mouseup', onResizeEnd)
|
||||
@@ -612,6 +680,12 @@ body.ai-sidebar-resizing * {
|
||||
.ai-conv-item--active {
|
||||
background: var(--df-sidebar-active);
|
||||
}
|
||||
/* 切换进行时(UX走查 P1):与活跃项同底色示"已选中"(乐观预高亮),降不透明度示"加载中";
|
||||
IPC 返回后 activeConversationId 落定、本类移除,视觉无缝衔接;失败回滚高亮回原活跃项 */
|
||||
.ai-conv-item--switching {
|
||||
background: var(--df-sidebar-active);
|
||||
opacity: 0.7;
|
||||
}
|
||||
.ai-conv-item--archived .ai-conv-item-title {
|
||||
opacity: 0.6;
|
||||
}
|
||||
@@ -676,6 +750,21 @@ body.ai-sidebar-resizing * {
|
||||
0%, 100% { opacity: 0.35; transform: scale(0.85); }
|
||||
50% { opacity: 1; transform: scale(1.15); }
|
||||
}
|
||||
/* V1: 跨会话待审批徽标(橙点计数,warning 色系与待审批语义一致;固定尺寸不挤标题换行) */
|
||||
.ai-conv-approve-badge {
|
||||
flex-shrink: 0;
|
||||
min-width: 12px;
|
||||
height: 12px;
|
||||
padding: 0 3px;
|
||||
border-radius: 6px;
|
||||
background: var(--df-warning, #f0c75e);
|
||||
color: #000;
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
line-height: 12px;
|
||||
text-align: center;
|
||||
cursor: help;
|
||||
}
|
||||
.ai-conv-item-actions {
|
||||
display: none;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user