huminmin
3 天以前 b0c3dbefea78b106b7c680597361ea37930eaa0d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
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
  }
}