import { ref, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'
|
|
/**
|
* 图表背景位置调整 composable
|
* @param {Object} options 配置选项
|
* @param {Ref} options.wrapperRef - 图表容器的 ref
|
* @param {Ref} options.backgroundRef - 背景元素的 ref
|
* @param {String} options.left - 背景 left 位置,如 '25%' 或 '50%',默认 '50%'
|
* @param {String} options.top - 背景 top 位置,如 '50%',默认 '50%'
|
* @param {String} options.offsetX - X 轴偏移值,如 '-51.5%' 或 '-50%',默认 '-50%'
|
* @param {String} options.offsetY - Y 轴偏移值,如 '-39%' 或 '-50%',默认 '-50%'
|
* @param {Ref} options.watchData - 可选,监听的数据变化,数据变化时重新调整位置
|
* @returns {Function} adjustBackgroundPosition - 手动调整背景位置的方法
|
*/
|
export function useChartBackground(options = {}) {
|
const {
|
wrapperRef,
|
backgroundRef,
|
left = '50%',
|
top = '50%',
|
offsetX = '-50%',
|
offsetY = '-50%',
|
watchData = null
|
} = options
|
|
let resizeObserver = null
|
let intersectionObserver = null
|
let retryTimers = []
|
|
const clearRetryTimers = () => {
|
if (!retryTimers.length) return
|
retryTimers.forEach((t) => clearTimeout(t))
|
retryTimers = []
|
}
|
|
// 调整背景位置
|
const adjustBackgroundPosition = () => {
|
nextTick(() => {
|
if (!wrapperRef?.value || !backgroundRef?.value) {
|
return
|
}
|
|
// 初始化阶段经常出现:容器尚未可见/尺寸为 0(非全屏、tab、动画等)
|
// 这种情况下先不对齐,等 ResizeObserver / IntersectionObserver 再触发
|
const rect = wrapperRef.value.getBoundingClientRect()
|
if (!rect.width || !rect.height) return
|
|
const background = backgroundRef.value
|
|
// 使用百分比定位 + transform 微调(这是最可靠的方式)
|
background.style.left = left
|
background.style.top = top
|
background.style.transform = `translate(${offsetX}, ${offsetY})`
|
})
|
}
|
|
// 初始化阶段多次“补偿对齐”,覆盖 Echarts 首次渲染/动画造成的延迟布局
|
const scheduleKickAlign = () => {
|
clearRetryTimers()
|
;[0, 60, 180, 360, 800].forEach((ms) => {
|
retryTimers.push(
|
setTimeout(() => {
|
adjustBackgroundPosition()
|
}, ms)
|
)
|
})
|
}
|
|
// 窗口 resize 处理
|
const resizeHandler = () => {
|
adjustBackgroundPosition()
|
}
|
|
// 如果提供了 watchData,监听数据变化(需要在 setup 阶段创建)
|
if (watchData) {
|
watch(watchData, () => {
|
adjustBackgroundPosition()
|
}, { deep: true })
|
}
|
|
// 初始化
|
const init = () => {
|
// 监听窗口 resize
|
window.addEventListener('resize', resizeHandler)
|
|
// 使用 ResizeObserver 监听容器尺寸变化
|
nextTick(() => {
|
if (wrapperRef?.value && window.ResizeObserver) {
|
resizeObserver = new ResizeObserver(() => {
|
adjustBackgroundPosition()
|
})
|
resizeObserver.observe(wrapperRef.value)
|
}
|
|
// 监听“从不可见到可见”,解决初始化时未对齐但热更新又正常的问题
|
if (wrapperRef?.value && window.IntersectionObserver) {
|
intersectionObserver = new IntersectionObserver(
|
(entries) => {
|
const entry = entries?.[0]
|
if (entry?.isIntersecting) {
|
scheduleKickAlign()
|
}
|
},
|
{ threshold: 0.01 }
|
)
|
intersectionObserver.observe(wrapperRef.value)
|
}
|
|
// 初始化多次补偿对齐,确保图表渲染完成
|
scheduleKickAlign()
|
})
|
}
|
|
// 清理
|
const cleanup = () => {
|
window.removeEventListener('resize', resizeHandler)
|
clearRetryTimers()
|
if (resizeObserver) {
|
resizeObserver.disconnect()
|
resizeObserver = null
|
}
|
if (intersectionObserver) {
|
intersectionObserver.disconnect()
|
intersectionObserver = null
|
}
|
}
|
|
return {
|
adjustBackgroundPosition,
|
init,
|
cleanup
|
}
|
}
|