108 lines
3.2 KiB
Vue
108 lines
3.2 KiB
Vue
<script setup lang="ts">
|
||
import type { Component } from 'vue'
|
||
import { ref, watch } from 'vue'
|
||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||
import { Skeleton } from '@/components/ui/skeleton'
|
||
|
||
const props = defineProps<{
|
||
activeComponent: Component | null
|
||
activeModule: string
|
||
loading: boolean
|
||
}>()
|
||
|
||
const containerRef = ref<HTMLElement | null>(null)
|
||
|
||
// 切换模块时重置主滚动区位置,避免新模块沿用上一个模块的滚动距离
|
||
watch(
|
||
() => props.activeModule,
|
||
() => {
|
||
const viewport = containerRef.value?.querySelector<HTMLElement>(
|
||
'[data-slot="scroll-area-viewport"]'
|
||
)
|
||
if (viewport) viewport.scrollTop = 0
|
||
}
|
||
)
|
||
</script>
|
||
|
||
<template>
|
||
<main
|
||
ref="containerRef"
|
||
class="flex-1 min-w-0"
|
||
:style="{ backgroundColor: 'var(--effect-bg)', backdropFilter: 'var(--effect-blur)' }"
|
||
>
|
||
<ScrollArea data-main-scroll class="h-full w-full">
|
||
<Transition name="fade-slide" mode="out-in">
|
||
<div
|
||
v-if="activeComponent"
|
||
:key="activeModule"
|
||
class="h-full w-full"
|
||
>
|
||
<component :is="activeComponent" />
|
||
</div>
|
||
<div
|
||
v-else-if="loading"
|
||
key="loading"
|
||
class="h-full w-full p-6"
|
||
>
|
||
<!-- 模块加载骨架:撑起画面,避免空白闪屏 -->
|
||
<div class="h-full max-w-5xl mx-auto space-y-5">
|
||
<div class="flex items-center gap-3">
|
||
<Skeleton class="h-8 w-40" />
|
||
<Skeleton class="h-6 w-24 ml-auto" />
|
||
</div>
|
||
<div class="grid gap-4 md:grid-cols-2">
|
||
<div v-for="n in 4" :key="n" class="rounded-lg border p-5 space-y-4">
|
||
<div class="flex items-center justify-between">
|
||
<Skeleton class="h-5 w-32" />
|
||
<Skeleton class="h-5 w-16" />
|
||
</div>
|
||
<Skeleton class="h-4 w-full" />
|
||
<Skeleton class="h-4 w-5/6" />
|
||
<Skeleton class="h-4 w-2/3" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div
|
||
v-else
|
||
key="empty"
|
||
class="h-full w-full flex items-center justify-center text-muted-foreground"
|
||
>
|
||
<p>未找到模块</p>
|
||
</div>
|
||
</Transition>
|
||
</ScrollArea>
|
||
</main>
|
||
</template>
|
||
|
||
<style scoped>
|
||
/*
|
||
* 穿透 reka-ui ScrollArea 的内部 content wrapper。
|
||
*
|
||
* reka 的 viewport(overflow 滚动容器)与我们的内容之间还有一层**无高度样式的
|
||
* div**,百分比高度链在这里断掉——模块根的 `h-full` 解析为 auto,全高模块
|
||
* (终端/翻译)塌缩成内容高度,表现为卡片下方留白。给它显式 100%:
|
||
* 内容矮于视口时撑满,高于视口时溢出仍由 viewport 滚动(scrollHeight 计入
|
||
* 后代溢出),两种场景都不破坏。限定 data-main-scroll 只作用于主滚动区,
|
||
* 不影响模块内部的局部 ScrollArea。
|
||
*/
|
||
:deep([data-main-scroll] [data-reka-scroll-area-viewport] > div) {
|
||
height: 100%;
|
||
}
|
||
|
||
.fade-slide-enter-active,
|
||
.fade-slide-leave-active {
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.fade-slide-enter-from {
|
||
opacity: 0;
|
||
transform: translateX(20px);
|
||
}
|
||
|
||
.fade-slide-leave-to {
|
||
opacity: 0;
|
||
transform: translateX(-20px);
|
||
}
|
||
</style>
|