import { ref, watch, nextTick, getCurrentInstance } from 'vue'; import { onReady } from '@dcloudio/uni-app'; import { generateHazardHistory } from './hazardDetail.js'; /** * 隐患详情左右联动滚动(与 HazardDetailPanel 保持一致) * - 右侧滚动时,根据 node-section 位置同步左侧 activeIndex * - 点击左侧步骤时,右侧 scroll-into-view 定位到对应卡片 */ export function useHazardDetailScroll(detailSource, loadingSource) { const instance = getCurrentInstance(); const queryScope = instance?.proxy || instance; const historyList = ref([]); const activeIndex = ref(0); const sectionOffsets = ref([0]); const contentViewHeight = ref(0); const contentScrollIntoView = ref(''); const stepScrollIntoView = ref(''); const scrollWithAnimation = ref(true); const isProgrammaticScroll = ref(false); let measureLayoutTimer = null; const measureLayout = () => { if (!historyList.value.length) return; nextTick(() => { const query = uni.createSelectorQuery().in(queryScope); query.select('.content-scroll').boundingClientRect(); query.select('.content-scroll').scrollOffset(); query.selectAll('.node-section').boundingClientRect(); query.exec((res) => { const containerRect = res?.[0]; const scrollOffset = res?.[1]; const sections = res?.[2] || []; if (!containerRect || !sections.length) return; contentViewHeight.value = containerRect.height || 0; const baseScrollTop = scrollOffset?.scrollTop || 0; sectionOffsets.value = sections.map( (section) => section.top - containerRect.top + baseScrollTop ); }); }); }; const scheduleMeasureLayout = () => { clearTimeout(measureLayoutTimer); measureLayoutTimer = setTimeout(() => { measureLayout(); }, 80); }; const rebuildHistory = (data) => { historyList.value = generateHazardHistory(data); activeIndex.value = 0; contentScrollIntoView.value = ''; scheduleMeasureLayout(); setTimeout(scheduleMeasureLayout, 300); }; const syncActiveIndex = (scrollTop, scrollHeight = 0) => { const count = historyList.value.length; if (!count) return; if (scrollHeight > 0 && contentViewHeight.value > 0) { if (scrollTop + contentViewHeight.value >= scrollHeight - 60) { if (activeIndex.value !== count - 1) { activeIndex.value = count - 1; } return; } } const offsets = sectionOffsets.value; if (!offsets.length) return; let idx = 0; for (let i = offsets.length - 1; i >= 0; i--) { if (scrollTop >= offsets[i] - 80) { idx = i; break; } } if (idx !== activeIndex.value) { activeIndex.value = idx; } }; const onContentScroll = (e) => { if (isProgrammaticScroll.value) return; const { scrollTop = 0, scrollHeight = 0 } = e.detail || {}; syncActiveIndex(scrollTop, scrollHeight); scheduleMeasureLayout(); }; const onScrollToLower = () => { const count = historyList.value.length; if (count > 0 && activeIndex.value !== count - 1) { activeIndex.value = count - 1; } }; const scrollToNode = (index) => { if (index < 0 || index >= historyList.value.length) return; isProgrammaticScroll.value = true; scrollWithAnimation.value = true; activeIndex.value = index; contentScrollIntoView.value = 'hazard-node-' + index; setTimeout(() => { contentScrollIntoView.value = ''; isProgrammaticScroll.value = false; scheduleMeasureLayout(); }, 350); }; watch( () => (typeof loadingSource === 'function' ? loadingSource() : loadingSource?.value), (loading) => { if (!loading) { scheduleMeasureLayout(); setTimeout(scheduleMeasureLayout, 300); } } ); watch( () => (typeof detailSource === 'function' ? detailSource() : detailSource?.value), (val) => rebuildHistory(val), { immediate: true, deep: true } ); watch(activeIndex, (index) => { stepScrollIntoView.value = 'hazard-step-' + index; setTimeout(() => { stepScrollIntoView.value = ''; }, 300); }); watch( () => historyList.value.length, () => scheduleMeasureLayout() ); onReady(() => { scheduleMeasureLayout(); setTimeout(scheduleMeasureLayout, 300); }); return { historyList, activeIndex, contentScrollIntoView, stepScrollIntoView, scrollWithAnimation, onContentScroll, onScrollToLower, scrollToNode, scheduleMeasureLayout }; }