fix 拖动流畅度
This commit is contained in:
1 parent
37f4a99914
commit
57a2029847
13 files changed
+576
-64
No files matched your search
+75
-1
@@ -56,6 +56,7 @@ const dataMenuOpen = ref(false)
|
||||
const dataMenuRef = ref<HTMLElement | null>(null)
|
||||
const importFileRef = ref<HTMLInputElement | null>(null)
|
||||
const tabItemElMap = new Map<string, HTMLElement>()
|
||||
const tabPanelElMap = new Map<string, HTMLElement>()
|
||||
|
||||
const tabsModel = computed({
|
||||
get: () => tabStore.tabs,
|
||||
@@ -125,6 +126,14 @@ const setTabItemRef = (id: string, el: Element | ComponentPublicInstance | null)
|
||||
tabItemElMap.delete(id)
|
||||
}
|
||||
|
||||
const setTabPanelRef = (id: string, el: Element | ComponentPublicInstance | null) => {
|
||||
if (el instanceof HTMLElement) {
|
||||
tabPanelElMap.set(id, el)
|
||||
return
|
||||
}
|
||||
tabPanelElMap.delete(id)
|
||||
}
|
||||
|
||||
const ensureActiveTabVisible = () => {
|
||||
const activeId = tabStore.activeTabId
|
||||
const el = tabItemElMap.get(activeId)
|
||||
@@ -132,6 +141,50 @@ const ensureActiveTabVisible = () => {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' })
|
||||
}
|
||||
|
||||
const getActivePanelScrollViewport = (tabId?: string | null) => {
|
||||
if (!tabId) return null
|
||||
const panelEl = tabPanelElMap.get(tabId)
|
||||
if (!panelEl) return null
|
||||
return panelEl.querySelector<HTMLElement>('[data-slot="scroll-area-viewport"]')
|
||||
}
|
||||
|
||||
const getTabScrollSessionKey = (tabId: string) => `tab-scroll-top:${tabId}`
|
||||
|
||||
const saveTabInnerScrollTop = (tabId?: string | null) => {
|
||||
if (!tabId) return
|
||||
const viewport = getActivePanelScrollViewport(tabId)
|
||||
if (!viewport) return
|
||||
const top = viewport.scrollTop || 0
|
||||
try {
|
||||
sessionStorage.setItem(getTabScrollSessionKey(tabId), String(top))
|
||||
} catch (error) {
|
||||
console.error('save tab scroll failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const restoreTabInnerScrollTop = (tabId?: string | null) => {
|
||||
if (!tabId) return
|
||||
const viewport = getActivePanelScrollViewport(tabId)
|
||||
if (!viewport) return
|
||||
let top = 0
|
||||
try {
|
||||
top = Number(sessionStorage.getItem(getTabScrollSessionKey(tabId)) || '0') || 0
|
||||
} catch (error) {
|
||||
console.error('restore tab scroll failed:', error)
|
||||
}
|
||||
viewport.scrollTop = top
|
||||
}
|
||||
|
||||
const scheduleRestoreTabInnerScrollTop = (tabId?: string | null) => {
|
||||
if (!tabId) return
|
||||
requestAnimationFrame(() => {
|
||||
restoreTabInnerScrollTop(tabId)
|
||||
requestAnimationFrame(() => {
|
||||
restoreTabInnerScrollTop(tabId)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const readWebStorage = (storageObj: Storage): DataEntry[] => {
|
||||
const entries: DataEntry[] = []
|
||||
for (let i = 0; i < storageObj.length; i++) {
|
||||
@@ -251,6 +304,7 @@ onMounted(() => {
|
||||
window.addEventListener('mousedown', handleGlobalMouseDown)
|
||||
void nextTick(() => {
|
||||
ensureActiveTabVisible()
|
||||
scheduleRestoreTabInnerScrollTop(tabStore.activeTabId)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -260,12 +314,31 @@ onBeforeUnmount(() => {
|
||||
|
||||
watch(
|
||||
() => tabStore.activeTabId,
|
||||
() => {
|
||||
(nextId, prevId) => {
|
||||
saveTabInnerScrollTop(prevId)
|
||||
void nextTick(() => {
|
||||
ensureActiveTabVisible()
|
||||
scheduleRestoreTabInnerScrollTop(nextId)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => tabStore.tabs.map(t => t.id),
|
||||
(ids) => {
|
||||
const idSet = new Set(ids)
|
||||
try {
|
||||
for (let i = sessionStorage.length - 1; i >= 0; i--) {
|
||||
const key = sessionStorage.key(i)
|
||||
if (!key || !key.startsWith('tab-scroll-top:')) continue
|
||||
const tabId = key.slice('tab-scroll-top:'.length)
|
||||
if (!idSet.has(tabId)) sessionStorage.removeItem(key)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('cleanup tab scroll cache failed:', error)
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -372,6 +445,7 @@ watch(
|
||||
<div
|
||||
v-for="tab in tabStore.tabs"
|
||||
:key="tab.id"
|
||||
:ref="el => setTabPanelRef(tab.id, el)"
|
||||
v-show="tabStore.activeTabId === tab.id"
|
||||
class="h-full w-full p-6 animate-in fade-in duration-300"
|
||||
>
|
||||
|
||||
+52
-2
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch, type Component } from 'vue'
|
||||
import { computed, onBeforeUnmount, ref, watch, type Component } from 'vue'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
interface TypeLineCategory {
|
||||
key: string
|
||||
@@ -12,6 +13,8 @@ const props = withDefaults(
|
||||
defineProps<{
|
||||
scene?: string
|
||||
title?: string
|
||||
subtitle?: string
|
||||
copyText?: string
|
||||
categories: TypeLineCategory[]
|
||||
storageKey?: string
|
||||
defaultCategory?: string
|
||||
@@ -19,6 +22,8 @@ const props = withDefaults(
|
||||
{
|
||||
scene: 'default',
|
||||
title: '配置',
|
||||
subtitle: '',
|
||||
copyText: '',
|
||||
storageKey: '',
|
||||
defaultCategory: ''
|
||||
}
|
||||
@@ -52,12 +57,57 @@ const activeComponent = computed(() => {
|
||||
const selected = props.categories.find(item => item.key === activeCategory.value)
|
||||
return selected?.component || props.categories[0]?.component || null
|
||||
})
|
||||
|
||||
const copyBtnText = ref('复制')
|
||||
let copyBtnTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const handleCopySubtitle = async () => {
|
||||
const text = (props.copyText || '').trim()
|
||||
if (!text) return
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
copyBtnText.value = '已复制'
|
||||
} catch (error) {
|
||||
console.error('copy failed:', error)
|
||||
copyBtnText.value = '复制失败'
|
||||
}
|
||||
|
||||
if (copyBtnTimer) clearTimeout(copyBtnTimer)
|
||||
copyBtnTimer = setTimeout(() => {
|
||||
copyBtnText.value = '复制'
|
||||
}, 1200)
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (copyBtnTimer) clearTimeout(copyBtnTimer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full w-full bg-background">
|
||||
<div class="w-12/100 border-r p-6 flex flex-col gap-8 relative">
|
||||
<!-- <div class="font-bold text-lg mb-4 text-primary">{{ props.title }}</div> -->
|
||||
<div v-if="props.title || props.subtitle" class="space-y-1">
|
||||
<div v-if="props.title" class="font-bold text-base leading-6 text-primary break-words">
|
||||
{{ props.title }}
|
||||
</div>
|
||||
<div
|
||||
v-if="props.subtitle"
|
||||
class="flex flex-wrap items-center gap-2 text-xs leading-5 text-muted-foreground"
|
||||
>
|
||||
<span class="break-all">{{ props.subtitle }}</span>
|
||||
<Button
|
||||
v-if="props.copyText"
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-6 rounded-md px-2 text-[11px]"
|
||||
@click.stop="handleCopySubtitle"
|
||||
>
|
||||
{{ copyBtnText }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-10 relative">
|
||||
<div class="absolute left-[11px] top-2 bottom-2 w-[2px] bg-muted"></div>
|
||||
|
||||
Reference in new issue
Block a user