Files
voidraft/frontend/src/components/tabs/TabContainer.vue
2026-01-02 00:03:50 +08:00

276 lines
7.3 KiB
Vue

<template>
<div class="tab-container" style="--wails-draggable:drag">
<div class="tab-bar" ref="tabBarRef">
<div class="tab-scroll-wrapper" ref="tabScrollWrapperRef" style="--wails-draggable:drag" @wheel.prevent.stop="onWheelScroll">
<div class="tab-list" ref="tabListRef">
<TabItem
v-for="tab in tabStore.tabs"
:key="tab.documentId"
:tab="tab"
:isActive="tab.documentId === documentStore.currentDocumentId"
:canClose="tabStore.canCloseTab"
@click="switchToTab"
@close="closeTab"
@dragstart="onDragStart"
@dragover="onDragOver"
@drop="onDrop"
@contextmenu="onContextMenu"
/>
</div>
</div>
</div>
<!-- 右键菜单 -->
<TabContextMenu
:visible="showContextMenu"
:position="contextMenuPosition"
:targetDocumentId="contextMenuTargetId"
@close="closeContextMenu"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
import TabItem from './TabItem.vue';
import TabContextMenu from './TabContextMenu.vue';
import { useTabStore } from '@/stores/tabStore';
import { useDocumentStore } from '@/stores/documentStore';
import { useEditorStore } from '@/stores/editorStore';
import { useEditorStateStore } from '@/stores/editorStateStore';
const tabStore = useTabStore();
const documentStore = useDocumentStore();
const editorStore = useEditorStore();
const editorStateStore = useEditorStateStore();
// DOM 引用
const tabBarRef = ref<HTMLElement>();
const tabListRef = ref<HTMLElement>();
const tabScrollWrapperRef = ref<HTMLDivElement | null>(null);
// 右键菜单状态
const showContextMenu = ref(false);
const contextMenuPosition = ref({ x: 0, y: 0 });
const contextMenuTargetId = ref<number | null>(null);
// 标签页操作
const switchToTab = async (documentId: number) => {
// 保存旧文档的光标位置
const oldDocId = documentStore.currentDocumentId;
if (oldDocId) {
const cursorPos = editorStore.getCurrentCursorPosition();
editorStateStore.saveCursorPosition(oldDocId, cursorPos);
}
// 如果旧文档有未保存修改,保存它
if (oldDocId && editorStore.hasUnsavedChanges(oldDocId)) {
try {
const content = editorStore.getCurrentContent();
await documentStore.saveDocument(oldDocId, content);
editorStore.syncAfterSave(oldDocId);
} catch (error) {
console.error('save document error:', error);
}
}
// 切换文档
await tabStore.switchToTabAndDocument(documentId);
// 切换到新编辑器
await editorStore.switchToEditor(documentId);
// 更新标签页
if (documentStore.currentDocument && tabStore.isTabsEnabled) {
tabStore.addOrActivateTab(documentStore.currentDocument);
}
};
const closeTab = (documentId: number) => {
tabStore.closeTab(documentId);
};
// 拖拽操作
const onDragStart = (event: DragEvent, documentId: number) => {
tabStore.draggedTabId = documentId;
event.dataTransfer!.effectAllowed = 'move';
};
const onDragOver = (event: DragEvent) => {
event.preventDefault();
event.dataTransfer!.dropEffect = 'move';
};
const onDrop = (event: DragEvent, targetDocumentId: number) => {
event.preventDefault();
if (tabStore.draggedTabId && tabStore.draggedTabId !== targetDocumentId) {
const draggedIndex = tabStore.getTabIndex(tabStore.draggedTabId);
const targetIndex = tabStore.getTabIndex(targetDocumentId);
if (draggedIndex !== -1 && targetIndex !== -1) {
tabStore.moveTab(draggedIndex, targetIndex);
}
}
tabStore.draggedTabId = null;
};
// 右键菜单操作
const onContextMenu = (event: MouseEvent, documentId: number) => {
event.preventDefault();
event.stopPropagation();
contextMenuPosition.value = { x: event.clientX, y: event.clientY };
contextMenuTargetId.value = documentId;
showContextMenu.value = true;
};
const closeContextMenu = () => {
showContextMenu.value = false;
contextMenuTargetId.value = null;
};
// 滚轮滚动处理
const onWheelScroll = (event: WheelEvent) => {
const el = tabScrollWrapperRef.value;
if (!el) return;
const delta = event.deltaY || event.deltaX || 0;
el.scrollLeft += delta;
};
// 自动滚动到活跃标签页
const scrollToActiveTab = async () => {
await nextTick();
const scrollWrapper = tabScrollWrapperRef.value;
const tabList = tabListRef.value;
if (!scrollWrapper || !tabList) return;
// 查找当前活跃的标签页元素
const activeTabElement = tabList.querySelector('.tab-item.active') as HTMLElement;
if (!activeTabElement) return;
const scrollWrapperRect = scrollWrapper.getBoundingClientRect();
const activeTabRect = activeTabElement.getBoundingClientRect();
// 计算活跃标签页相对于滚动容器的位置
const activeTabLeft = activeTabRect.left - scrollWrapperRect.left + scrollWrapper.scrollLeft;
const activeTabRight = activeTabLeft + activeTabRect.width;
// 获取滚动容器的可视区域
const scrollLeft = scrollWrapper.scrollLeft;
const scrollRight = scrollLeft + scrollWrapper.clientWidth;
// 如果活跃标签页不在可视区域内,则滚动到合适位置
if (activeTabLeft < scrollLeft) {
// 标签页在左侧不可见,滚动到左边
scrollWrapper.scrollLeft = activeTabLeft - 10; // 留一点边距
} else if (activeTabRight > scrollRight) {
// 标签页在右侧不可见,滚动到右边
scrollWrapper.scrollLeft = activeTabRight - scrollWrapper.clientWidth + 10; // 留一点边距
}
};
onMounted(() => {
// 组件挂载时的初始化逻辑
});
onUnmounted(() => {
// 组件卸载时的清理逻辑
});
// 监听当前活跃标签页的变化
watch(() => documentStore.currentDocumentId, () => {
scrollToActiveTab();
});
// 监听标签页列表变化
watch(() => tabStore.tabs.length, () => {
scrollToActiveTab();
});
</script>
<style scoped lang="scss">
.tab-container {
position: relative;
background: transparent;
height: 32px;
}
.tab-bar {
display: flex;
align-items: center;
height: 100%;
background: var(--toolbar-bg);
min-width: 0; /* 允许子项收缩,确保产生横向溢出 */
}
.tab-scroll-wrapper {
flex: 1;
min-width: 0; // 关键:允许作为 flex 子项收缩,从而产生横向溢出
overflow-x: auto;
overflow-y: hidden;
position: relative;
scrollbar-width: none;
-ms-overflow-style: none;
pointer-events: auto;
&::-webkit-scrollbar {
display: none;
}
}
.tab-list {
display: flex;
width: max-content; /* 令宽度等于所有子项总宽度,必定溢出 */
white-space: nowrap;
pointer-events: auto;
}
.tab-actions {
display: flex;
align-items: center;
padding: 0 4px;
background: var(--toolbar-bg);
flex-shrink: 0; /* 防止被压缩 */
}
.tab-action-btn {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
margin: 0 1px;
border-radius: 3px;
cursor: pointer;
color: var(--toolbar-text);
transition: background-color 0.2s ease;
&:hover {
background: var(--toolbar-button-hover);
}
svg {
width: 12px;
height: 12px;
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.tab-action-btn {
width: 20px;
height: 20px;
svg {
width: 10px;
height: 10px;
}
}
}
</style>