feat(chat): 快捷导航三态自适应布局与快捷窗口手动收起
- 左侧输入跳转导航支持钉死/居中/隐藏三态:随轨宽空白自适应, 波浪宽度按可用空白比例压缩避免遮挡消息;滚动位置同步高亮当前阅读轮次 - QuickDock 新增悬浮展开/收起按钮,userCollapsed 与无内容自动收起相互独立 - 轨宽/空白经 ResizeObserver 实测校准,兼容 macOS 经典滚动条宽度差异
This commit is contained in:
parent
eaa312acd4
commit
8741edec18
@ -356,6 +356,28 @@
|
|||||||
v-if="!isMobileViewport"
|
v-if="!isMobileViewport"
|
||||||
:host-mode="versioningHostMode"
|
:host-mode="versioningHostMode"
|
||||||
/>
|
/>
|
||||||
|
<!-- 快捷窗口展开/收起按钮:零宽锚点始终贴在 QuickDock 右缘,
|
||||||
|
按钮绝对定位悬浮在原处,不随面板挤压移动 -->
|
||||||
|
<div
|
||||||
|
v-if="!isMobileViewport && qdHasContent"
|
||||||
|
class="qd-toggle-anchor"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="qd-toggle"
|
||||||
|
:class="{ 'is-active': qdExpanded }"
|
||||||
|
:aria-label="qdExpanded ? '收起快捷窗口' : '展开快捷窗口'"
|
||||||
|
:aria-pressed="qdExpanded"
|
||||||
|
@click="qdUserCollapsed = !qdUserCollapsed"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 18 18" width="16" height="16" aria-hidden="true">
|
||||||
|
<line x1="2" y1="3.5" x2="16" y2="3.5" />
|
||||||
|
<line x1="2" y1="7.5" x2="10" y2="7.5" />
|
||||||
|
<line x1="2" y1="11.5" x2="14" y2="11.5" />
|
||||||
|
<line x1="2" y1="15.5" x2="7" y2="15.5" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<FilePreviewPanel
|
<FilePreviewPanel
|
||||||
v-if="!isMobileViewport"
|
v-if="!isMobileViewport"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import { usePolicyStore } from '../stores/policy';
|
|||||||
import { useSubAgentStore } from '../stores/subAgent';
|
import { useSubAgentStore } from '../stores/subAgent';
|
||||||
import { useBackgroundCommandStore } from '../stores/backgroundCommand';
|
import { useBackgroundCommandStore } from '../stores/backgroundCommand';
|
||||||
import { usePersonalizationStore } from '../stores/personalization';
|
import { usePersonalizationStore } from '../stores/personalization';
|
||||||
|
import { useQuickDockStore } from '../stores/quickDock';
|
||||||
import { getToolStatusText, getToolDescription } from '../utils/chatDisplay';
|
import { getToolStatusText, getToolDescription } from '../utils/chatDisplay';
|
||||||
import { toolFaceKey } from '../utils/avatarFace';
|
import { toolFaceKey } from '../utils/avatarFace';
|
||||||
|
|
||||||
@ -65,6 +66,13 @@ export const computed = {
|
|||||||
'reasoningEffort'
|
'reasoningEffort'
|
||||||
]),
|
]),
|
||||||
...mapState(useFileStore, ['contextMenu', 'fileTree', 'expandedFolders', 'todoList']),
|
...mapState(useFileStore, ['contextMenu', 'fileTree', 'expandedFolders', 'todoList']),
|
||||||
|
...mapState(useQuickDockStore, {
|
||||||
|
qdHasContent: 'hasContent',
|
||||||
|
qdExpanded: 'expanded'
|
||||||
|
}),
|
||||||
|
...mapWritableState(useQuickDockStore, {
|
||||||
|
qdUserCollapsed: 'userCollapsed'
|
||||||
|
}),
|
||||||
...mapWritableState(useUiStore, [
|
...mapWritableState(useUiStore, [
|
||||||
'sidebarCollapsed',
|
'sidebarCollapsed',
|
||||||
'chatDisplayMode',
|
'chatDisplayMode',
|
||||||
|
|||||||
@ -681,9 +681,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 左侧用户输入快捷跳转导航:每个真实用户输入一根横线,hover 波浪 + 预览,点击跳转 -->
|
<!-- 左侧用户输入快捷跳转导航:每个真实用户输入一根横线,hover 波浪 + 预览,点击跳转 -->
|
||||||
<nav
|
<nav
|
||||||
v-if="quickNavItems.length > 0"
|
v-if="quickNavItems.length > 0 && quickNavMode !== 'hidden'"
|
||||||
class="chat-quick-nav"
|
class="chat-quick-nav"
|
||||||
:style="{ '--qn-count': String(quickNavItems.length) }"
|
:class="{ 'chat-quick-nav--center': quickNavMode === 'center' }"
|
||||||
|
:style="quickNavNavStyle"
|
||||||
aria-label="用户输入快捷跳转"
|
aria-label="用户输入快捷跳转"
|
||||||
@mousemove="onQuickNavMouseMove"
|
@mousemove="onQuickNavMouseMove"
|
||||||
@mouseleave="onQuickNavMouseLeave"
|
@mouseleave="onQuickNavMouseLeave"
|
||||||
@ -692,7 +693,7 @@
|
|||||||
v-for="(item, i) in quickNavItems"
|
v-for="(item, i) in quickNavItems"
|
||||||
:key="item.msgIndex"
|
:key="item.msgIndex"
|
||||||
class="qn-line"
|
class="qn-line"
|
||||||
:class="{ 'is-hot': quickNavActiveIndex === i }"
|
:class="{ 'is-hot': quickNavActiveIndex === i, 'is-current': quickNavCurrentIndex === i }"
|
||||||
:style="{ width: quickNavLineWidth(i) + 'px' }"
|
:style="{ width: quickNavLineWidth(i) + 'px' }"
|
||||||
:ref="(el) => registerQuickNavLine(i, el)"
|
:ref="(el) => registerQuickNavLine(i, el)"
|
||||||
@click="onQuickNavLineClick(item)"
|
@click="onQuickNavLineClick(item)"
|
||||||
@ -1346,6 +1347,9 @@ function attachBounceListener() {
|
|||||||
lastObservedTop = top;
|
lastObservedTop = top;
|
||||||
lastObservedHeight = height;
|
lastObservedHeight = height;
|
||||||
|
|
||||||
|
// 左侧快捷导航:滚动位置同步(直接 DOM 监听,用户滚动与程序平滑滚动都会触发)
|
||||||
|
updateQuickNavCurrent(top);
|
||||||
|
|
||||||
if (heightDelta !== 0) {
|
if (heightDelta !== 0) {
|
||||||
bounceTraceLog(
|
bounceTraceLog(
|
||||||
'height-change',
|
'height-change',
|
||||||
@ -2058,6 +2062,7 @@ onMounted(() => {
|
|||||||
timerHandle = window.setInterval(() => {
|
timerHandle = window.setInterval(() => {
|
||||||
nowMs.value = Date.now();
|
nowMs.value = Date.now();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
setupQuickNavLayoutObserver();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(scrollRef, () => {
|
watch(scrollRef, () => {
|
||||||
@ -2091,6 +2096,7 @@ onBeforeUnmount(() => {
|
|||||||
clearInterval(timerHandle);
|
clearInterval(timerHandle);
|
||||||
timerHandle = null;
|
timerHandle = null;
|
||||||
}
|
}
|
||||||
|
teardownQuickNavLayoutObserver();
|
||||||
});
|
});
|
||||||
|
|
||||||
const isStackable = (action: any) =>
|
const isStackable = (action: any) =>
|
||||||
@ -2246,8 +2252,114 @@ function quickNavLineWidth(index: number): number {
|
|||||||
return QUICK_NAV_LINE_BASE_WIDTH;
|
return QUICK_NAV_LINE_BASE_WIDTH;
|
||||||
}
|
}
|
||||||
const dist = Math.abs(index - active);
|
const dist = Math.abs(index - active);
|
||||||
return QUICK_NAV_LINE_WIDTHS[dist] ?? QUICK_NAV_LINE_BASE_WIDTH;
|
const raw = QUICK_NAV_LINE_WIDTHS[dist] ?? QUICK_NAV_LINE_BASE_WIDTH;
|
||||||
|
if (raw === QUICK_NAV_LINE_BASE_WIDTH) {
|
||||||
|
return raw;
|
||||||
}
|
}
|
||||||
|
// 接近内容轨/边界时按比例压缩波浪宽度,避免盖到消息
|
||||||
|
const scale = quickNavWaveScale.value;
|
||||||
|
return Math.max(QUICK_NAV_LINE_BASE_WIDTH, Math.round(raw * scale));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 滚动位置同步:高亮当前阅读轮次对应的横线 =====
|
||||||
|
// 触发源:attachBounceListener 里已有的 DOM scroll 监听(virtua 的 onScroll 事件转发在该版本不触发)
|
||||||
|
const quickNavCurrentIndex = ref(-1);
|
||||||
|
|
||||||
|
function updateQuickNavCurrent(offset?: number) {
|
||||||
|
const items = quickNavItems.value;
|
||||||
|
if (!items.length) {
|
||||||
|
quickNavCurrentIndex.value = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const v = virtualizerRef.value;
|
||||||
|
if (!v) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const scrollOffset = typeof offset === 'number' ? offset : Number(v.scrollOffset) || 0;
|
||||||
|
const viewport = Number(v.viewportSize) || shellRef.value?.clientHeight || 0;
|
||||||
|
// 视口 25% 位置作为「正在阅读」的判定线
|
||||||
|
const mark = scrollOffset + viewport * 0.25;
|
||||||
|
const itemIdx = v.findItemIndex(mark);
|
||||||
|
// 找 ≤ itemIdx 的最近真实用户输入对应的导航项下标(msgIndex 递增)
|
||||||
|
let k = -1;
|
||||||
|
for (let i = 0; i < items.length; i += 1) {
|
||||||
|
if (items[i].msgIndex <= itemIdx) {
|
||||||
|
k = i;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
quickNavCurrentIndex.value = k < 0 ? 0 : k;
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(quickNavItems, () => {
|
||||||
|
updateQuickNavCurrent();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== 横线位置自适应:钉死(向右延伸)/ 居中(两侧延伸)/ 隐藏 =====
|
||||||
|
// 与 _chat-area.scss 轨宽公式对应:rail = min(900px, 100% - 57px),流式区间空白固定 ≈ 48.7px。
|
||||||
|
// 判定:空白达到最小值 ⇔ 内容轨已低于上限(轨宽满 → 钉死;轨宽未满 → 空白正中);空白太短 → 隐藏。
|
||||||
|
const QUICK_NAV_RAIL_MAX = 900;
|
||||||
|
const QUICK_NAV_GAP_HIDE = 36; // 空白 < 36px 时隐藏(移动端轨宽 100% 时自然命中)
|
||||||
|
// 临界固定空白 = 滚动区 padding 24 + rail 公式固定余量 (57-7.52)/2 + 实测滚动条宽/2
|
||||||
|
// (57/7.52 见 _chat-area.scss 的 --chat-rail-fluid / --chat-message-rail-width)。
|
||||||
|
// 钉死线中心 = 临界空白的一半:钉死→居中切换瞬间线中心重合,无瞬移。
|
||||||
|
// 注意不能写死:macOS 经典滚动条(接鼠标)会让空白多 4px,写死会右跳 2px。
|
||||||
|
const QUICK_NAV_GAP_CRITICAL_BASE = 48.74;
|
||||||
|
const QUICK_NAV_LINE_MAX_WIDTH = 34;
|
||||||
|
|
||||||
|
const quickNavGap = ref(100); // 初始给足,挂载后由 ResizeObserver 校准
|
||||||
|
const quickNavRailAtMax = ref(true);
|
||||||
|
const quickNavPinCenter = ref(QUICK_NAV_GAP_CRITICAL_BASE / 2);
|
||||||
|
|
||||||
|
const quickNavMode = computed<'pinned' | 'center' | 'hidden'>(() => {
|
||||||
|
if (quickNavGap.value < QUICK_NAV_GAP_HIDE) {
|
||||||
|
return 'hidden';
|
||||||
|
}
|
||||||
|
return quickNavRailAtMax.value ? 'pinned' : 'center';
|
||||||
|
});
|
||||||
|
|
||||||
|
const quickNavWaveScale = computed(() => {
|
||||||
|
const gap = quickNavGap.value;
|
||||||
|
if (quickNavMode.value === 'pinned') {
|
||||||
|
const pinLeft = quickNavPinCenter.value - QUICK_NAV_LINE_BASE_WIDTH / 2;
|
||||||
|
return Math.max(0, Math.min(1, (gap - pinLeft - 6) / QUICK_NAV_LINE_MAX_WIDTH));
|
||||||
|
}
|
||||||
|
if (quickNavMode.value === 'center') {
|
||||||
|
return Math.max(0, Math.min(1, (gap - 6) / QUICK_NAV_LINE_MAX_WIDTH));
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
const quickNavNavStyle = computed<Record<string, string>>(() => {
|
||||||
|
const base: Record<string, string> = { '--qn-count': String(quickNavItems.value.length) };
|
||||||
|
if (quickNavMode.value === 'center') {
|
||||||
|
// 空白内居中:切换瞬间空白 = 临界值,线中心与钉死位置重合,无瞬移
|
||||||
|
return { ...base, left: '0px', width: `${quickNavGap.value}px` };
|
||||||
|
}
|
||||||
|
// 钉死:位置与窗口宽度无关,左不动右变长
|
||||||
|
const pinLeft = quickNavPinCenter.value - QUICK_NAV_LINE_BASE_WIDTH / 2;
|
||||||
|
return { ...base, left: `${pinLeft}px`, width: `${QUICK_NAV_LINE_MAX_WIDTH}px` };
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateQuickNavLayout() {
|
||||||
|
const shell = shellRef.value;
|
||||||
|
const rail = contentRef.value;
|
||||||
|
if (!shell || !rail) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const shellRect = shell.getBoundingClientRect();
|
||||||
|
const railRect = rail.getBoundingClientRect();
|
||||||
|
// 实测滚动条宽度(与 updateChatScrollbarWidth 同口径):经典滚动条会增大临界空白,
|
||||||
|
// 钉死位置必须随之调整,否则钉死/居中切换瞬间线中心不重合
|
||||||
|
const scroller = scrollRef.value;
|
||||||
|
const sb = scroller ? Math.max(0, scroller.offsetWidth - scroller.clientWidth) : 0;
|
||||||
|
quickNavPinCenter.value = (QUICK_NAV_GAP_CRITICAL_BASE + sb / 2) / 2;
|
||||||
|
quickNavGap.value = railRect.left - shellRect.left;
|
||||||
|
quickNavRailAtMax.value = railRect.width >= QUICK_NAV_RAIL_MAX - 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
let quickNavLayoutObserver: ResizeObserver | null = null;
|
||||||
|
|
||||||
function registerQuickNavLine(index: number, el: any) {
|
function registerQuickNavLine(index: number, el: any) {
|
||||||
if (el) {
|
if (el) {
|
||||||
@ -2275,6 +2387,28 @@ function onQuickNavMouseLeave() {
|
|||||||
quickNavActiveIndex.value = -1;
|
quickNavActiveIndex.value = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupQuickNavLayoutObserver() {
|
||||||
|
if (typeof ResizeObserver === 'undefined' || !shellRef.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
quickNavLayoutObserver = new ResizeObserver(() => {
|
||||||
|
updateQuickNavLayout();
|
||||||
|
});
|
||||||
|
quickNavLayoutObserver.observe(shellRef.value);
|
||||||
|
// 轨宽随滚动条出现/消失也会变化(不改变 shell 尺寸),需要一并观察
|
||||||
|
if (contentRef.value) {
|
||||||
|
quickNavLayoutObserver.observe(contentRef.value);
|
||||||
|
}
|
||||||
|
updateQuickNavLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
function teardownQuickNavLayoutObserver() {
|
||||||
|
if (quickNavLayoutObserver) {
|
||||||
|
quickNavLayoutObserver.disconnect();
|
||||||
|
quickNavLayoutObserver = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onQuickNavLineClick(item: any) {
|
function onQuickNavLineClick(item: any) {
|
||||||
virtualizerRef.value?.scrollToIndex?.(item.msgIndex, { align: 'start', smooth: true });
|
virtualizerRef.value?.scrollToIndex?.(item.msgIndex, { align: 'start', smooth: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<aside class="quick-dock" :class="{ 'quick-dock--empty': !hasContent }">
|
<aside
|
||||||
|
class="quick-dock"
|
||||||
|
:class="{ 'quick-dock--empty': !hasContent || userCollapsed }"
|
||||||
|
>
|
||||||
<div ref="scrollRef" class="quick-dock__scroll" @scroll.passive="handleStackScroll">
|
<div ref="scrollRef" class="quick-dock__scroll" @scroll.passive="handleStackScroll">
|
||||||
<TodoWindow />
|
<TodoWindow />
|
||||||
<RunnerWindow kind="agent" />
|
<RunnerWindow kind="agent" />
|
||||||
@ -39,7 +42,6 @@ import { useQuickDockStore } from '@/stores/quickDock';
|
|||||||
import { useSubAgentStore } from '@/stores/subAgent';
|
import { useSubAgentStore } from '@/stores/subAgent';
|
||||||
import { useBackgroundCommandStore } from '@/stores/backgroundCommand';
|
import { useBackgroundCommandStore } from '@/stores/backgroundCommand';
|
||||||
import { useConversationStore } from '@/stores/conversation';
|
import { useConversationStore } from '@/stores/conversation';
|
||||||
import { useFileStore } from '@/stores/file';
|
|
||||||
import { useUiStore } from '@/stores/ui';
|
import { useUiStore } from '@/stores/ui';
|
||||||
import TodoWindow from './TodoWindow.vue';
|
import TodoWindow from './TodoWindow.vue';
|
||||||
import RunnerWindow from './RunnerWindow.vue';
|
import RunnerWindow from './RunnerWindow.vue';
|
||||||
@ -58,21 +60,24 @@ const quickDock = useQuickDockStore();
|
|||||||
const subAgentStore = useSubAgentStore();
|
const subAgentStore = useSubAgentStore();
|
||||||
const bgStore = useBackgroundCommandStore();
|
const bgStore = useBackgroundCommandStore();
|
||||||
const conversationStore = useConversationStore();
|
const conversationStore = useConversationStore();
|
||||||
const fileStore = useFileStore();
|
|
||||||
const uiStore = useUiStore();
|
const uiStore = useUiStore();
|
||||||
|
|
||||||
const { menu } = storeToRefs(quickDock);
|
// hasContent / userCollapsed 提升到 store:App.vue 的展开收起按钮共用同一判定
|
||||||
|
const { menu, hasContent, userCollapsed } = storeToRefs(quickDock);
|
||||||
const scrollRef = ref<HTMLElement | null>(null);
|
const scrollRef = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
/** 四个窗口任一有内容时才占位显示(空列不挤压对话区) */
|
// 手动收起时关掉所有瞬态面板(详情/预览/菜单),避免固定定位的详情面板悬空
|
||||||
const hasContent = computed(() => {
|
watch(userCollapsed, (collapsed) => {
|
||||||
const todoCount = fileStore.todoList?.tasks?.length || 0;
|
if (collapsed) {
|
||||||
return (
|
quickDock.resetTransient();
|
||||||
todoCount > 0 ||
|
}
|
||||||
subAgentStore.subAgents.length > 0 ||
|
});
|
||||||
bgStore.commands.length > 0 ||
|
|
||||||
quickDock.editedFiles.length > 0
|
// 内容清空后重置手动标记:下次有新内容时恢复自动展开
|
||||||
);
|
watch(hasContent, (has) => {
|
||||||
|
if (!has) {
|
||||||
|
quickDock.userCollapsed = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const AGENT_TERMINAL = new Set(['completed', 'failed', 'timeout', 'terminated']);
|
const AGENT_TERMINAL = new Set(['completed', 'failed', 'timeout', 'terminated']);
|
||||||
|
|||||||
@ -18,7 +18,8 @@
|
|||||||
.quick-dock {
|
.quick-dock {
|
||||||
flex: none;
|
flex: none;
|
||||||
width: 294px; /* 270 + 左右 12 padding */
|
width: 294px; /* 270 + 左右 12 padding */
|
||||||
padding: 20px 12px 20px 0;
|
/* top 44px:避开右上角展开/收起悬浮按钮(top 8 + 高 28 + 8 间距) */
|
||||||
|
padding: 44px 12px 20px 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -953,3 +954,49 @@
|
|||||||
color: var(--text-tertiary);
|
color: var(--text-tertiary);
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------------- 展开/收起悬浮按钮(App.vue 零宽锚点) ---------------- */
|
||||||
|
|
||||||
|
/* 零宽锚点:flex 兄弟节点,始终贴在 QuickDock 右缘;
|
||||||
|
dock 收起时 = main 右缘(落在对话标题栏右端),展开时 = dock 右缘(悬浮面板上方) */
|
||||||
|
.qd-toggle-anchor {
|
||||||
|
flex: none;
|
||||||
|
width: 0;
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
|
z-index: 70;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qd-toggle {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 16px;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
/* 底色与背后面板一致(收起=对话标题栏 / 展开=应用背景,两者同为 --surface-panel) */
|
||||||
|
background: var(--surface-panel);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 140ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qd-toggle svg line {
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-width: 1.8;
|
||||||
|
stroke-linecap: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* hover 只变图标颜色,不变底色 */
|
||||||
|
.qd-toggle:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qd-toggle.is-active {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
|
import { useFileStore } from './file';
|
||||||
|
import { useSubAgentStore } from './subAgent';
|
||||||
|
import { useBackgroundCommandStore } from './backgroundCommand';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 快捷窗口(Quick Dock)状态
|
* 快捷窗口(Quick Dock)状态
|
||||||
@ -38,6 +41,8 @@ interface QuickDockState {
|
|||||||
detail: QuickDockDetailTarget | null;
|
detail: QuickDockDetailTarget | null;
|
||||||
previewPath: string | null;
|
previewPath: string | null;
|
||||||
menu: QuickDockMenuState | null;
|
menu: QuickDockMenuState | null;
|
||||||
|
/** 用户通过顶部悬浮按钮手动收起(与「无内容自动收起」相互独立) */
|
||||||
|
userCollapsed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useQuickDockStore = defineStore('quickDock', {
|
export const useQuickDockStore = defineStore('quickDock', {
|
||||||
@ -46,8 +51,28 @@ export const useQuickDockStore = defineStore('quickDock', {
|
|||||||
editedFilesLive: false,
|
editedFilesLive: false,
|
||||||
detail: null,
|
detail: null,
|
||||||
previewPath: null,
|
previewPath: null,
|
||||||
menu: null
|
menu: null,
|
||||||
|
userCollapsed: false
|
||||||
}),
|
}),
|
||||||
|
getters: {
|
||||||
|
/** 四个窗口(待办/子智能体/后台指令/文件记录)任一有内容 */
|
||||||
|
hasContent(state): boolean {
|
||||||
|
const fileStore = useFileStore();
|
||||||
|
const subAgentStore = useSubAgentStore();
|
||||||
|
const bgStore = useBackgroundCommandStore();
|
||||||
|
const todoCount = fileStore.todoList?.tasks?.length || 0;
|
||||||
|
return (
|
||||||
|
todoCount > 0 ||
|
||||||
|
subAgentStore.subAgents.length > 0 ||
|
||||||
|
bgStore.commands.length > 0 ||
|
||||||
|
state.editedFiles.length > 0
|
||||||
|
);
|
||||||
|
},
|
||||||
|
/** 实际处于展开态:有内容且未被用户手动收起 */
|
||||||
|
expanded(state): boolean {
|
||||||
|
return this.hasContent && !state.userCollapsed;
|
||||||
|
}
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
setEditedFiles(list: EditedFileEntry[] | null | undefined, live = false) {
|
setEditedFiles(list: EditedFileEntry[] | null | undefined, live = false) {
|
||||||
this.editedFilesLive = live;
|
this.editedFilesLive = live;
|
||||||
|
|||||||
@ -2,7 +2,9 @@
|
|||||||
.chat-container {
|
.chat-container {
|
||||||
--chat-surface-color: var(--surface-panel);
|
--chat-surface-color: var(--surface-panel);
|
||||||
--chat-rail-max: 900px;
|
--chat-rail-max: 900px;
|
||||||
--chat-rail-fluid: 94%;
|
/* 流式宽度:两侧空白固定(消息侧 ≈49px / 输入栏侧 ≈53px),窗口变窄只压缩轨宽;
|
||||||
|
57px 的取值与旧 94% 公式在轨宽 900px 临界点处连续,移动端仍覆盖为 100% */
|
||||||
|
--chat-rail-fluid: calc(100% - 57px);
|
||||||
--chat-scrollbar-width: 8px;
|
--chat-scrollbar-width: 8px;
|
||||||
--chat-rail-width: min(var(--chat-rail-max), var(--chat-rail-fluid));
|
--chat-rail-width: min(var(--chat-rail-max), var(--chat-rail-fluid));
|
||||||
--chat-message-rail-width: min(
|
--chat-message-rail-width: min(
|
||||||
@ -28,7 +30,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0 0 auto 0;
|
inset: 0 0 auto 0;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
padding: 8px 16px;
|
padding: 8px 52px 8px 16px; /* 右侧为快捷窗口展开/收起悬浮按钮预留位置 */
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -105,17 +107,22 @@
|
|||||||
/* 左侧用户输入快捷跳转导航 */
|
/* 左侧用户输入快捷跳转导航 */
|
||||||
.chat-quick-nav {
|
.chat-quick-nav {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 18px;
|
left: 0; /* left/width 由 JS 按模式(钉死/居中)驱动 */
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 10px 8px;
|
padding: 10px 0;
|
||||||
height: min(62vh, calc(var(--qn-count, 1) * 9px + 20px));
|
height: min(62vh, calc(var(--qn-count, 1) * 9px + 20px));
|
||||||
z-index: 30;
|
z-index: 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 居中模式:占满空白宽度,横线水平居中,宽度变化即对称展开 */
|
||||||
|
.chat-quick-nav--center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.qn-line {
|
.qn-line {
|
||||||
height: 2px;
|
height: 2px;
|
||||||
width: 8px;
|
width: 8px;
|
||||||
@ -123,7 +130,9 @@
|
|||||||
background: var(--text-tertiary);
|
background: var(--text-tertiary);
|
||||||
opacity: 0.45;
|
opacity: 0.45;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: width 0.16s ease, opacity 0.16s ease, background-color 0.16s ease;
|
/* 宽度(hover 波浪)保持快速;颜色/透明度(当前位置亮灭)走 0.25s 交叉渐变,
|
||||||
|
一根逐渐亮起、一根逐渐熄灭,肉眼可分辨但仍然很快 */
|
||||||
|
transition: width 0.16s ease, opacity 0.25s ease, background-color 0.25s ease;
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
@ -138,6 +147,19 @@
|
|||||||
bottom: -5px;
|
bottom: -5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 滚动位置同步:当前阅读轮次(与 hover 同色,保证醒目) */
|
||||||
|
.qn-line.is-current {
|
||||||
|
background: var(--text-primary);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* hover 任一根线时,当前位置线让位:只有 hover 的那根亮 */
|
||||||
|
.chat-quick-nav:hover .qn-line.is-current:not(:hover) {
|
||||||
|
background: var(--text-tertiary);
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* hover 高亮优先于滚动定位 */
|
||||||
.qn-line.is-hot {
|
.qn-line.is-hot {
|
||||||
background: var(--text-primary);
|
background: var(--text-primary);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user