276 lines
7.3 KiB
Vue
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> |