|
|
@@ -1,6 +1,6 @@
|
|
|
<template>
|
|
|
<ModalDialog :visible="visible" :title="institution?.name || ''" @close="handleClose">
|
|
|
- <div class="institution-detail">
|
|
|
+ <div class="institution-detail" ref="detailContainer">
|
|
|
<!-- 基本信息和摄像头区域 -->
|
|
|
<div class="detail-section detail-section-first" style="height: 936px">
|
|
|
<!-- 基本信息 -->
|
|
|
@@ -164,15 +164,61 @@
|
|
|
|
|
|
<div class="five-dimensions">
|
|
|
<!-- 钱花得明白 -->
|
|
|
- <FinanceDimension :visible="visible" />
|
|
|
+ <div :ref="(el) => setDimensionRef(el as HTMLElement, 0)" class="dimension-wrapper">
|
|
|
+ <FinanceDimension v-if="loadedDimensions[0]" :visible="visible" />
|
|
|
+ <div v-else class="dimension-placeholder">
|
|
|
+ <div class="loading-spinner"></div>
|
|
|
+ <div class="loading-text">加载中...</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
<!-- 饭吃得满意 -->
|
|
|
- <FoodSatisfactionDimension :visible="visible" style="margin-top: 60px" />
|
|
|
+ <div
|
|
|
+ :ref="(el) => setDimensionRef(el as HTMLElement, 1)"
|
|
|
+ class="dimension-wrapper"
|
|
|
+ style="margin-top: 60px"
|
|
|
+ >
|
|
|
+ <FoodSatisfactionDimension v-if="loadedDimensions[1]" :visible="visible" />
|
|
|
+ <div v-else class="dimension-placeholder">
|
|
|
+ <div class="loading-spinner"></div>
|
|
|
+ <div class="loading-text">加载中...</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
<!-- 衣穿得舒适 -->
|
|
|
- <ClothingSatisfactionDimension :visible="visible" style="margin-top: 60px" />
|
|
|
+ <div
|
|
|
+ :ref="(el) => setDimensionRef(el as HTMLElement, 2)"
|
|
|
+ class="dimension-wrapper"
|
|
|
+ style="margin-top: 60px"
|
|
|
+ >
|
|
|
+ <ClothingSatisfactionDimension v-if="loadedDimensions[2]" :visible="visible" />
|
|
|
+ <div v-else class="dimension-placeholder">
|
|
|
+ <div class="loading-spinner"></div>
|
|
|
+ <div class="loading-text">加载中...</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
<!-- 房住得安全 -->
|
|
|
- <HouseSafetyDimension :visible="visible" style="margin-top: 60px" />
|
|
|
+ <div
|
|
|
+ :ref="(el) => setDimensionRef(el as HTMLElement, 3)"
|
|
|
+ class="dimension-wrapper"
|
|
|
+ style="margin-top: 60px"
|
|
|
+ >
|
|
|
+ <HouseSafetyDimension v-if="loadedDimensions[3]" :visible="visible" />
|
|
|
+ <div v-else class="dimension-placeholder">
|
|
|
+ <div class="loading-spinner"></div>
|
|
|
+ <div class="loading-text">加载中...</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
<!-- 人活得体面 -->
|
|
|
- <DignityDimension :visible="visible" style="margin-top: 60px" />
|
|
|
+ <div
|
|
|
+ :ref="(el) => setDimensionRef(el as HTMLElement, 4)"
|
|
|
+ class="dimension-wrapper"
|
|
|
+ style="margin-top: 60px"
|
|
|
+ >
|
|
|
+ <DignityDimension v-if="loadedDimensions[4]" :visible="visible" />
|
|
|
+ <div v-else class="dimension-placeholder">
|
|
|
+ <div class="loading-spinner"></div>
|
|
|
+ <div class="loading-text">加载中...</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -180,6 +226,7 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
+import { ref, onMounted, onUnmounted, watch } from 'vue'
|
|
|
import ModalDialog from './ModalDialog.vue'
|
|
|
import NumberAnimation from './NumberAnimation.vue'
|
|
|
import FinanceDimension from './FinanceDimension.vue'
|
|
|
@@ -198,9 +245,99 @@ const emit = defineEmits<{
|
|
|
close: []
|
|
|
}>()
|
|
|
|
|
|
+// 五维组件的引用 - 使用 Map 存储,key 为索引
|
|
|
+const dimensionRefs = ref<Map<number, HTMLElement>>(new Map())
|
|
|
+// 记录哪些维度已经加载
|
|
|
+const loadedDimensions = ref<boolean[]>(new Array(5).fill(false))
|
|
|
+// IntersectionObserver 实例
|
|
|
+let observer: IntersectionObserver | null = null
|
|
|
+
|
|
|
+// 设置 ref 的函数
|
|
|
+const setDimensionRef = (el: HTMLElement | null, index: number) => {
|
|
|
+ if (el) {
|
|
|
+ dimensionRefs.value.set(index, el)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 创建 IntersectionObserver
|
|
|
+const createObserver = () => {
|
|
|
+ if (observer) {
|
|
|
+ observer.disconnect()
|
|
|
+ }
|
|
|
+
|
|
|
+ observer = new IntersectionObserver(
|
|
|
+ (entries) => {
|
|
|
+ entries.forEach((entry) => {
|
|
|
+ if (entry.isIntersecting) {
|
|
|
+ // 通过遍历 Map 找到对应的索引
|
|
|
+ let foundIndex = -1
|
|
|
+ dimensionRefs.value.forEach((el, idx) => {
|
|
|
+ if (el === entry.target) {
|
|
|
+ foundIndex = idx
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ if (foundIndex !== -1 && !loadedDimensions.value[foundIndex]) {
|
|
|
+ // 使用 requestAnimationFrame 确保在下一帧渲染,避免阻塞
|
|
|
+ requestAnimationFrame(() => {
|
|
|
+ loadedDimensions.value[foundIndex] = true
|
|
|
+ })
|
|
|
+ // 加载后取消观察
|
|
|
+ observer?.unobserve(entry.target)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ {
|
|
|
+ rootMargin: '200px', // 在元素进入视口前200px开始加载
|
|
|
+ threshold: 0.1,
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+ // 观察所有维度组件容器
|
|
|
+ dimensionRefs.value.forEach((el) => {
|
|
|
+ if (el) {
|
|
|
+ observer?.observe(el)
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 重置加载状态
|
|
|
+const resetLoadedDimensions = () => {
|
|
|
+ loadedDimensions.value = new Array(5).fill(false)
|
|
|
+}
|
|
|
+
|
|
|
+// 监听弹窗可见性变化
|
|
|
+watch(
|
|
|
+ () => props.visible,
|
|
|
+ (newVisible) => {
|
|
|
+ if (newVisible) {
|
|
|
+ // 弹窗打开时重置状态
|
|
|
+ resetLoadedDimensions()
|
|
|
+ // 等待DOM更新后创建观察者
|
|
|
+ requestAnimationFrame(() => {
|
|
|
+ createObserver()
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ // 弹窗关闭时清理观察者
|
|
|
+ if (observer) {
|
|
|
+ observer.disconnect()
|
|
|
+ observer = null
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+)
|
|
|
+
|
|
|
const handleClose = () => {
|
|
|
emit('close')
|
|
|
}
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ if (observer) {
|
|
|
+ observer.disconnect()
|
|
|
+ observer = null
|
|
|
+ }
|
|
|
+})
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
@@ -473,5 +610,40 @@ $neon-cyan: #00ffe6;
|
|
|
flex-direction: column;
|
|
|
gap: 30px;
|
|
|
}
|
|
|
+
|
|
|
+ .dimension-wrapper {
|
|
|
+ min-height: 800px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .dimension-placeholder {
|
|
|
+ min-height: 800px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ border: 1px dashed rgba(0, 255, 230, 0.3);
|
|
|
+ background: rgba(0, 255, 230, 0.02);
|
|
|
+
|
|
|
+ .loading-spinner {
|
|
|
+ width: 60px;
|
|
|
+ height: 60px;
|
|
|
+ border: 4px solid rgba(0, 255, 230, 0.1);
|
|
|
+ border-top-color: #00ffe6;
|
|
|
+ border-radius: 50%;
|
|
|
+ animation: spin 1s linear infinite;
|
|
|
+ }
|
|
|
+
|
|
|
+ .loading-text {
|
|
|
+ margin-top: 20px;
|
|
|
+ font-size: 28px;
|
|
|
+ color: rgba(0, 255, 230, 0.6);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @keyframes spin {
|
|
|
+ to {
|
|
|
+ transform: rotate(360deg);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
</style>
|