Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 71 additions & 56 deletions src/renderer/components/workspace/TabBar.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import { vDraggable } from 'vue-draggable-plus'
import useFile from '@/renderer/hooks/useFile'
import useTab from '@/renderer/hooks/useTab'
import { onMounted, onUnmounted, ref } from "vue";
import { vDraggable } from "vue-draggable-plus";
import useFile from "@/renderer/hooks/useFile";
import useTab from "@/renderer/hooks/useTab";
Comment on lines +2 to +5
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is described as a hover-style tweak, but the script section also introduces broad non-functional reformatting (quote style + semicolons). Consider reverting unrelated formatting-only changes so the diff stays focused and easier to review/blame in the future.

Copilot uses AI. Check for mistakes.

const {
formattedTabs,
Expand All @@ -14,99 +14,111 @@ const {
setupTabScrollListener,
cleanupInertiaScroll,
reorderTabs,
} = useTab()
} = useTab();

const { createNewFile } = useFile()
const { createNewFile } = useFile();

// 拦截 ctrl/cmd + w 快捷键关闭tab
function handleCloseTabShortcut(e: KeyboardEvent) {
const isMac = window.electronAPI.platform === 'darwin'
if ((isMac ? e.metaKey : e.ctrlKey) && e.key.toLowerCase() === 'w') {
e.preventDefault()
const isMac = window.electronAPI.platform === "darwin";
if ((isMac ? e.metaKey : e.ctrlKey) && e.key.toLowerCase() === "w") {
e.preventDefault();
if (activeTabId.value) {
closeWithConfirm(activeTabId.value)
closeWithConfirm(activeTabId.value);
}
}
}
window.addEventListener('keydown', handleCloseTabShortcut)
window.addEventListener("keydown", handleCloseTabShortcut);

Comment on lines +31 to 32
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

window.addEventListener('keydown', ...) is registered at setup evaluation time (outside onMounted). To avoid listeners being attached before mount (and to reduce leak risk if setup ever errors), register the handler inside onMounted and keep the corresponding removal in onUnmounted.

Suggested change
window.addEventListener("keydown", handleCloseTabShortcut);
onMounted(() => {
window.addEventListener("keydown", handleCloseTabShortcut);
});
onUnmounted(() => {
window.removeEventListener("keydown", handleCloseTabShortcut);
});

Copilot uses AI. Check for mistakes.
// 获取tab容器的DOM引用
const tabContainerRef = ref<HTMLElement | null>(null)
const tabContainerRef = ref<HTMLElement | null>(null);

// 保存wheel事件处理器引用以便正确移除
let wheelHandler: ((event: WheelEvent) => void) | null = null
let wheelHandler: ((event: WheelEvent) => void) | null = null;

function handleTabClick(id: string) {
switchToTab(id)
switchToTab(id);
}

function handleAddTab() {
createNewFile()
createNewFile();
}

async function handleCloseTab(id: string, event: Event) {
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleCloseTab is marked async but doesn't await anything. This returns a Promise unnecessarily and can be misleading; consider removing async (or await the confirm/close call if it is actually asynchronous).

Suggested change
async function handleCloseTab(id: string, event: Event) {
function handleCloseTab(id: string, event: Event) {

Copilot uses AI. Check for mistakes.
event.stopPropagation()
closeWithConfirm(id)
event.stopPropagation();
closeWithConfirm(id);
}

// 处理拖动排序
function handleDragEnd(event: { oldIndex: number, newIndex: number }) {
reorderTabs(event.oldIndex, event.newIndex)
function handleDragEnd(event: { oldIndex: number; newIndex: number }) {
reorderTabs(event.oldIndex, event.newIndex);
}

// 因为vueTransition的移除会让元素回到父元素0, so 需要保存位置信息
function handleBeforeLeave(el: Element) {
const element = el as HTMLElement
const rect = element.getBoundingClientRect()
const parentRect = element.parentElement?.getBoundingClientRect()
const element = el as HTMLElement;
const rect = element.getBoundingClientRect();
const parentRect = element.parentElement?.getBoundingClientRect();

if (parentRect) {
const left = rect.left - parentRect.left
const top = rect.top - parentRect.top
const left = rect.left - parentRect.left;
const top = rect.top - parentRect.top;

element.style.setProperty('--tab-left', `${left}px`)
element.style.setProperty('--tab-top', `${top}px`)
element.style.setProperty("--tab-left", `${left}px`);
element.style.setProperty("--tab-top", `${top}px`);
}
}

// 设置滚动监听
setupTabScrollListener(tabContainerRef)
setupTabScrollListener(tabContainerRef);

// 组件挂载时添加事件监听器
onMounted(() => {
const container = tabContainerRef.value
const container = tabContainerRef.value;
if (container) {
// 创建并保存事件处理器引用
wheelHandler = (event: WheelEvent) => handleWheelScroll(event, tabContainerRef)
container.addEventListener('wheel', wheelHandler, { passive: false })
wheelHandler = (event: WheelEvent) => handleWheelScroll(event, tabContainerRef);
container.addEventListener("wheel", wheelHandler, { passive: false });
}
})
});

// 组件卸载时移除事件监听器和清理惯性滚动实例
onUnmounted(() => {
const container = tabContainerRef.value
const container = tabContainerRef.value;
if (container && wheelHandler) {
// 使用保存的引用移除监听器
container.removeEventListener('wheel', wheelHandler)
container.removeEventListener("wheel", wheelHandler);
// 清理惯性滚动实例
cleanupInertiaScroll(container)
cleanupInertiaScroll(container);
}
// 移除全局键盘事件监听器
window.removeEventListener('keydown', handleCloseTabShortcut)
})
window.removeEventListener("keydown", handleCloseTabShortcut);
});
</script>

<template>
<div ref="tabContainerRef" class="tabBarContarner" :class="{ 'offset-right': shouldOffsetTabBar }">
<div
ref="tabContainerRef"
class="tabBarContarner"
:class="{ 'offset-right': shouldOffsetTabBar }"
>
<TransitionGroup
v-draggable="[formattedTabs, { animation: 1500, onEnd: handleDragEnd, ghostClass: 'ghost' }]"
name="tab" class="tabBar" mode="out-in" tag="div" @before-leave="handleBeforeLeave"
name="tab"
class="tabBar"
mode="out-in"
tag="div"
@before-leave="handleBeforeLeave"
>
<div
v-for="tab in formattedTabs" :key="tab.id" class="tabItem" :class="{ active: activeTabId === tab.id }"
:data-tab-id="tab.id" @click="handleTabClick(tab.id)"
v-for="tab in formattedTabs"
:key="tab.id"
class="tabItem"
:class="{ active: activeTabId === tab.id }"
:data-tab-id="tab.id"
@click="handleTabClick(tab.id)"
>
<p>{{ `${tab.readOnly ? '[只读] ' : ''}${tab.displayName}` }}</p>
<p>{{ `${tab.readOnly ? "[只读] " : ""}${tab.displayName}` }}</p>

<div class="closeIcon">
<span class="iconfont icon-close" @click="handleCloseTab(tab.id, $event)"></span>
Expand All @@ -115,15 +127,21 @@ onUnmounted(() => {
<!-- pre -->

<svg
:class="{ active: activeTabId === tab.id }" class="pre" viewBox="0 0 5 5" fill="none"
:class="{ active: activeTabId === tab.id }"
class="pre"
viewBox="0 0 5 5"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M5 5L0 5C3.33333 5 5 3.33333 5 -2.18557e-07L5 5Z" />
</svg>

<!-- after -->
<svg
:class="{ active: activeTabId === tab.id }" class="after" viewBox="0 0 5 5" fill="none"
:class="{ active: activeTabId === tab.id }"
class="after"
viewBox="0 0 5 5"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M0 5L5 5C1.66667 5 7.28523e-08 3.33333 2.18557e-07 -2.18557e-07L0 5Z" />
Expand All @@ -140,7 +158,6 @@ onUnmounted(() => {

<style lang="less" scoped>
.tabBarContarner {

flex: 1;
height: 100%;
display: flex;
Expand All @@ -149,7 +166,7 @@ onUnmounted(() => {
justify-content: flex-end;
overflow-x: scroll;
overflow-y: hidden;
transition: margin-left 0.6s 0.02s cubic-bezier(0.035, 0.630, 0.000, 1.000); //一个延迟能变得高级,你就学吧
transition: margin-left 0.6s 0.02s cubic-bezier(0.035, 0.63, 0, 1); //一个延迟能变得高级,你就学吧

&::-webkit-scrollbar {
display: none;
Expand Down Expand Up @@ -199,13 +216,21 @@ onUnmounted(() => {
line-height: 28px;
cursor: pointer;
color: var(--text-color-3);

&::before {
padding: 2px;
}
}
}

.closeIcon:hover {

span {
color: var(--text-color-1);

&::before {
Comment on lines 226 to +230
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.closeIcon:hover span { color: var(--text-color-1) } is effectively overridden when the tab is hovered because the later &:hover .closeIcon span { color: var(--text-color-2) } selector is more specific and appears later in the file. If the intent is to change the close icon color on close-button hover, increase selector specificity (e.g. &:hover .closeIcon:hover span) and/or move the override after the tab hover block so it wins in the cascade.

Copilot uses AI. Check for mistakes.
background: var(--active-color);
border-radius: 9999px;
}
}
}

Expand All @@ -228,7 +253,6 @@ onUnmounted(() => {
&:hover {
color: var(--border-color-2);
}

}

&.active {
Expand All @@ -243,29 +267,24 @@ onUnmounted(() => {
span {
color: var(--text-color-1);
}

}

&:hover {

z-index: 1;

p {
color: var(--text-color-2);
}

.closeIcon {

span {
color: var(--text-color-2);
}
}

}
}

.addTab {

display: flex;
align-items: center;
justify-content: center;
Expand All @@ -282,7 +301,6 @@ onUnmounted(() => {
cursor: pointer;
color: var(--text-color-3);
padding: 4px 11px;

}

.addTabLine {
Expand All @@ -293,15 +311,13 @@ onUnmounted(() => {

&:hover {
span {

background: var(--hover-color);
}

.addTabLine {
background: var(--border-color-2);
}
}

}

.pre {
Expand All @@ -319,7 +335,6 @@ onUnmounted(() => {
&.active {
fill: var(--background-color-1);
}

}

.after {
Expand Down
Loading