优化: 前端u-agent令牌对齐与组件拆分续批

- 全局样式类(df-sec-h/df-toast族)替换散装实现,主题token去硬编码兜底
- 拆分子组件:ai-chat/queue-panel(队列时钟与编辑随迁)、
  conversation-sidebar/ConversationList、settings/provider-panel
- 父组件死副本清理(队列编辑/时钟/键位表,TS6133清零)并补
  fill-draft父侧转发(重构半成品断链修复)
- 验收:vue-tsc EXIT 0
This commit is contained in:
lxy
2026-10-18 23:15:57 +08:00
parent 302c4bcf52
commit 25ec9cf69a
62 changed files with 3475 additions and 2006 deletions
+149 -45
View File
@@ -52,6 +52,18 @@
@keydown="onItemKeydown($event, conv.id, true)"
>
<div class="ai-conv-item-main">
<!-- wave2 u-agent 范式: 左侧 7px 状态点。字段源 store.isGenerating(conv_state
generating/stopping/compressed) / pendingCountOf(审批挂起) / loop_state='parked',
无 error 字段故不映射;优先级 generating > 待审批 > parked > 灰 -->
<span
class="df-dot ai-conv-status"
:class="store.isGenerating(conv.id) ? 'df-dot--run ai-conv-status--run'
: pendingCountOf(conv.id) > 0 ? 'ai-conv-status--danger'
: conv.loop_state === 'parked' ? 'ai-conv-status--paused'
: 'df-dot--stop'"
:title="store.isGenerating(conv.id) ? t('aiChat.generating') : undefined"
aria-hidden="true"
></span>
<!-- UX走查 P3-1: 搜索态此前无重命名入口(无 startRename 分支),与分组视图降级不一致;
双击入口对齐分组视图,菜单入口供键盘用户(P2-6) -->
<input
@@ -70,7 +82,6 @@
@dblclick.stop="startRename(conv)"
>
{{ 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>
<!-- 无人值守边界徽标:该会话已授权任务边界(镜像渐进填充);点击随会话切回显 -->
@@ -145,6 +156,16 @@
@keydown="onItemKeydown($event, conv.id, true)"
>
<div class="ai-conv-item-main">
<!-- wave2 u-agent 范式: 左侧 7px 状态点(映射源同搜索态块注) -->
<span
class="df-dot ai-conv-status"
:class="store.isGenerating(conv.id) ? 'df-dot--run ai-conv-status--run'
: pendingCountOf(conv.id) > 0 ? 'ai-conv-status--danger'
: conv.loop_state === 'parked' ? 'ai-conv-status--paused'
: 'df-dot--stop'"
:title="store.isGenerating(conv.id) ? t('aiChat.generating') : undefined"
aria-hidden="true"
></span>
<input
v-if="editingConvId === conv.id"
:ref="setEditEl"
@@ -168,8 +189,6 @@
class="ai-conv-item-title-badge"
width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"
><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>
<!-- 无人值守边界徽标:该会话已授权任务边界(镜像渐进填充);点击随会话切回显 -->
@@ -225,6 +244,8 @@
@keydown.space.prevent="onRecover(conv.id)"
>
<div class="ai-conv-item-main">
<!-- wave2: 本组数据源 ai_list_recoverable(loop_state='parked' 完备真相),整组恒 parked→静态琥珀点 -->
<span class="df-dot ai-conv-status ai-conv-status--paused" aria-hidden="true"></span>
<span class="ai-conv-item-title">
{{ conv.title || t('aiChat.newConversation') }}
<span class="ai-conv-recover-badge">⏸</span>
@@ -260,9 +281,18 @@
@keydown="onItemKeydown($event, conv.id)"
>
<div class="ai-conv-item-main">
<!-- wave2 u-agent 范式: 归档项同款状态点(归档为静息态,默认落灰) -->
<span
class="df-dot ai-conv-status"
:class="store.isGenerating(conv.id) ? 'df-dot--run ai-conv-status--run'
: pendingCountOf(conv.id) > 0 ? 'ai-conv-status--danger'
: conv.loop_state === 'parked' ? 'ai-conv-status--paused'
: 'df-dot--stop'"
:title="store.isGenerating(conv.id) ? t('aiChat.generating') : undefined"
aria-hidden="true"
></span>
<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>
<!-- 无人值守边界徽标:该会话已授权任务边界(镜像渐进填充);点击随会话切回显 -->
@@ -296,6 +326,17 @@
</div>
</template>
<!-- F4-1: 会话列表分页——底部「加载更多」续拉下一页(首屏 50/组,归档组同口径放开) -->
<div
v-if="convListPaging.hasMore"
class="ai-conv-load-more"
role="button"
tabindex="0"
@click="onLoadMore"
@keydown.enter.prevent="onLoadMore"
@keydown.space.prevent="onLoadMore"
>{{ convListPaging.loading ? t('common.loading') : t('gitChanges.loadMore') }}</div>
<div v-if="store.state.conversations.length === 0" class="ai-conv-empty">
{{ t('aiChat.emptyConversation') }}
</div>
@@ -338,7 +379,8 @@ import { convPendingApprovals } from '../../composables/ai/aiShared'
// 镜像由边界配置卡在切会话/写入时渐进填充,无批量 IPC 属契约限制)
import { getConvBoundary, fetchConvBoundary } from '../../composables/ai/useTaskBoundary'
// UX走查 P2-7 只读订阅:rename/archive/pin 失败通知由 useAiConversations 发布,本组件弹就近 toast
import { convOpFailNotice } from '../../composables/ai/useAiConversations'
// F4-1: 会话列表分页游标(reactive,绑「加载更多」显隐/loading)+ 续拉/拉满动作
import { convOpFailNotice, convListPaging, loadMoreConversations, loadAllConversations } from '../../composables/ai/useAiConversations'
import { useToast } from '../../composables/useToast'
import { formatRelative } from '../../utils/time'
import type { AiConversationSummary } from '../../api/types'
@@ -662,6 +704,28 @@ const searchResults = computed<AiConversationSummary[] | null>(
() => store.filteredConversations.value,
)
// ── F4-1: 会话列表分页(加载更多按钮 + 搜索全量兜底) ──
/** 「加载更多」:续拉下一页(失败/无新增静默,按钮 loading 态由 convListPaging 驱动) */
function onLoadMore(): void {
void loadMoreConversations()
}
// 搜索输入即拉满:搜索过滤(stores.filteredConversations)跑在已加载列表上,未拉满时
// 第 51 条起的会话搜不到(硬 50 截断=搜索失灵)——输入非空且还有未拉页时一次拉尽,
// 之后搜索自然覆盖全量;已拉满(hasMore=false)零开销不触发。
watch(() => store.state.searchQuery, (q) => {
if (q.trim() && convListPaging.hasMore && !convListPaging.loading) {
void loadAllConversations()
}
})
// 加载更多/搜索拉满追加的新会话补拉边界徽标镜像(A10 空洞随列表加深扩大;
// hydrate 对已入镜像的会话跳过,常规刷新时零 IPC)
watch(() => store.state.conversations.length, () => {
void hydrateBoundaryBadges()
})
// ── 侧栏宽度拖拽 ──
const resizeState = { startX: 0, startWidth: 0 }
function onResizeMove(e: MouseEvent) {
@@ -756,7 +820,7 @@ defineExpose({
transition: background 0.15s var(--df-ease);
}
.ai-sidebar-resize-handle:hover {
background: var(--df-accent, #4f8cff);
background: var(--df-accent);
opacity: 0.5;
}
/* 拖拽中全局锁 cursor + 禁选中文本(挂在 body 上,跨元素生效) */
@@ -778,13 +842,21 @@ body.ai-sidebar-resizing * {
color: var(--df-text);
}
/* UX-06 §3.1: 对话搜索框(紧贴 header 下,样式跟随周围输入控件) */
/* UX-06 §3.1: 对话搜索框(wave2 u-agent .sb-search 范式:容器成框,卡底+focus 边框 info) */
.ai-conv-search {
position: relative;
display: flex;
align-items: center;
padding: 6px 10px;
border-bottom: 0.5px solid var(--df-border);
margin: 8px 10px;
padding: 0 8px;
height: 30px;
background: var(--df-bg-card);
border: 0.5px solid var(--df-border);
border-radius: var(--df-radius-sm);
transition: border-color 0.15s var(--df-ease);
}
.ai-conv-search:focus-within {
border-color: var(--df-info);
}
.ai-conv-search-icon {
flex: none;
@@ -800,8 +872,8 @@ body.ai-sidebar-resizing * {
font-size: 12px;
color: var(--df-text);
}
/* 聚焦可见环:无边框输入聚焦无变化,且 scoped 选择器特异性高于全局 :focus-visible 兜底,须就地补环,仅键盘聚焦显示 */
.ai-conv-search-input:focus-visible { outline: 2px solid var(--df-accent); outline-offset: 1px; }
/* 聚焦可见环:容器已随 :focus-within 变 info 边框,键盘环同色补强(scoped 特异性须盖全局兜底) */
.ai-conv-search-input:focus-visible { outline: 2px solid var(--df-info); outline-offset: 1px; }
.ai-conv-search-input::placeholder {
color: var(--df-text-dim);
}
@@ -819,12 +891,15 @@ body.ai-sidebar-resizing * {
.ai-conv-list {
flex: 1;
overflow-y: auto;
padding: 4px;
padding: 4px 6px; /* u-agent .sb-list 范式;滚动条走 global ::-webkit-scrollbar token 体系 */
}
/* wave2 u-agent 范式: 分组头 10px uppercase + letter-spacing 1px + dim */
.ai-conv-group-title {
padding: 8px 8px 3px;
font-size: 11px;
padding: 10px 8px 4px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--df-text-dim);
user-select: none;
}
@@ -851,7 +926,10 @@ body.ai-sidebar-resizing * {
.ai-conv-group-count {
margin-left: 2px;
font-weight: 400;
opacity: 0.7;
font-family: var(--df-font-mono); /* mono 计数(df-mono 范式),upper/lsp 回退保证数字不散 */
font-size: 10px;
letter-spacing: 0;
opacity: 0.9;
}
.ai-conv-group-empty {
padding: 2px 8px 4px 20px;
@@ -859,40 +937,47 @@ body.ai-sidebar-resizing * {
color: var(--df-text-dim);
font-style: italic;
}
/* wave2 u-agent .cv 范式: 单行条目,8px 级密度;hover/active 同走灰阶卡底(GitHub 两级同底、
active 由左 2px accent 实线区分),深浅主题同 token 自动适配 */
.ai-conv-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 8px;
padding: 8px;
border-radius: var(--df-radius);
cursor: pointer;
transition: background 0.15s;
gap: 4px;
gap: 6px;
}
.ai-conv-item:hover {
background: var(--df-sidebar-hover);
background: var(--df-bg-card-hover);
}
.ai-conv-item--active {
background: var(--df-sidebar-active);
background: var(--df-bg-card-hover);
box-shadow: inset 2px 0 0 var(--df-accent);
}
/* 切换进行时(UX走查 P1):与活跃项同底色示"已选中"(乐观预高亮),降不透明度示"加载中";
IPC 返回后 activeConversationId 落定、本类移除,视觉无缝衔接;失败回滚高亮回原活跃项 */
.ai-conv-item--switching {
background: var(--df-sidebar-active);
background: var(--df-bg-card-hover);
opacity: 0.7;
}
.ai-conv-item--archived .ai-conv-item-title {
opacity: 0.6;
}
/* wave2: 行内布局对齐 .cv-top([状态点][标题省略][时间]) */
.ai-conv-item-main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
flex-direction: row;
align-items: center;
gap: 6px;
}
.ai-conv-item-title {
font-size: 13px;
flex: 1;
min-width: 0;
font-size: 12px;
font-weight: 500;
color: var(--df-text);
white-space: nowrap;
@@ -902,7 +987,6 @@ body.ai-sidebar-resizing * {
display: inline-flex;
align-items: center;
gap: 3px;
min-width: 0;
}
/* UX-2025-20 §7.4: AI 生成标题淡入(0.3s opacity + transform 过渡) */
.ai-conv-item-title--flash {
@@ -917,34 +1001,35 @@ body.ai-sidebar-resizing * {
to { opacity: 1; transform: translateX(0); }
}
.ai-conv-item-edit {
flex: 1 1 0; /* wave2: main 已行化,输入框按 flex 填充(min-width:0 防 input 固有宽撑破窄侧栏) */
min-width: 0;
width: auto;
font-size: 11px;
color: var(--df-text);
width: 100%;
height: 18px;
padding: 0 4px;
border: 1px solid var(--df-accent);
border-radius: var(--df-radius-sm);
background: var(--df-bg, #fff);
background: var(--df-bg);
outline: none;
box-sizing: border-box;
}
/* 聚焦可见环:边框恒为 accent 聚焦无变化,且 scoped 选择器特异性高于全局 :focus-visible 兜底,须就地补环,仅键盘聚焦显示 */
.ai-conv-item-edit:focus-visible { outline: 2px solid var(--df-accent); outline-offset: 1px; }
/* wave2 u-agent 范式: 时间/计数元数据 mono 10px dim(.df-mono 同款视觉) */
.ai-conv-item-time {
font-size: 9px;
flex: none;
font-family: var(--df-font-mono);
font-size: 10px;
color: var(--df-text-dim);
}
/* 多会话并发生成指示器(会话项标题行内脉冲点,多会话可同时显示) */
.ai-conv-gen-dot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--df-accent);
flex-shrink: 0;
animation: ai-conv-gen-pulse 1.4s ease-in-out infinite;
}
/* wave2 u-agent .st 状态点变体: 公共 .df-dot/.df-dot--run/.df-dot--stop 管造型与绿/灰底,
此处只补动效与缺位色(全 token): run 绿+glow+脉冲(global df-pulse)、danger 待审批红、paused 停靠琥珀 */
.ai-conv-status--run { box-shadow: 0 0 6px var(--df-success); animation: df-pulse 1.5s infinite; }
.ai-conv-status--danger { background: var(--df-danger); }
.ai-conv-status--paused { background: var(--df-warning); }
@keyframes ai-conv-gen-pulse {
/* 供 .ai-conv-recover-dot 分组级脉冲用(原行内 gen-dot 已由状态点接管) */
0%, 100% { opacity: 0.35; transform: scale(0.85); }
50% { opacity: 1; transform: scale(1.15); }
}
@@ -955,7 +1040,7 @@ body.ai-sidebar-resizing * {
height: 6px;
border-radius: 50%;
margin-right: 5px;
background: var(--df-warning, #f0c75e);
background: var(--df-warning);
animation: ai-conv-gen-pulse 2s ease-in-out infinite;
}
.ai-conv-item--recoverable {
@@ -966,7 +1051,7 @@ body.ai-sidebar-resizing * {
margin-left: 2px;
font-size: 10px;
line-height: 1;
color: var(--df-warning, #f0c75e);
color: var(--df-warning);
cursor: help;
}
/* V1: 跨会话待审批徽标(橙点计数,warning 色系与待审批语义一致;固定尺寸不挤标题换行) */
@@ -975,7 +1060,7 @@ body.ai-sidebar-resizing * {
height: 12px;
padding: 0 3px;
border-radius: 6px;
background: var(--df-warning, #f0c75e);
background: var(--df-warning);
color: #000;
font-size: 9px;
font-weight: 600;
@@ -1032,9 +1117,13 @@ body.ai-sidebar-resizing * {
color: var(--df-text);
}
/* 已置顶对话视觉强调(左边线,不依赖菜单展开,一眼识别)。
pinned 类挂在 .ai-conv-item 上(操作收进更多菜单后,按钮不再常驻,改 item 级类)。 */
pinned 类挂在 .ai-conv-item 上(操作收进更多菜单后,按钮不再常驻,改 item 级类)。
wave2: 选中态同占左缘,置顶线降为 45% 透明 accent 让位,选中实线优先 */
.ai-conv-item--pinned {
box-shadow: inset 2px 0 0 var(--df-accent, var(--df-text));
box-shadow: inset 2px 0 0 color-mix(in srgb, var(--df-accent) 45%, transparent);
}
.ai-conv-item--active.ai-conv-item--pinned {
box-shadow: inset 2px 0 0 var(--df-accent);
}
/* ── 「更多」操作菜单(置顶/归档/删除)──
「...」按钮 hover 时随 actions 浮出(同原 4 按钮交互),点击展开下拉。
@@ -1056,8 +1145,8 @@ body.ai-sidebar-resizing * {
display: flex;
flex-direction: column;
gap: 1px;
background: var(--df-bg, #fff);
border: 1px solid var(--df-border, #e2e2e2);
background: var(--df-bg);
border: 1px solid var(--df-border);
border-radius: var(--df-radius-sm);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
}
@@ -1088,6 +1177,21 @@ body.ai-sidebar-resizing * {
font-size: 11px;
color: var(--df-text-dim);
}
/* F4-1: 列表底部「加载更多」(轻量文字行,hover 同列表项灰阶卡底范式) */
.ai-conv-load-more {
margin: 4px 2px;
padding: 6px 8px;
border-radius: var(--df-radius);
font-size: 11px;
color: var(--df-text-dim);
text-align: center;
cursor: pointer;
user-select: none;
}
.ai-conv-load-more:hover {
background: var(--df-bg-card-hover);
color: var(--df-text);
}
/* UX走查 P2-7: 会话操作失败就近 toast(仅 error 一型)。视觉对齐 AiChat .ai-toast,
但 AiChat 版是 scoped 样式渗透不进来,故自持一份;侧栏窄,max-width 收 85% */
@@ -1102,7 +1206,7 @@ body.ai-sidebar-resizing * {
z-index: 30; /* 高于操作菜单(20),菜单展开时失败反馈不被盖 */
max-width: 85%;
overflow-wrap: anywhere;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
box-shadow: var(--df-shadow-sm);
background: var(--df-danger-bg);
color: var(--df-danger);
border: 0.5px solid var(--df-danger);