feat(ui): 导航按钮迁移右上角+删除会话栏底部工具栏

- 会话/扩展/知识图谱/设置 4 按钮自侧栏迁移至 TopBar 右侧(与主题按钮并列)
- 按需求收敛为扩展/设置两按钮,与主题按钮右上角同行水平对齐
  (.theme-toggle-anim 改 inline-flex + svg 脱离基线,消除图标下沉)
- SessionRail 全档位删除(桌面底部工具栏/折叠态图标轨/窄屏抽屉)
- 清理死接线:openKg 函数与 toggle-side/goto-kg 事件
- 同步删除 r3debug 的 SessionRail 测试用例

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 22:49:41 +08:00
co-authored by Claude Fable 5
parent e3870d19de
commit 957f3cb062
7 changed files with 36 additions and 141 deletions
+9 -2
View File
@@ -15,6 +15,7 @@ import TopBar from './TopBar.vue';
import StatusBar from './StatusBar.vue';
import Drawer from './Drawer.vue';
import SessionPanel from '../session/SessionPanel.vue';
import ExtensionsPane from '../session/ExtensionsPane.vue';
import SettingsPanel from '../settings/SettingsPanel.vue';
import ChatPanel from '../chat/ChatPanel.vue';
import BenchPanel from '../bench/BenchPanel.vue';
@@ -116,6 +117,7 @@ function onResize() {
const sideOpen = ref(false);
const benchOpen = ref(false);
const settingsOpen = ref(false);
const extensionsOpen = ref(false);
// 移动端工作台全屏覆盖层(FR-RESP-004)
const benchOverlayOpen = ref(false);
@@ -227,11 +229,13 @@ function overlaySelectTab(tab) {
@menu="openSide"
@bench="openBench"
@close-bench="closeAllBench"
@goto-extensions="extensionsOpen = true"
@goto-settings="settingsOpen = true"
/>
<!-- 左栏(≥1024px 固定;<1024px 抽屉形态) -->
<div v-if="!drawersMode" class="aside-host">
<SessionPanel @goto-settings="settingsOpen = true" />
<SessionPanel />
</div>
<!-- 左折叠把手(FR-SHELL-004;仅桌面档显示) -->
@@ -287,7 +291,7 @@ function overlaySelectTab(tab) {
<!-- <1024px 抽屉:左 = 会话抽屉,右 = 工作台抽屉 -->
<template v-if="drawersMode">
<Drawer :open="sideOpen" side="left" width="260px" title="会话列表" @close="closeSide">
<SessionPanel @goto-settings="settingsOpen = true" />
<SessionPanel />
</Drawer>
<Drawer :open="benchOpen" side="right" width="300px" title="工作台" @close="closeBench">
<BenchPanel />
@@ -297,6 +301,9 @@ function overlaySelectTab(tab) {
<!-- 设置:中央模态框(SettingsPanel 自带 Teleport 遮罩 + 高斯模糊,全档位可用) -->
<SettingsPanel :open="settingsOpen" @close="settingsOpen = false" />
<!-- 扩展管理(2026-09-15 自侧栏迁移至右上角;中央模态,与 SessionPanel 内同款组件) -->
<ExtensionsPane :open="extensionsOpen" @close="extensionsOpen = false" />
<!-- <768px 全屏工作台覆盖层(FR-RESP-004:顶部 tab 条 + 关闭按钮) -->
<Teleport to="body">
<div v-if="benchOverlayOpen && tier === 'mobile'" class="bench-overlay">
+18 -2
View File
@@ -10,7 +10,8 @@
bench.currentSources → 来源数组(工作台按钮角标数) -->
<script setup>
import { computed, onMounted, ref } from 'vue';
import { APP_NAME, STATUS_LABEL } from '../../i18n/labels.js';
import { APP_NAME, STATUS_LABEL, T } from '../../i18n/labels.js';
import { BF } from '../../i18n/bioflow.js';
import { useThemeStore } from '../../stores/theme.js';
const props = defineProps({
@@ -28,7 +29,7 @@ const props = defineProps({
benchOverlayOpen: { type: Boolean, default: false },
});
const emit = defineEmits(['menu', 'bench', 'close-bench']);
const emit = defineEmits(['menu', 'bench', 'close-bench', 'goto-extensions', 'goto-settings']);
/* ---------- 主题 ---------- */
const themeStore = useThemeStore();
@@ -143,6 +144,9 @@ const iconMoon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" str
const iconMenu = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M3 12h18M3 6h18M3 18h18"/></svg>';
const iconBench = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 5h12M6 12h12M6 19h12"/></svg>';
const iconClose = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>';
// 右上角快捷按钮图标(2026-09-15:会话/扩展/知识图谱/设置 自侧栏迁移)
const iconRailExt = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l2.4 4.9 5.4.8-3.9 3.8.9 5.4-4.8-2.5-4.8 2.5.9-5.4L4.2 7.7l5.4-.8z"/><path d="M20 14v5a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-5"/></svg>';
const iconRailSettings = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.7 1.7 0 0 0-1.87-.34 1.7 1.7 0 0 0-1 1.55V21a2 2 0 1 1-4 0v-.09a1.7 1.7 0 0 0-1-1.55 1.7 1.7 0 0 0-1.87.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.7 1.7 0 0 0 .34-1.87 1.7 1.7 0 0 0-1.55-1H3a2 2 0 1 1 0-4h.09a1.7 1.7 0 0 0 1.55-1 1.7 1.7 0 0 0-.34-1.87l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.7 1.7 0 0 0 1.87.34h0a1.7 1.7 0 0 0 1-1.55V3a2 2 0 1 1 4 0v.09a1.7 1.7 0 0 0 1 1.55h0a1.7 1.7 0 0 0 1.87-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.7 1.7 0 0 0-.34 1.87v0a1.7 1.7 0 0 0 1.55 1H21a2 2 0 1 1 0 4h-.09a1.7 1.7 0 0 0-1.55 1z"/></svg>';
</script>
<template>
@@ -193,6 +197,18 @@ const iconClose = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" st
<span v-if="!benchOverlayOpen && sourceCount != null" class="bench-btn-count">{{ sourceCount }}</span>
</button>
<!-- 右上角快捷按钮(2026-09-15 自侧栏迁移;仅 desktop/laptop;三按钮同行右对齐) -->
<template v-if="tier === 'desktop' || tier === 'laptop'">
<button class="icon-btn top-rail-btn" type="button" :title="BF.extRailTitle" :aria-label="BF.extRailTitle"
@click="$emit('goto-extensions')">
<span v-html="iconRailExt"></span>
</button>
<button class="icon-btn top-rail-btn" type="button" :title="T.railSettings" :aria-label="T.railSettings"
@click="$emit('goto-settings')">
<span v-html="iconRailSettings"></span>
</button>
</template>
<!-- 主题切换(日月 SVG + rotate 180° 400ms,FR-THEME-002) -->
<button
class="icon-btn"
+1 -19
View File
@@ -3,24 +3,17 @@
// 新建按钮 / 搜索框 / 分组列表(今天/昨天/更早)/ 状态点四态 / 活动渐变条 /
// 重命名双击 / 删除二次确认 / 空搜索提示(FR-SESSION-001~009)。
// 注意:左抽屉形态由 B1 布局层负责;本组件只渲染面板内容(session-panel 根元素)。
import { computed, onMounted, ref } from 'vue';
import { computed, onMounted } from 'vue';
import { useSessionsStore } from '../../stores/sessions.js';
import { useChatStore } from '../../stores/chat.js';
import { T, SESSION_GROUPS } from '../../i18n/labels.js';
import SearchBox from './SearchBox.vue';
import SessionItem from './SessionItem.vue';
import SessionRail from './SessionRail.vue';
// 用户自定义扩展分析:侧栏「扩展」入口(仅列表/历史/删除;创建在对话引导卡)
import ExtensionsPane from './ExtensionsPane.vue';
const emit = defineEmits(['open-rail', 'goto-kg', 'goto-settings']);
const sessions = useSessionsStore();
const chat = useChatStore();
const confirmDelete = computed(() => sessions._confirmDelete || null);
const extensionsOpen = ref(false);
onMounted(() => {
sessions.load().catch(() => {
/* 列表加载失败 → error 状态展示空态/离线态(失败必须可见,PRD §2.2) */
@@ -146,17 +139,6 @@ const doDelete = async () => {
</div>
<!-- 折叠后的图标轨(FR-SHELL-004 的 B2 侧内容) -->
<SessionRail
:active="true"
@open="$emit('open-rail')"
@goto-extensions="extensionsOpen = true"
@goto-kg="$emit('goto-kg')"
@goto-settings="$emit('goto-settings')"
/>
<!-- 用户自定义扩展分析侧栏管理(仅列表/历史/删除) -->
<ExtensionsPane :open="extensionsOpen" @close="extensionsOpen = false" />
<!-- 删除二次确认(FR-SESSION-007)
Teleport 到 body:SessionPanel 在平板档渲染于 B1 的 Drawer 内,
-89
View File
@@ -1,89 +0,0 @@
<script setup>
// session/SessionRail.vue —— 左栏折叠后的 64px 图标轨(FR-SHELL-004 的 B2 侧)
// 会话/扩展/图谱/设置四个 rail-item;会话项当前,带左侧渐变竖条。
// 「扩展」入口 = 用户自定义扩展分析侧栏管理(仅列表/历史/删除,创建在对话引导卡)。
import { BF } from '../../i18n/bioflow.js';
import { T } from '../../i18n/labels.js';
defineProps({
active: { type: Boolean, default: true },
});
const emit = defineEmits(['open', 'goto-extensions', 'goto-kg', 'goto-settings']);
</script>
<template>
<div class="collapsed-rail">
<button class="rail-item" :class="{ active }" :title="T.railSession" type="button" @click="emit('open')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg>
</button>
<button class="rail-item" :title="BF.extRailTitle" type="button" @click="emit('goto-extensions')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2l2.4 4.9 5.4.8-3.9 3.8.9 5.4-4.8-2.5-4.8 2.5.9-5.4L4.2 7.7l5.4-.8z" />
<path d="M20 14v5a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-5" />
</svg>
</button>
<button class="rail-item" :title="T.railKg" type="button" @click="emit('goto-kg')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" />
</svg>
</button>
<button class="rail-item" :title="T.railSettings" type="button" @click="emit('goto-settings')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="3" />
<path d="M19.4 15a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.7 1.7 0 0 0-1.87-.34 1.7 1.7 0 0 0-1 1.55V21a2 2 0 1 1-4 0v-.09a1.7 1.7 0 0 0-1-1.55 1.7 1.7 0 0 0-1.87.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.7 1.7 0 0 0 .34-1.87 1.7 1.7 0 0 0-1.55-1H3a2 2 0 1 1 0-4h.09a1.7 1.7 0 0 0 1.55-1 1.7 1.7 0 0 0-.34-1.87l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.7 1.7 0 0 0 1.87.34h0a1.7 1.7 0 0 0 1-1.55V3a2 2 0 1 1 4 0v.09a1.7 1.7 0 0 0 1 1.55h0a1.7 1.7 0 0 0 1.87-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.7 1.7 0 0 0-.34 1.87v0a1.7 1.7 0 0 0 1.55 1H21a2 2 0 1 1 0 4h-.09a1.7 1.7 0 0 0-1.55 1z" />
</svg>
</button>
</div>
</template>
<style scoped>
/* 兜底样式(类名按 UI_SPEC §3 / §7) */
.collapsed-rail {
display: flex;
flex-direction: row; /* 展开态:三个图标横向一排 */
align-items: center;
justify-content: center; /* 展开态:横向居中 */
gap: 14px;
width: 100%;
padding: 0 14px 14px;
margin-top: auto; /* 居底:与折叠态同一元素,切换时 Y 高度不变 */
}
.rail-item {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border-radius: var(--pa-r-md);
color: var(--pa-ink-muted);
position: relative;
transition: background 0.2s var(--pa-ease), color 0.2s var(--pa-ease);
cursor: pointer;
}
.rail-item:hover {
background: var(--pa-surface-2);
color: var(--pa-ink);
}
.rail-item svg {
width: 17px;
height: 17px;
}
.rail-item.active {
color: var(--pa-ink);
background: var(--pa-surface-2);
}
.rail-item.active::before {
content: '';
position: absolute;
left: -8px;
top: 8px;
bottom: 8px;
width: 2px;
background: var(--pa-gradient);
border-radius: 2px;
}
</style>
+5
View File
@@ -61,8 +61,13 @@
/* 主题图标切换 rotate 180°(400ms)——挂在 .theme-toggle-anim 上 */
.theme-toggle-anim {
display: inline-flex;
align-items: center;
justify-content: center;
transition: transform .4s var(--pa-ease);
}
/* svg 脱离行内基线,避免主题图标较其他按钮图标下沉(2026-09-15 对齐修复) */
.theme-toggle-anim svg { display: block; }
.theme-toggle-anim.flipped { transform: rotate(180deg); }
/* 面板折叠 .3s ease-out:app-shell 的 grid-template-columns 过渡在 base.css .app-shell 上声明 */
+2 -19
View File
@@ -119,6 +119,8 @@ html.drawer-open { overflow: hidden; }
}
.icon-btn svg { width: 16px; height: 16px; }
.icon-btn:disabled { opacity: .45; cursor: not-allowed; }
.top-rail-btn { margin-left: 2px; }
.top-rail-btn > span { display: flex; align-items: center; justify-content: center; }
/* 状态徽章(pill,三编码:颜色+图标+文字,UI_SPEC §4.1 / PRD §2.1) */
.badge {
@@ -175,23 +177,6 @@ html.drawer-open { overflow: hidden; }
}
.session-panel .inner { display: flex; flex-direction: column; min-height: 0; width: 260px; flex: 1; transition: opacity .2s var(--pa-ease); }
.side-collapsed .session-panel .inner { opacity: 0; pointer-events: none; }
.side-collapsed .session-panel .collapsed-rail {
flex-direction: column; /* 折叠态:64px 窄轨纵向排布 */
align-items: center; /* 折叠态:轨内居中,图标原样 */
padding: 0 0 14px; /* 折叠态:去掉展开态的水平 14px(底 14px 两态一致) */
}
.rail-item {
display: inline-flex; align-items: center; justify-content: center;
width: 34px; height: 34px; border-radius: var(--pa-r-md);
color: var(--pa-ink-muted); position: relative;
transition: background .2s var(--pa-ease), color .2s var(--pa-ease);
}
.rail-item svg { width: 17px; height: 17px; }
.rail-item.active { color: var(--pa-ink); background: var(--pa-surface-2); }
.rail-item.active::before {
content: ""; position: absolute; left: -8px; top: 8px; bottom: 8px; width: 2px;
background: var(--pa-gradient); border-radius: 2px;
}
.session-panel .pad { padding: 14px 14px 10px; display: flex; flex-direction: column; gap: 10px; }
.new-task-btn {
@@ -798,7 +783,6 @@ html.drawer-open { overflow: hidden; }
============================================================ */
@media (hover: hover) {
.icon-btn:hover:not(:disabled) { color: var(--pa-ink); background: var(--pa-surface-2); }
.rail-item:hover { background: var(--pa-surface-2); color: var(--pa-ink); }
.new-task-btn:hover {
box-shadow: 0 0 24px rgba(0,212,255, calc(.4 * var(--pa-glow-opacity)));
transform: translateY(-1px);
@@ -833,7 +817,6 @@ html.drawer-open { overflow: hidden; }
.msg.assistant::before { background: var(--pa-gradient); }
.ft-dl { opacity: 1; }
.session-item:active { background: var(--pa-surface-2); }
.rail-item:active { background: var(--pa-surface-2); color: var(--pa-ink); }
.icon-btn:active:not(:disabled) { color: var(--pa-ink); background: var(--pa-surface-2); }
.chip:active { color: var(--pa-ink); }
.mode-pill:active { color: var(--pa-ink); background: var(--pa-surface-2); }
+1 -10
View File
@@ -3,7 +3,7 @@
// 1) §4 事件帧 → 块映射(applyExtensionFrame 全事件集幂等/追加/终态纪律)
// 2) sendExtension 流(用户消息落库 → 骨架 → SSE 帧分派 → 终态收口/错误标注)
// 3) uploadExtensionFile / trialRunExtension / confirmExtensionRun store 动作
// 4) 卡片组件 mount 冒烟(8 个扩展卡 + SessionRail 扩展入口)
// 4) 卡片组件 mount 冒烟(8 个扩展卡)
// 5) intent.js 创建意图词典(§5 A 层同表双跑)
//
// 运行方式(worktree 内):cd ui && npx vitest run --config tests/r3debug/vitest.config.js tests/r3debug/extensions.test.js
@@ -97,7 +97,6 @@ import ExtensionPublishCard from '../../src/components/chat/ExtensionPublishCard
import ExtensionCandidatesCard from '../../src/components/chat/ExtensionCandidatesCard.vue';
import ExtensionConfirmCard from '../../src/components/chat/ExtensionConfirmCard.vue';
import ExtensionRunCard from '../../src/components/chat/ExtensionRunCard.vue';
import SessionRail from '../../src/components/session/SessionRail.vue';
/** 构造一个最小 SSE Response(供 bioflowChatStream mock 返回) */
function makeSseResponse(frames) {
@@ -631,12 +630,4 @@ describe('卡片组件 mount 冒烟', () => {
expect(w.text()).toContain('out/x');
});
it('SessionRail 渲染:扩展入口 emits goto-extensions', async () => {
const w = mount(SessionRail, { props: { active: true } });
const items = w.findAll('.rail-item');
expect(items.length).toBe(4);
expect(items[1].attributes('title')).toContain('扩展');
await items[1].trigger('click');
expect(w.emitted('goto-extensions')).toBeTruthy();
});
});