优化: 无障碍第二批(icon-only aria-label + 容器键盘化 20 处)

- TopBar 10 处 icon-only 按钮补 aria-label(复用同 :title 表达式,含三态 title)
- ConversationSidebar: 4 icon-only aria-label + 2 分组标题键盘化(role/tabindex/aria-expanded/keydown)
- MessageList 2 icon-only; KnowledgeDetail 3 章节折叠键盘化; dashboard 两列表项键盘化
- FileExplorer 面包屑/模块下拉项键盘化; ConflictResolver 冲突文件折叠键盘化
- 约定: 遮罩/点外关闭类(@click.self)非键盘缺口,跳过;零新增 i18n 键
- 验收: bunx vue-tsc --noEmit EXIT 0;看板 P3-2 更新(两批共 23 处)
This commit is contained in:
lxy
2026-10-10 01:04:09 +08:00
parent 9d83a06300
commit 2a3a6cd25b
9 changed files with 98 additions and 26 deletions
+1 -1
View File
@@ -25,7 +25,7 @@
> 详情:[05-代码审查/UX走查-Web规范合规-2026-10-05.md](./05-代码审查/UX走查-Web规范合规-2026-10-05.md) > 详情:[05-代码审查/UX走查-Web规范合规-2026-10-05.md](./05-代码审查/UX走查-Web规范合规-2026-10-05.md)
- [x] P2-2 高频可点 div 键盘化:ToolCard:11 卡头/MessageList:1423 轨迹头/AuditLog:67,119 审计行(@click div 全仓 56 处,去 mask 合规项约 20 真缺口,高频改 button 其余 tabindex+keydown) ✅ **2026-10-10 完成(named 高频点)**:四面包裹处均补 `role="button" tabindex="0"` + `@keydown.enter/.space.prevent` 复用原 handler(带展开态的补 `:aria-expanded`;AuditLog 的 `<tr>` 仅补 tabindex+keydown——加 role=button 会抹掉表格行语义,故不加)。验收:`bunx vue-tsc --noEmit` EXIT 0。**剩余**:全仓另 ~16 处 @click div 未清(见 P3-2)。 - [x] P2-2 高频可点 div 键盘化:ToolCard:11 卡头/MessageList:1423 轨迹头/AuditLog:67,119 审计行(@click div 全仓 56 处,去 mask 合规项约 20 真缺口,高频改 button 其余 tabindex+keydown) ✅ **2026-10-10 完成(named 高频点)**:四面包裹处均补 `role="button" tabindex="0"` + `@keydown.enter/.space.prevent` 复用原 handler(带展开态的补 `:aria-expanded`;AuditLog 的 `<tr>` 仅补 tabindex+keydown——加 role=button 会抹掉表格行语义,故不加)。验收:`bunx vue-tsc --noEmit` EXIT 0。**剩余**:全仓另 ~16 处 @click div 未清(见 P3-2)。
- [~] P3-2 icon-only 按钮缺 aria-label ~15 处(title 非等价读屏补偿;EnrichmentPanel 关闭钮/ProjectDetail:185,189 编辑删除钮等)+剩余 div 键盘化 ✅ **2026-10-10 完成 3/15**(EnrichmentPanel 关闭钮 + ProjectDetail 模块编辑/删除钮,均复用已有 `:title` 同表达式为 `:aria-label`,未新增 i18n 键);**剩余 ~12 处** 未清,按「存量 vs 仅本批」拍板点待用户定范围。 - [~] P3-2 icon-only 按钮缺 aria-label ~15 处(title 非等价读屏补偿;EnrichmentPanel 关闭钮/ProjectDetail:185,189 编辑删除钮等)+剩余 div 键盘化 —— ✅ **2026-10-10 两批共 23 处**:①批次一 3 处(EnrichmentPanel 关闭 / ProjectDetail 模块编辑·删除);②批次二 20 处(TopBar 10 icon-only 按钮 aria-label / ConversationSidebar 4 aria-label + 2 分组标题键盘化 / MessageList 2 icon-only / KnowledgeDetail 3 章节折叠键盘化 / dashboard 两列表项键盘化 / FileExplorer 面包屑·模块下拉项键盘化 / ConflictResolver 冲突文件折叠键盘化)。全部复用已有 `:title`/i18n key,**零新增键**;遮罩类(@click.self)按约定跳过。验收:`bunx vue-tsc --noEmit` EXIT 0。**剩余**少数(会话项 space 键、dblclick 重命名入口等)+「存量 vs 仅本批」拍板点仍在。
- 拍板点:修复范围「存量 vs 仅本批」——transition:all/outline:none 大半为存量;本批新代码 a11y 意识已好(svg aria-hidden 批补/MessageList role=log+aria-live/hover-only focus-within 补偿),仅清本批则工作量降 60%+ - 拍板点:修复范围「存量 vs 仅本批」——transition:all/outline:none 大半为存量;本批新代码 a11y 意识已好(svg aria-hidden 批补/MessageList role=log+aria-live/hover-only focus-within 补偿),仅清本批则工作量降 60%+
### 💡 2026-09-29 aichat 可用性与交互成本研究(三动线走查+异常循环截图实证,39 项新发现) ### 💡 2026-09-29 aichat 可用性与交互成本研究(三动线走查+异常循环截图实证,39 项新发现)
+9 -1
View File
@@ -4,7 +4,15 @@
<span class="conflict-title">⚠ {{ t('aiChat.conflictsPending', { n: conflicts.length }) }}</span> <span class="conflict-title">⚠ {{ t('aiChat.conflictsPending', { n: conflicts.length }) }}</span>
</div> </div>
<div v-for="c in conflicts" :key="c.id" class="conflict-item"> <div v-for="c in conflicts" :key="c.id" class="conflict-item">
<div class="conflict-file conflict-file--toggle" @click="toggleConflict(c.id)"> <div
class="conflict-file conflict-file--toggle"
role="button"
tabindex="0"
:aria-expanded="isExpanded(c.id)"
@click="toggleConflict(c.id)"
@keydown.enter.prevent="toggleConflict(c.id)"
@keydown.space.prevent="toggleConflict(c.id)"
>
<span class="conflict-arrow" :class="{ 'conflict-arrow--open': isExpanded(c.id) }">▸</span> <span class="conflict-arrow" :class="{ 'conflict-arrow--open': isExpanded(c.id) }">▸</span>
<span class="conflict-type-badge" :class="'badge--' + c.conflict_type"> <span class="conflict-type-badge" :class="'badge--' + c.conflict_type">
{{ c.conflict_type === 'semantic' ? '语义' : '文件' }} {{ c.conflict_type === 'semantic' ? '语义' : '文件' }}
+18 -5
View File
@@ -14,7 +14,7 @@
/> />
<div class="ai-conv-sidebar-header"> <div class="ai-conv-sidebar-header">
<span class="ai-conv-sidebar-title">{{ t('aiChat.sidebarTitle') }}</span> <span class="ai-conv-sidebar-title">{{ t('aiChat.sidebarTitle') }}</span>
<button class="ai-btn-icon" @click="emit('confirm-new-conversation')" :title="t('aiChat.newConversation')"> <button class="ai-btn-icon" @click="emit('confirm-new-conversation')" :title="t('aiChat.newConversation')" :aria-label="t('aiChat.newConversation')">
<svg aria-hidden="true" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> <svg aria-hidden="true" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
</button> </button>
</div> </div>
@@ -80,7 +80,7 @@
</div> </div>
<div class="ai-conv-item-actions"> <div class="ai-conv-item-actions">
<div class="ai-conv-item-more"> <div class="ai-conv-item-more">
<button class="ai-conv-item-act" @click.stop="toggleActionsMenu(conv.id)" :title="t('aiChat.moreActions')"> <button class="ai-conv-item-act" @click.stop="toggleActionsMenu(conv.id)" :title="t('aiChat.moreActions')" :aria-label="t('aiChat.moreActions')">
<svg aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="19" cy="12" r="2"/></svg> <svg aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="19" cy="12" r="2"/></svg>
</button> </button>
<div v-if="actionsOpenId === conv.id" class="ai-conv-actions-menu"> <div v-if="actionsOpenId === conv.id" class="ai-conv-actions-menu">
@@ -115,7 +115,12 @@
<template v-for="group in groupedActive" :key="'g-' + group.key"> <template v-for="group in groupedActive" :key="'g-' + group.key">
<div <div
class="ai-conv-group-title ai-conv-group-title--fold" class="ai-conv-group-title ai-conv-group-title--fold"
role="button"
tabindex="0"
:aria-expanded="!store.state.foldedGroups[group.key]"
@click="store.toggleGroupFold(group.key)" @click="store.toggleGroupFold(group.key)"
@keydown.enter.prevent="store.toggleGroupFold(group.key)"
@keydown.space.prevent="store.toggleGroupFold(group.key)"
> >
<span class="ai-conv-group-arrow" :class="{ 'is-folded': store.state.foldedGroups[group.key] }">▸</span> <span class="ai-conv-group-arrow" :class="{ 'is-folded': store.state.foldedGroups[group.key] }">▸</span>
{{ group.label }} {{ group.label }}
@@ -174,7 +179,7 @@
</div> </div>
<div class="ai-conv-item-actions"> <div class="ai-conv-item-actions">
<div class="ai-conv-item-more"> <div class="ai-conv-item-more">
<button class="ai-conv-item-act" @click.stop="toggleActionsMenu(conv.id)" :title="t('aiChat.moreActions')"> <button class="ai-conv-item-act" @click.stop="toggleActionsMenu(conv.id)" :title="t('aiChat.moreActions')" :aria-label="t('aiChat.moreActions')">
<svg aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="19" cy="12" r="2"/></svg> <svg aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="19" cy="12" r="2"/></svg>
</button> </button>
<div v-if="actionsOpenId === conv.id" class="ai-conv-actions-menu"> <div v-if="actionsOpenId === conv.id" class="ai-conv-actions-menu">
@@ -228,7 +233,15 @@
<!-- 归档分组(可折叠) --> <!-- 归档分组(可折叠) -->
<template v-if="archivedConvs.length"> <template v-if="archivedConvs.length">
<div class="ai-conv-group-title ai-conv-group-title--fold" @click="store.toggleArchivedFold()"> <div
class="ai-conv-group-title ai-conv-group-title--fold"
role="button"
tabindex="0"
:aria-expanded="!store.state.archivedCollapsed"
@click="store.toggleArchivedFold()"
@keydown.enter.prevent="store.toggleArchivedFold()"
@keydown.space.prevent="store.toggleArchivedFold()"
>
<span class="ai-conv-group-arrow" :class="{ 'is-folded': store.state.archivedCollapsed }">▸</span> <span class="ai-conv-group-arrow" :class="{ 'is-folded': store.state.archivedCollapsed }">▸</span>
{{ t('aiChat.archivedCount', { n: archivedConvs.length }) }} {{ t('aiChat.archivedCount', { n: archivedConvs.length }) }}
</div> </div>
@@ -258,7 +271,7 @@
</div> </div>
<div class="ai-conv-item-actions"> <div class="ai-conv-item-actions">
<div class="ai-conv-item-more"> <div class="ai-conv-item-more">
<button class="ai-conv-item-act" @click.stop="toggleActionsMenu(conv.id)" :title="t('aiChat.moreActions')"> <button class="ai-conv-item-act" @click.stop="toggleActionsMenu(conv.id)" :title="t('aiChat.moreActions')" :aria-label="t('aiChat.moreActions')">
<svg aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="19" cy="12" r="2"/></svg> <svg aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="19" cy="12" r="2"/></svg>
</button> </button>
<div v-if="actionsOpenId === conv.id" class="ai-conv-actions-menu"> <div v-if="actionsOpenId === conv.id" class="ai-conv-actions-menu">
+2 -2
View File
@@ -1877,7 +1877,7 @@ defineExpose({
<!-- 回到底部(上滑看历史 / 有新内容时显示) --> <!-- 回到底部(上滑看历史 / 有新内容时显示) -->
<!-- R11(2026-10-05):生成中且非跟随态 → 按钮加脉冲点,提示「下面有新内容」而非仅导航 --> <!-- R11(2026-10-05):生成中且非跟随态 → 按钮加脉冲点,提示「下面有新内容」而非仅导航 -->
<Transition name="back-to-bottom"> <Transition name="back-to-bottom">
<button v-if="showBackToBottom" class="ai-back-to-bottom" :class="{ 'has-new': store.state.streaming && !isFollowingBottom }" @click="() => scrollToBottom()" :title="$t('aiChat.backToBottom')"> <button v-if="showBackToBottom" class="ai-back-to-bottom" :class="{ 'has-new': store.state.streaming && !isFollowingBottom }" @click="() => scrollToBottom()" :title="$t('aiChat.backToBottom')" :aria-label="$t('aiChat.backToBottom')">
<svg aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg> <svg aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg>
<span v-if="store.state.streaming && !isFollowingBottom" class="ai-btb-new-dot"></span> <span v-if="store.state.streaming && !isFollowingBottom" class="ai-btb-new-dot"></span>
</button> </button>
@@ -1897,7 +1897,7 @@ defineExpose({
<span v-if="agentPeekSubject.task" class="ai-agent-peek-task">{{ agentPeekSubject.task }}</span> <span v-if="agentPeekSubject.task" class="ai-agent-peek-task">{{ agentPeekSubject.task }}</span>
</div> </div>
</div> </div>
<button class="ai-agent-peek-close" :title="$t('common.close')" @click="closeAgentTrace"> <button class="ai-agent-peek-close" :title="$t('common.close')" :aria-label="$t('common.close')" @click="closeAgentTrace">
<svg aria-hidden="true" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg> <svg aria-hidden="true" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button> </button>
</div> </div>
+10 -9
View File
@@ -4,7 +4,7 @@
动作按钮经 emit 回父(父持有 clear/compress/confirm 逻辑 + 单一 toast 源)。 ═══ --> 动作按钮经 emit 回父(父持有 clear/compress/confirm 逻辑 + 单一 toast 源)。 ═══ -->
<div class="ai-header"> <div class="ai-header">
<div class="ai-header-left"> <div class="ai-header-left">
<button class="ai-btn-icon" @click="emit('toggle-sidebar')" :title="$t('aiChat.conversationList')"> <button class="ai-btn-icon" @click="emit('toggle-sidebar')" :title="$t('aiChat.conversationList')" :aria-label="$t('aiChat.conversationList')">
<svg aria-hidden="true" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg> <svg aria-hidden="true" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
</button> </button>
<span class="ai-header-icon"> <span class="ai-header-icon">
@@ -79,6 +79,7 @@
class="ai-btn-icon" class="ai-btn-icon"
:class="{ 'ai-btn-icon--active': moreMenuOpen }" :class="{ 'ai-btn-icon--active': moreMenuOpen }"
:title="$t('aiChat.moreActions')" :title="$t('aiChat.moreActions')"
:aria-label="$t('aiChat.moreActions')"
@click="moreMenuOpen = !moreMenuOpen" @click="moreMenuOpen = !moreMenuOpen"
> >
<svg aria-hidden="true" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="12" cy="19" r="1.5"/></svg> <svg aria-hidden="true" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="12" cy="19" r="1.5"/></svg>
@@ -118,14 +119,14 @@
<!-- 嵌入模式:最大化/还原 + 分离 + 关闭 --> <!-- 嵌入模式:最大化/还原 + 分离 + 关闭 -->
<template v-if="!detached"> <template v-if="!detached">
<button class="ai-btn-icon" @click="emit('toggle-maximize')" :title="store.state.maximized ? $t('aiChat.restoreSidebar') : $t('aiChat.maximize')"> <button class="ai-btn-icon" @click="emit('toggle-maximize')" :title="store.state.maximized ? $t('aiChat.restoreSidebar') : $t('aiChat.maximize')" :aria-label="store.state.maximized ? $t('aiChat.restoreSidebar') : $t('aiChat.maximize')">
<svg aria-hidden="true" v-if="store.state.maximized" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="3" x2="9" y2="21"/></svg> <svg aria-hidden="true" v-if="store.state.maximized" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="3" x2="9" y2="21"/></svg>
<svg aria-hidden="true" v-else width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 00-2 2v3m18 0V5a2 2 0 00-2-2h-3m0 18h3a2 2 0 002-2v-3M3 16v3a2 2 0 002 2h3"/></svg> <svg aria-hidden="true" v-else width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 00-2 2v3m18 0V5a2 2 0 00-2-2h-3m0 18h3a2 2 0 002-2v-3M3 16v3a2 2 0 002 2h3"/></svg>
</button> </button>
<button class="ai-btn-icon" @click="emit('detach')" :title="$t('aiChat.detachWindow')"> <button class="ai-btn-icon" @click="emit('detach')" :title="$t('aiChat.detachWindow')" :aria-label="$t('aiChat.detachWindow')">
<svg aria-hidden="true" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"/><path d="M9 21H3v-6"/><path d="M21 3l-7 7"/><path d="M3 21l7-7"/></svg> <svg aria-hidden="true" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"/><path d="M9 21H3v-6"/><path d="M21 3l-7 7"/><path d="M3 21l7-7"/></svg>
</button> </button>
<button class="ai-btn-icon" @click="emit('close-panel')" :title="$t('aiChat.closePanel')"> <button class="ai-btn-icon" @click="emit('close-panel')" :title="$t('aiChat.closePanel')" :aria-label="$t('aiChat.closePanel')">
<svg aria-hidden="true" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg> <svg aria-hidden="true" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button> </button>
</template> </template>
@@ -133,16 +134,16 @@
<template v-else> <template v-else>
<!-- V11b(2026-10-05):收回分离窗回主面板(emitReattachToMain → 主窗 reattachPanel), <!-- V11b(2026-10-05):收回分离窗回主面板(emitReattachToMain → 主窗 reattachPanel),
分离窗自关。与「关闭」区别:关闭仅关窗不恢复主面板,收回=分离→主面板合一 --> 分离窗自关。与「关闭」区别:关闭仅关窗不恢复主面板,收回=分离→主面板合一 -->
<button class="ai-btn-icon" @click="emit('reattach')" :title="$t('aiChat.reattachToMain')"> <button class="ai-btn-icon" @click="emit('reattach')" :title="$t('aiChat.reattachToMain')" :aria-label="$t('aiChat.reattachToMain')">
<svg aria-hidden="true" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/><path d="M9 12h6"/><path d="M12 9l0 6"/></svg> <svg aria-hidden="true" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/><path d="M9 12h6"/><path d="M12 9l0 6"/></svg>
</button> </button>
<button class="ai-btn-icon" :class="{ 'ai-btn-icon--active': store.state.docked }" @click="emit('dock')" :title="store.state.docked ? $t('aiChat.undock') : $t('aiChat.dockRight')"> <button class="ai-btn-icon" :class="{ 'ai-btn-icon--active': store.state.docked }" @click="emit('dock')" :title="store.state.docked ? $t('aiChat.undock') : $t('aiChat.dockRight')" :aria-label="store.state.docked ? $t('aiChat.undock') : $t('aiChat.dockRight')">
<svg aria-hidden="true" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="8" height="18" rx="1"/><rect x="14" y="3" width="8" height="18" rx="1"/></svg> <svg aria-hidden="true" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="8" height="18" rx="1"/><rect x="14" y="3" width="8" height="18" rx="1"/></svg>
</button> </button>
<button class="ai-btn-icon" :class="{ 'ai-btn-icon--active': alwaysOnTop }" @click="emit('toggle-always-on-top')" :title="alwaysOnTop ? $t('aiChat.unpin') : $t('aiChat.pinOnTop')"> <button class="ai-btn-icon" :class="{ 'ai-btn-icon--active': alwaysOnTop }" @click="emit('toggle-always-on-top')" :title="alwaysOnTop ? $t('aiChat.unpin') : $t('aiChat.pinOnTop')" :aria-label="alwaysOnTop ? $t('aiChat.unpin') : $t('aiChat.pinOnTop')">
<svg aria-hidden="true" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2C8 2 6 5 6 8v3l-2 2v1h16v-1l-2-2V8c0-3-2-6-6-6z"/><line x1="12" y1="14" x2="12" y2="22"/></svg> <svg aria-hidden="true" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2C8 2 6 5 6 8v3l-2 2v1h16v-1l-2-2V8c0-3-2-6-6-6z"/><line x1="12" y1="14" x2="12" y2="22"/></svg>
</button> </button>
<button class="ai-btn-icon" @click="emit('close-detached')" :title="$t('aiChat.closeWindow')"> <button class="ai-btn-icon" @click="emit('close-detached')" :title="$t('aiChat.closeWindow')" :aria-label="$t('aiChat.closeWindow')">
<svg aria-hidden="true" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg> <svg aria-hidden="true" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button> </button>
</template> </template>
@@ -246,7 +247,7 @@
</div> </div>
<!-- L3 完整上下文 --> <!-- L3 完整上下文 -->
<div v-if="contextInfo.goals.length || contextInfo.summaryCount > 0" class="ai-context-inline"> <div v-if="contextInfo.goals.length || contextInfo.summaryCount > 0" class="ai-context-inline">
<button type="button" class="ai-context-inline-badge ai-tool-badge" :class="{ 'is-active': contextExpanded }" @click="togglePanel('context')" :title="$t('aiChat.viewContext')"> <button type="button" class="ai-context-inline-badge ai-tool-badge" :class="{ 'is-active': contextExpanded }" @click="togglePanel('context')" :title="$t('aiChat.viewContext')" :aria-label="$t('aiChat.viewContext')">
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><circle cx="12" cy="8" r="1" fill="currentColor"/></svg> <svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><circle cx="12" cy="8" r="1" fill="currentColor"/></svg>
</button> </button>
<Transition name="ai-pop"> <Transition name="ai-pop">
@@ -5,7 +5,7 @@
<router-link to="/projects" class="df-link">{{ $t('dashboard.viewAll') }}</router-link> <router-link to="/projects" class="df-link">{{ $t('dashboard.viewAll') }}</router-link>
</div> </div>
<div class="project-list"> <div class="project-list">
<div v-for="(p, i) in displayProjects" :key="p.id" class="project-row clickable" :style="{ animationDelay: `${300 + i * 50}ms` }" @click="goDetail(p.id)" :title="t('dashboard.goProjectDetail')"> <div v-for="(p, i) in displayProjects" :key="p.id" class="project-row clickable" role="button" tabindex="0" :style="{ animationDelay: `${300 + i * 50}ms` }" @click="goDetail(p.id)" @keydown.enter.prevent="goDetail(p.id)" @keydown.space.prevent="goDetail(p.id)" :title="t('dashboard.goProjectDetail')">
<div class="project-row-top"> <div class="project-row-top">
<div class="project-identity"> <div class="project-identity">
<span class="project-dot" :class="'dot-' + p.stage"></span> <span class="project-dot" :class="'dot-' + p.stage"></span>
+1 -1
View File
@@ -19,7 +19,7 @@
</div> </div>
</div> </div>
<div class="idea-rows"> <div class="idea-rows">
<div v-for="idea in displayIdeas" :key="idea.id" class="idea-row clickable" @click="goDetail(idea.id)" :title="t('dashboard.goIdeaDetail')"> <div v-for="idea in displayIdeas" :key="idea.id" class="idea-row clickable" role="button" tabindex="0" @click="goDetail(idea.id)" @keydown.enter.prevent="goDetail(idea.id)" @keydown.space.prevent="goDetail(idea.id)" :title="t('dashboard.goIdeaDetail')">
<div class="idea-score-ring" :class="'ring-' + idea.tier"> <div class="idea-score-ring" :class="'ring-' + idea.tier">
<span class="idea-score-num">{{ idea.score }}</span> <span class="idea-score-num">{{ idea.score }}</span>
</div> </div>
+27 -3
View File
@@ -68,7 +68,15 @@
<!-- ② 溯源(P0 折叠:标题点击切换,默认展开)--> <!-- ② 溯源(P0 折叠:标题点击切换,默认展开)-->
<section class="detail-section"> <section class="detail-section">
<div class="section-title section-title-toggle" @click="sectionOpen.trace = !sectionOpen.trace"> <div
class="section-title section-title-toggle"
role="button"
tabindex="0"
:aria-expanded="sectionOpen.trace"
@click="sectionOpen.trace = !sectionOpen.trace"
@keydown.enter.prevent="sectionOpen.trace = !sectionOpen.trace"
@keydown.space.prevent="sectionOpen.trace = !sectionOpen.trace"
>
<span>{{ t('knowledge.traceTitle') }}</span> <span>{{ t('knowledge.traceTitle') }}</span>
<span class="section-chevron">{{ sectionOpen.trace ? '▾' : '▸' }}</span> <span class="section-chevron">{{ sectionOpen.trace ? '▾' : '▸' }}</span>
</div> </div>
@@ -103,7 +111,15 @@
<!-- ③ 引用(P0 折叠)--> <!-- ③ 引用(P0 折叠)-->
<section class="detail-section"> <section class="detail-section">
<div class="section-title section-title-toggle" @click="sectionOpen.refs = !sectionOpen.refs"> <div
class="section-title section-title-toggle"
role="button"
tabindex="0"
:aria-expanded="sectionOpen.refs"
@click="sectionOpen.refs = !sectionOpen.refs"
@keydown.enter.prevent="sectionOpen.refs = !sectionOpen.refs"
@keydown.space.prevent="sectionOpen.refs = !sectionOpen.refs"
>
<span>{{ t('knowledge.refTitle', { n: referenceEvents.length }) }}</span> <span>{{ t('knowledge.refTitle', { n: referenceEvents.length }) }}</span>
<span class="section-chevron">{{ sectionOpen.refs ? '▾' : '▸' }}</span> <span class="section-chevron">{{ sectionOpen.refs ? '▾' : '▸' }}</span>
</div> </div>
@@ -124,7 +140,15 @@
<!-- ④ 生命周期时间线(P0 折叠:默认收起,回顾性信息通常无需常驻)--> <!-- ④ 生命周期时间线(P0 折叠:默认收起,回顾性信息通常无需常驻)-->
<section class="detail-section"> <section class="detail-section">
<div class="section-title section-title-toggle" @click="sectionOpen.lifecycle = !sectionOpen.lifecycle"> <div
class="section-title section-title-toggle"
role="button"
tabindex="0"
:aria-expanded="sectionOpen.lifecycle"
@click="sectionOpen.lifecycle = !sectionOpen.lifecycle"
@keydown.enter.prevent="sectionOpen.lifecycle = !sectionOpen.lifecycle"
@keydown.space.prevent="sectionOpen.lifecycle = !sectionOpen.lifecycle"
>
<span>{{ t('knowledge.lifecycleTitle') }}</span> <span>{{ t('knowledge.lifecycleTitle') }}</span>
<span class="section-chevron">{{ sectionOpen.lifecycle ? '▾' : '▸' }}</span> <span class="section-chevron">{{ sectionOpen.lifecycle ? '▾' : '▸' }}</span>
</div> </div>
+29 -3
View File
@@ -20,13 +20,24 @@
:key="m.id" :key="m.id"
class="module-dropdown-item" class="module-dropdown-item"
:class="{ active: m.id === currentModuleId }" :class="{ active: m.id === currentModuleId }"
role="button"
tabindex="0"
@click="onModuleSelect(m.id)" @click="onModuleSelect(m.id)"
@keydown.enter.prevent="onModuleSelect(m.id)"
@keydown.space.prevent="onModuleSelect(m.id)"
> >
<span class="module-dropdown-name">{{ m.name }}</span> <span class="module-dropdown-name">{{ m.name }}</span>
<span v-if="m.path" class="module-dropdown-path" :title="m.path">{{ m.path }}</span> <span v-if="m.path" class="module-dropdown-path" :title="m.path">{{ m.path }}</span>
</div> </div>
<div class="module-dropdown-divider"></div> <div class="module-dropdown-divider"></div>
<div class="module-dropdown-item module-dropdown-action" @click="onScanSubmodules"> <div
class="module-dropdown-item module-dropdown-action"
role="button"
tabindex="0"
@click="onScanSubmodules"
@keydown.enter.prevent="onScanSubmodules"
@keydown.space.prevent="onScanSubmodules"
>
<span class="module-dropdown-name">🔍 {{ $t('fileExplorer.scanSubmodules') }}</span> <span class="module-dropdown-name">🔍 {{ $t('fileExplorer.scanSubmodules') }}</span>
</div> </div>
</div> </div>
@@ -34,12 +45,27 @@
<!-- 路径面包屑 --> <!-- 路径面包屑 -->
<nav class="breadcrumb"> <nav class="breadcrumb">
<span class="crumb crumb-root" :title="currentModule?.path ?? ''" @click="goRoot"> <span
class="crumb crumb-root"
:title="currentModule?.path ?? ''"
role="button"
tabindex="0"
@click="goRoot"
@keydown.enter.prevent="goRoot"
@keydown.space.prevent="goRoot"
>
{{ currentModule?.name ?? '...' }} {{ currentModule?.name ?? '...' }}
</span> </span>
<template v-for="(seg, idx) in breadcrumbSegments" :key="idx"> <template v-for="(seg, idx) in breadcrumbSegments" :key="idx">
<span class="crumb-sep">/</span> <span class="crumb-sep">/</span>
<span class="crumb" @click="goBreadcrumb(seg.path)">{{ seg.name }}</span> <span
class="crumb"
role="button"
tabindex="0"
@click="goBreadcrumb(seg.path)"
@keydown.enter.prevent="goBreadcrumb(seg.path)"
@keydown.space.prevent="goBreadcrumb(seg.path)"
>{{ seg.name }}</span>
</template> </template>
</nav> </nav>
</div> </div>