forked from admin/PlantAI-rag-new
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:
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 内,
|
||||
|
||||
@@ -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>
|
||||
@@ -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
@@ -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); }
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user