优化: 无障碍第二批(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:
+1
-1
@@ -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 项新发现)
|
||||||
|
|||||||
@@ -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' ? '语义' : '文件' }}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user