This commit is contained in:
2026-08-05 15:55:02 +08:00
parent a6828df5b9
commit 356bec03fb
16 changed files with 640 additions and 27 deletions
+137
View File
@@ -0,0 +1,137 @@
<script setup lang="ts">
import { nextTick, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessageBox } from 'element-plus'
import { MoreHorizontal, X } from '@lucide/vue'
import { useAuthStore } from '@/modules/auth/public'
import { getFirstAccessiblePath } from '@/navigation/clientNavigation'
import { useUnsavedWorkStore } from '@/navigation/workspace-tabs/unsavedWork'
import { useWorkspaceTabsStore } from '@/navigation/workspace-tabs/workspaceTabs'
type CloseCommand = 'current' | 'others' | 'right' | 'all'
const router = useRouter()
const auth = useAuthStore()
const workspace = useWorkspaceTabsStore()
const unsaved = useUnsavedWorkStore()
const tabScroller = ref<HTMLElement>()
async function activate(fullPath: string) {
if (fullPath === workspace.activeTab?.fullPath) return
await router.push(fullPath)
}
async function confirmDiscard(keys: readonly string[]) {
const dirty = unsaved.findDirty(keys)
if (dirty.length === 0) return true
const labels = [...new Set(dirty.map((entry) => entry.label))].join('、')
try {
await ElMessageBox.confirm(
`“${labels}”还有未保存的修改。关闭页签后这些内容将丢失。`,
'放弃未保存的修改?',
{ type: 'warning', confirmButtonText: '放弃并关闭', cancelButtonText: '继续编辑' },
)
return true
} catch {
return false
}
}
async function closeTabs(keys: readonly string[]) {
const existingKeys = keys.filter((key) => workspace.tabs.some((tab) => tab.key === key))
if (existingKeys.length === 0 || !await confirmDiscard(existingKeys)) return
unsaved.removeRoutes(existingKeys)
const result = workspace.close(existingKeys)
if (!result.closedActive) return
await router.push(result.fallbackPath ?? getFirstAccessiblePath(auth.permissionCodes))
}
function keysForCommand(command: CloseCommand) {
const activeIndex = workspace.tabs.findIndex((tab) => tab.key === workspace.activeKey)
if (command === 'current') return workspace.activeKey ? [workspace.activeKey] : []
if (command === 'others') return workspace.tabs.filter((tab) => tab.key !== workspace.activeKey).map((tab) => tab.key)
if (command === 'right') return activeIndex < 0 ? [] : workspace.tabs.slice(activeIndex + 1).map((tab) => tab.key)
return workspace.tabs.map((tab) => tab.key)
}
function handleCommand(command: CloseCommand) {
void closeTabs(keysForCommand(command))
}
function handleTabWheel(event: WheelEvent) {
const scroller = tabScroller.value
if (!scroller || scroller.scrollWidth <= scroller.clientWidth) return
const delta = Math.abs(event.deltaX) > Math.abs(event.deltaY) ? event.deltaX : event.deltaY
if (delta === 0) return
const maxScrollLeft = scroller.scrollWidth - scroller.clientWidth
const canScroll = delta < 0 ? scroller.scrollLeft > 0 : scroller.scrollLeft < maxScrollLeft
if (!canScroll) return
event.preventDefault()
scroller.scrollLeft = Math.max(0, Math.min(maxScrollLeft, scroller.scrollLeft + delta))
}
watch(
() => workspace.activeKey,
async () => {
await nextTick()
tabScroller.value?.querySelector<HTMLElement>('.workspace-tab.is-active')?.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'nearest',
})
},
)
</script>
<template>
<section class="workspace-tabs" aria-label="已打开页面">
<div
ref="tabScroller"
class="workspace-tabs__scroller"
role="tablist"
aria-label="工作区页签"
@wheel="handleTabWheel"
>
<div
v-for="tab in workspace.tabs"
:key="tab.key"
class="workspace-tab"
:class="{ 'is-active': tab.key === workspace.activeKey, 'is-dirty': unsaved.dirtyRouteKeys.has(tab.key) }"
>
<button
type="button"
class="workspace-tab__label"
role="tab"
:aria-selected="tab.key === workspace.activeKey"
:aria-current="tab.key === workspace.activeKey ? 'page' : undefined"
:title="tab.title"
@click="activate(tab.fullPath)"
>
<span>{{ tab.title }}</span>
<i v-if="unsaved.dirtyRouteKeys.has(tab.key)" aria-label="有未保存修改" />
</button>
<button
type="button"
class="workspace-tab__close"
:aria-label="`关闭${tab.title}`"
:title="`关闭${tab.title}`"
@click="closeTabs([tab.key])"
><X :size="14" aria-hidden="true" /></button>
</div>
</div>
<el-dropdown trigger="click" placement="bottom-end" @command="handleCommand">
<button type="button" class="workspace-tabs__more" aria-label="页签管理" title="页签管理">
<MoreHorizontal :size="18" aria-hidden="true" />
</button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="current" :disabled="!workspace.activeKey">关闭当前</el-dropdown-item>
<el-dropdown-item command="others" :disabled="workspace.tabs.length <= 1">关闭其他</el-dropdown-item>
<el-dropdown-item command="right" :disabled="keysForCommand('right').length === 0">关闭右侧</el-dropdown-item>
<el-dropdown-item command="all" :disabled="workspace.tabs.length === 0" divided>关闭全部</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</section>
</template>