one-screen-scale 1.0.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,67 +1,69 @@
1
- /**
2
- * Vue 3 Composable
3
- * useFitScreen(ref, options) → { scale, inverseScale, refresh }
4
- */
5
- import { ref, onMounted, onUnmounted, watch } from 'vue'
6
- import { createScaler, getInverseScale } from './core.js'
7
-
8
- export function useFitScreen(targetRef, options = {}) {
9
- const scale = ref(1)
10
- const inverseScale = ref(1)
11
-
12
- let scaler = null
13
- let onResize = null
14
-
15
- function setup(el) {
16
- if (!el) return
17
- scaler = createScaler(el, options)
18
- const s = scaler.start()
19
- scale.value = s
20
- inverseScale.value = getInverseScale(el)
21
-
22
- onResize = () => {
23
- scale.value = scaler?.getScale?.() ?? 1
24
- inverseScale.value = getInverseScale(el)
25
- }
26
- window.addEventListener('resize', onResize)
27
- }
28
-
29
- function teardown() {
30
- if (onResize) {
31
- window.removeEventListener('resize', onResize)
32
- onResize = null
33
- }
34
- scaler?.stop()
35
- scaler = null
36
- }
37
-
38
- // 处理 ref 初始有值的情况
39
- onMounted(() => {
40
- const el = targetRef?.value ?? targetRef
41
- setup(el)
42
- })
43
-
44
- // 处理 ref 延迟赋值的情况(异步挂载)
45
- if (targetRef && typeof targetRef === 'object' && 'value' in targetRef) {
46
- watch(
47
- () => targetRef.value,
48
- (el) => {
49
- if (el && !scaler) setup(el)
50
- }
51
- )
52
- }
53
-
54
- onUnmounted(() => {
55
- teardown()
56
- })
57
-
58
- const refresh = () => {
59
- const el = targetRef?.value ?? targetRef
60
- if (!el || !scaler) return
61
- const s = scaler.refresh()
62
- scale.value = s
63
- inverseScale.value = getInverseScale(el)
64
- }
65
-
66
- return { scale, inverseScale, refresh }
67
- }
1
+ /**
2
+ * Vue 3 Composable
3
+ * useFitScreen(ref, options) → { scale, inverseScale, refresh }
4
+ */
5
+ import { ref, onMounted, onUnmounted, watch } from 'vue'
6
+ import { createScaler, SCALE_CHANGE_EVENT } from './core.js'
7
+
8
+ export function useFitScreen(targetRef, options = {}) {
9
+ const scale = ref(1)
10
+ const inverseScale = ref(1)
11
+
12
+ let scaler = null
13
+ let onScale = null
14
+
15
+ function setup(el) {
16
+ if (!el) return
17
+ scaler = createScaler(el, options)
18
+ const s = scaler.start()
19
+ scale.value = s
20
+ inverseScale.value = s > 0 ? 1 / s : 1
21
+
22
+ // 图片加载等导致的重算不经过 window.resize,跟页面缩放事件走
23
+ onScale = (event) => {
24
+ if (event.detail?.el !== el) return
25
+ scale.value = event.detail.scale
26
+ inverseScale.value = event.detail.inverseScale
27
+ }
28
+ window.addEventListener(SCALE_CHANGE_EVENT, onScale)
29
+ }
30
+
31
+ function teardown() {
32
+ if (onScale) {
33
+ window.removeEventListener(SCALE_CHANGE_EVENT, onScale)
34
+ onScale = null
35
+ }
36
+ scaler?.stop()
37
+ scaler = null
38
+ }
39
+
40
+ // 处理 ref 初始有值的情况
41
+ onMounted(() => {
42
+ const el = targetRef?.value ?? targetRef
43
+ setup(el)
44
+ })
45
+
46
+ // 处理 ref 延迟赋值的情况(异步挂载)
47
+ if (targetRef && typeof targetRef === 'object' && 'value' in targetRef) {
48
+ watch(
49
+ () => targetRef.value,
50
+ (el) => {
51
+ if (el && !scaler) setup(el)
52
+ }
53
+ )
54
+ }
55
+
56
+ onUnmounted(() => {
57
+ teardown()
58
+ })
59
+
60
+ const refresh = () => {
61
+ const el = targetRef?.value ?? targetRef
62
+ if (!el || !scaler) return
63
+ const s = scaler.refresh()
64
+ scale.value = s
65
+ inverseScale.value = s > 0 ? 1 / s : 1
66
+ }
67
+
68
+ return { scale, inverseScale, refresh }
69
+ }
package/src/core.js CHANGED
@@ -4,8 +4,16 @@
4
4
  * 两种模式:
5
5
  * 'contain' 一屏展示,内容不溢出,用 min(scaleX, scaleY)
6
6
  * 'fixed-width' 固定视觉宽度,内容可滚动,反向补高
7
+ *
8
+ * 配套前提:H5 使用 postcss-px-to-viewport(或同类)做移动端适配。
9
+ * createScaler / 指令默认会注入基础宽高样式,保证测量与缩放生效。
10
+ *
11
+ * 图片等异步内容会改变 offsetHeight:默认用 ResizeObserver / 图片 load
12
+ * 在尺寸稳定后再 refresh,避免首屏偶发「量矮了 → scale 偏大」。
7
13
  */
8
14
 
15
+ import { applyBaseStyles, removeBaseStyles } from './baseStyles.js'
16
+
9
17
  // ---------- helpers ----------
10
18
 
11
19
  function resolveEl(el) {
@@ -27,18 +35,14 @@ export function calculateScale({
27
35
  targetHeight = (typeof window !== 'undefined' ? window.innerHeight : 1080),
28
36
  mode = 'contain',
29
37
  visualWidth = 450, // fixed-width 模式下的目标视觉宽度
30
- screenWidth = (typeof window !== 'undefined' ? window.screen.width : 1920), // fixed-width 模式下的设备屏幕宽度
31
38
  }) {
32
39
  if (!sourceWidth || !sourceHeight || sourceWidth <= 0 || sourceHeight <= 0) return 1
33
40
 
34
41
  let scale
35
42
 
36
43
  if (mode === 'fixed-width') {
37
- if(screenWidth < visualWidth){
38
- scale = 1
39
- }else{
40
- scale = visualWidth / Math.max(targetWidth, screenWidth)
41
- }
44
+ // 只用视口宽:视口比目标宽才缩小,否则 1:1(保持清晰)
45
+ scale = targetWidth > visualWidth ? visualWidth / targetWidth : 1
42
46
  } else {
43
47
  const sx = targetWidth / sourceWidth
44
48
  const sy = targetHeight / sourceHeight
@@ -50,11 +54,23 @@ export function calculateScale({
50
54
 
51
55
  // ---------- apply / read ----------
52
56
 
57
+ /** 页面 scale 变化时抛出,供 ECharts.resize / 业务监听 */
58
+ export const SCALE_CHANGE_EVENT = 'one-screen-scale-change'
59
+
53
60
  export function applyScale(el, scale, origin = 'top center') {
54
61
  const target = resolveEl(el)
55
62
  if (!target) return
56
63
  target.style.transform = `scale(${scale})`
57
64
  target.style.transformOrigin = origin
65
+ // 供业务 / v-compensate 做反缩放(继承到子树)
66
+ const inv = scale > 0 ? 1 / scale : 1
67
+ target.style.setProperty('--one-screen-scale', String(scale))
68
+ target.style.setProperty('--one-screen-inverse-scale', String(inv))
69
+ if (typeof window !== 'undefined') {
70
+ window.dispatchEvent(
71
+ new CustomEvent(SCALE_CHANGE_EVENT, { detail: { scale, inverseScale: inv, el: target } })
72
+ )
73
+ }
58
74
  }
59
75
 
60
76
  export function getPageScale(el) {
@@ -98,18 +114,44 @@ export function createScaler(el, options = {}) {
98
114
  mode = 'contain',
99
115
  visualWidth = 450,
100
116
  origin = 'top center',
101
- autoResize = true
117
+ autoResize = true,
118
+ /** 是否自动注入模式所需的宽高基础样式,默认 true */
119
+ injectBaseStyles = true,
120
+ /**
121
+ * 内容尺寸变化时自动重算(图片 load、字体、DOM 撑高)。
122
+ * 解决刷新偶发「图未加载完就测量 → scale 偏大」。
123
+ */
124
+ observeContent = true,
102
125
  } = options
103
126
 
104
127
  let currentScale = 1
105
- let handler = null
128
+ let resizeHandler = null
129
+ let loadHandler = null
130
+ let windowLoadHandler = null
131
+ let rafId = 0
132
+ let unlockRaf = 0
133
+ let ro = null
134
+ let refreshing = false
135
+ let lastMeasured = { w: 0, h: 0 }
136
+ // start / stop 各加一,用来丢掉停掉之后才回来的字体、帧回调
137
+ let session = 0
138
+
139
+ // fixed-width 会改 inline width/height,stop 时还原
140
+ const savedInline = {
141
+ width: target.style.width,
142
+ height: target.style.height,
143
+ transform: target.style.transform,
144
+ transformOrigin: target.style.transformOrigin,
145
+ }
106
146
 
107
147
  function compute() {
108
148
  const sw = target.offsetWidth
109
149
  const sh = target.offsetHeight
110
- const tw = window.innerWidth
150
+ const tw = document.documentElement.clientWidth || window.innerWidth
111
151
  const th = window.innerHeight
112
152
 
153
+ lastMeasured = { w: sw, h: sh }
154
+
113
155
  return calculateScale({
114
156
  sourceWidth: sw,
115
157
  sourceHeight: sh,
@@ -117,40 +159,159 @@ export function createScaler(el, options = {}) {
117
159
  targetHeight: th,
118
160
  mode,
119
161
  visualWidth,
120
- screenWidth: window.screen.width
121
162
  })
122
163
  }
123
164
 
124
165
  function refresh() {
166
+ refreshing = true
125
167
  const next = compute()
126
- if (next !== currentScale) {
127
- currentScale = next
128
- applyScale(target, next, origin)
129
-
130
- // fixed-width 模式:反向补偿高度
131
- if (mode === 'fixed-width' && next > 0) {
132
- target.style.width = `${Math.max(window.innerWidth, window.screen.width)}px`
133
- target.style.height = `${Math.round(window.innerHeight / next)}px`
134
- }
168
+ currentScale = next
169
+ // 始终写入,保证首次 scale=1 时也带上 transform-origin
170
+ applyScale(target, next, origin)
171
+
172
+ // fixed-width 模式:反向补偿高度(视觉宽固定,布局宽仍用视口宽再缩小)
173
+ if (mode === 'fixed-width' && next > 0) {
174
+ const viewportWidth = document.documentElement.clientWidth || window.innerWidth
175
+ const nextW = `${viewportWidth}px`
176
+ const nextH = `${Math.round(window.innerHeight / next)}px`
177
+ // 值不变不写,避免 ResizeObserver 死循环
178
+ if (target.style.width !== nextW) target.style.width = nextW
179
+ if (target.style.height !== nextH) target.style.height = nextH
135
180
  }
181
+ // 等布局提交后再放开,避免 RO 回调里立刻重入
182
+ const token = session
183
+ cancelAnimationFrame(unlockRaf)
184
+ unlockRaf = requestAnimationFrame(() => {
185
+ if (token !== session) return
186
+ refreshing = false
187
+ })
136
188
  return currentScale
137
189
  }
138
190
 
191
+ function scheduleRefresh() {
192
+ if (refreshing) return
193
+ cancelAnimationFrame(rafId)
194
+ const token = session
195
+ rafId = requestAnimationFrame(() => {
196
+ if (token !== session) return
197
+ // 尺寸没变就跳过(fixed-width 改 height 触发的 RO)
198
+ const w = target.offsetWidth
199
+ const h = target.offsetHeight
200
+ if (w === lastMeasured.w && h === lastMeasured.h && target.style.transform) {
201
+ // 视口变化仍需算:由 window.resize 路径直接 refresh
202
+ return
203
+ }
204
+ refresh()
205
+ })
206
+ }
207
+
208
+ function onWindowResize() {
209
+ refresh()
210
+ }
211
+
139
212
  function start() {
213
+ session += 1
214
+ const token = session
215
+ // 复用同一实例 start 时,上一次 stop 之后留下的锁要清掉
216
+ refreshing = false
217
+ if (injectBaseStyles) {
218
+ applyBaseStyles(target, mode)
219
+ }
220
+ // 注入样式后再量尺寸
140
221
  refresh()
222
+
141
223
  if (autoResize) {
142
- handler = () => refresh()
143
- window.addEventListener('resize', handler)
224
+ resizeHandler = onWindowResize
225
+ window.addEventListener('resize', resizeHandler)
144
226
  }
227
+
228
+ if (observeContent && typeof window !== 'undefined') {
229
+ // 1) 子树布局尺寸变化(图片解码后撑高最常见)
230
+ if (typeof ResizeObserver !== 'undefined') {
231
+ ro = new ResizeObserver(() => scheduleRefresh())
232
+ ro.observe(target)
233
+ }
234
+
235
+ // 2) 捕获阶段听内部 img/video 的 load(含后插入的节点)
236
+ loadHandler = (e) => {
237
+ const t = e.target
238
+ if (t && (t.tagName === 'IMG' || t.tagName === 'VIDEO' || t.tagName === 'IFRAME')) {
239
+ scheduleRefresh()
240
+ }
241
+ }
242
+ target.addEventListener('load', loadHandler, true)
243
+ target.addEventListener('error', loadHandler, true)
244
+
245
+ // 3) 字体就绪。stop 之后 session 变了,这次回调直接丢掉
246
+ if (document.fonts && document.fonts.ready) {
247
+ document.fonts.ready
248
+ .then(() => {
249
+ if (token !== session) return
250
+ scheduleRefresh()
251
+ })
252
+ .catch(() => {})
253
+ }
254
+
255
+ // 4) 整页资源 load(缓存命中时可能已 complete)
256
+ if (document.readyState === 'complete') {
257
+ scheduleRefresh()
258
+ } else {
259
+ windowLoadHandler = () => scheduleRefresh()
260
+ window.addEventListener('load', windowLoadHandler, { once: true })
261
+ }
262
+
263
+ // 5) 再等两帧,覆盖首屏同步布局抖动
264
+ requestAnimationFrame(() => {
265
+ requestAnimationFrame(() => scheduleRefresh())
266
+ })
267
+ }
268
+
145
269
  return currentScale
146
270
  }
147
271
 
148
272
  function stop() {
149
- if (handler) {
150
- window.removeEventListener('resize', handler)
151
- handler = null
273
+ session += 1
274
+ if (resizeHandler) {
275
+ window.removeEventListener('resize', resizeHandler)
276
+ resizeHandler = null
277
+ }
278
+ if (loadHandler) {
279
+ target.removeEventListener('load', loadHandler, true)
280
+ target.removeEventListener('error', loadHandler, true)
281
+ loadHandler = null
282
+ }
283
+ if (windowLoadHandler) {
284
+ window.removeEventListener('load', windowLoadHandler)
285
+ windowLoadHandler = null
152
286
  }
287
+ if (ro) {
288
+ ro.disconnect()
289
+ ro = null
290
+ }
291
+ cancelAnimationFrame(rafId)
292
+ cancelAnimationFrame(unlockRaf)
293
+ rafId = 0
294
+ unlockRaf = 0
295
+
296
+ if (injectBaseStyles) {
297
+ removeBaseStyles(target)
298
+ }
299
+ target.style.width = savedInline.width
300
+ target.style.height = savedInline.height
301
+ target.style.transform = savedInline.transform
302
+ target.style.transformOrigin = savedInline.transformOrigin
303
+ target.style.removeProperty('--one-screen-scale')
304
+ target.style.removeProperty('--one-screen-inverse-scale')
153
305
  }
154
306
 
155
307
  return { start, stop, refresh, getScale: () => currentScale }
156
308
  }
309
+
310
+ export {
311
+ applyBaseStyles,
312
+ removeBaseStyles,
313
+ applyCompensateStyles,
314
+ removeCompensateStyles,
315
+ ensureBaseStyleSheet,
316
+ BASE_CLASS,
317
+ } from './baseStyles.js'
package/src/directives.js CHANGED
@@ -1,34 +1,144 @@
1
1
  /**
2
2
  * Vue 3 指令
3
- * v-fit-screen 页面级缩放
4
- * v-compensate 组件级反缩放补偿
3
+ * v-fit-screen 页面级缩放
4
+ * v-compensate 组件级反缩放补偿(ECharts / Element Plus 等)
5
5
  */
6
6
  import { createScaler } from './core.js'
7
+ import { createCompensate } from './compensate.js'
8
+
9
+ function readOptions(binding) {
10
+ return binding.value && typeof binding.value === 'object' ? binding.value : {}
11
+ }
12
+
13
+ function mountScaler(el, binding) {
14
+ const options = readOptions(binding)
15
+ const scaler = createScaler(el, options)
16
+ scaler.start()
17
+ el.__fitScreenScaler = scaler
18
+ el.__fitScreenOptionsKey = JSON.stringify({
19
+ mode: options.mode || 'contain',
20
+ visualWidth: options.visualWidth,
21
+ origin: options.origin,
22
+ autoResize: options.autoResize,
23
+ injectBaseStyles: options.injectBaseStyles,
24
+ observeContent: options.observeContent,
25
+ })
26
+ }
27
+
28
+ function unmountScaler(el) {
29
+ const scaler = el.__fitScreenScaler
30
+ if (scaler) {
31
+ scaler.stop()
32
+ delete el.__fitScreenScaler
33
+ delete el.__fitScreenOptionsKey
34
+ }
35
+ }
7
36
 
8
37
  // ---- v-fit-screen ----
9
38
 
10
39
  export const vFitScreen = {
11
40
  mounted(el, binding) {
12
- const options = binding.value || {}
13
- const scaler = createScaler(el, options)
14
- scaler.start()
15
- el.__fitScreenScaler = scaler
41
+ mountScaler(el, binding)
16
42
  },
17
43
 
18
44
  updated(el, binding) {
19
- const scaler = el.__fitScreenScaler
20
- if (scaler) {
21
- const options = binding.value || {}
22
- // 简单处理:重新 refresh
23
- scaler.refresh()
45
+ const options = readOptions(binding)
46
+ const key = JSON.stringify({
47
+ mode: options.mode || 'contain',
48
+ visualWidth: options.visualWidth,
49
+ origin: options.origin,
50
+ autoResize: options.autoResize,
51
+ injectBaseStyles: options.injectBaseStyles,
52
+ observeContent: options.observeContent,
53
+ })
54
+
55
+ if (el.__fitScreenOptionsKey !== key) {
56
+ unmountScaler(el)
57
+ mountScaler(el, binding)
58
+ return
24
59
  }
60
+
61
+ el.__fitScreenScaler?.refresh()
25
62
  },
26
63
 
27
64
  unmounted(el) {
28
- const scaler = el.__fitScreenScaler
29
- if (scaler) {
30
- scaler.stop()
31
- delete el.__fitScreenScaler
32
- }
65
+ unmountScaler(el)
66
+ },
67
+ }
68
+
69
+ function readCompensateOptions(binding) {
70
+ const value = binding?.value
71
+ const modifiers = binding?.modifiers || {}
72
+ const options = value && typeof value === 'object' ? { ...value } : {}
73
+ // v-compensate.resize-canvas / .resizeCanvas / 旧名 .echarts
74
+ if (
75
+ modifiers.resizeCanvas ||
76
+ modifiers['resize-canvas'] ||
77
+ modifiers.echarts ||
78
+ value === 'resize-canvas' ||
79
+ value === 'resizeCanvas' ||
80
+ value === 'echarts'
81
+ ) {
82
+ options.resizeCanvas = options.resizeCanvas || true
33
83
  }
84
+ return options
85
+ }
86
+
87
+ function compensateOptionsKey(options) {
88
+ return JSON.stringify({
89
+ resizeCanvas: Boolean(options.resizeCanvas || options.echarts || options.echartsLib),
90
+ hasLib: Boolean(
91
+ (options.echarts && typeof options.echarts === 'object') || options.echartsLib
92
+ ),
93
+ hasOnRefresh: typeof options.onRefresh === 'function',
94
+ })
34
95
  }
96
+
97
+ /**
98
+ * v-compensate — 挂在「被页面 scale 搞坏」的第三方组件外包一层。
99
+ *
100
+ * 原理与业务 WinRateChart 手写一致(JS 行内,非纯 CSS):
101
+ * transform: scale(inverse); width/height: 100% * pageScale; origin: top left
102
+ *
103
+ * 用法:
104
+ * <div class="slot"><div v-compensate><Whatever /></div></div>
105
+ * <div class="slot"><div v-compensate.resize-canvas><Chart /></div></div>
106
+ *
107
+ * `.resize-canvas` / `{ resizeCanvas: true }`:补偿后通知子树 canvas 重绘
108
+ * (探测 ECharts / Chart.js,并派发 one-screen-scale-canvas-resize)。
109
+ */
110
+ function mountCompensate(el) {
111
+ const api = createCompensate(el, () => el.__oneScreenCompensateOptions || {})
112
+ el.__oneScreenCompensateApi = api
113
+ el.__oneScreenCompensateKey = compensateOptionsKey(el.__oneScreenCompensateOptions || {})
114
+ }
115
+
116
+ export const vCompensate = {
117
+ mounted(el, binding) {
118
+ el.__oneScreenCompensateOptions = readCompensateOptions(binding)
119
+ mountCompensate(el)
120
+ },
121
+
122
+ updated(el, binding) {
123
+ const options = readCompensateOptions(binding)
124
+ const key = compensateOptionsKey(options)
125
+ el.__oneScreenCompensateOptions = options
126
+ // resizeCanvas / 图表库变了才重建监听;onRefresh 每次 refresh 现读
127
+ if (el.__oneScreenCompensateKey !== key) {
128
+ el.__oneScreenCompensateApi?.stop()
129
+ mountCompensate(el)
130
+ return
131
+ }
132
+ el.__oneScreenCompensateApi?.refresh()
133
+ },
134
+
135
+ unmounted(el) {
136
+ el.__oneScreenCompensateApi?.stop()
137
+ delete el.__oneScreenCompensateApi
138
+ delete el.__oneScreenCompensateKey
139
+ delete el.__oneScreenCompensateOptions
140
+ },
141
+ }
142
+
143
+ /** 别名,与早期注释里的命名一致 */
144
+ export const vFitCompensate = vCompensate
package/src/index.d.ts CHANGED
@@ -7,7 +7,6 @@ export interface ScaleOptions {
7
7
  targetHeight?: number
8
8
  mode?: 'contain' | 'fixed-width'
9
9
  visualWidth?: number
10
- screenWidth?: number
11
10
  }
12
11
 
13
12
  export interface ScalerOptions {
@@ -15,6 +14,17 @@ export interface ScalerOptions {
15
14
  visualWidth?: number
16
15
  origin?: string
17
16
  autoResize?: boolean
17
+ /**
18
+ * 是否自动注入模式所需基础样式(默认 true)
19
+ * - contain: width:100vw; min-height:100vh/100dvh
20
+ * - fixed-width: 暂不注入(宽高由 refresh 行内补偿)
21
+ */
22
+ injectBaseStyles?: boolean
23
+ /**
24
+ * 内容尺寸变化时自动重算(默认 true)。
25
+ * 监听 ResizeObserver / 内部图片 load 等,避免图未加载完就测量导致 scale 偏大。
26
+ */
27
+ observeContent?: boolean
18
28
  }
19
29
 
20
30
  export interface ScalerInstance {
@@ -24,17 +34,83 @@ export interface ScalerInstance {
24
34
  getScale: () => number
25
35
  }
26
36
 
37
+ export const BASE_CLASS: {
38
+ contain: string
39
+ 'fixed-width': string
40
+ compensate: string
41
+ }
42
+
43
+ export const SCALE_CHANGE_EVENT: 'one-screen-scale-change'
44
+ export const CANVAS_RESIZE_EVENT: 'one-screen-scale-canvas-resize'
45
+
27
46
  export function calculateScale(options: ScaleOptions): number
28
47
  export function applyScale(el: HTMLElement | string, scale: number, origin?: string): void
29
48
  export function getPageScale(el?: HTMLElement | string): number
30
49
  export function getInverseScale(el?: HTMLElement | string): number
31
50
  export function createScaler(el: HTMLElement | string, options?: ScalerOptions): ScalerInstance
51
+ export function ensureBaseStyleSheet(): HTMLStyleElement | null
52
+ export function applyBaseStyles(el: HTMLElement, mode?: 'contain' | 'fixed-width'): string | null
53
+ export function removeBaseStyles(el: HTMLElement): void
54
+ export function applyCompensateStyles(el: HTMLElement): string | null
55
+ export function removeCompensateStyles(el: HTMLElement): void
56
+
57
+ // ===== 组件反缩放补偿(仅抵消页面 v-fit-screen 的 scale;不处理浏览器 Ctrl+/-) =====
58
+
59
+ export interface CompensateRefreshDetail {
60
+ scale: number
61
+ inverseScale: number
62
+ el: HTMLElement
63
+ }
64
+
65
+ export interface CompensateOptions {
66
+ /**
67
+ * 补偿后通知子树 canvas 重绘:探测 ECharts / Chart.js,
68
+ * 并对每个 &lt;canvas&gt; 派发 CANVAS_RESIZE_EVENT。
69
+ */
70
+ resizeCanvas?: boolean
71
+ /** @deprecated 请用 resizeCanvas;传入 echarts 模块时仍表示开启 + 注入库 */
72
+ echarts?: boolean | { getInstanceByDom?: (el: Element) => { resize: (opts?: object) => void } | undefined }
73
+ echartsLib?: { getInstanceByDom?: (el: Element) => { resize: (opts?: object) => void } | undefined }
74
+ onRefresh?: (detail: CompensateRefreshDetail) => void
75
+ }
76
+
77
+ export interface CompensateInstance {
78
+ refresh: () => void
79
+ stop: () => void
80
+ }
81
+
82
+ export function resolvePageScale(fromEl?: HTMLElement | null): number
83
+ export function applyCompensateTransform(el: HTMLElement): {
84
+ scale: number
85
+ inverseScale: number
86
+ }
87
+ export function clearCompensateTransform(el: HTMLElement): void
88
+ export function resizeCanvasUnder(el: HTMLElement, echartsLib?: CompensateOptions['echartsLib']): number
89
+ /** @deprecated 请用 resizeCanvasUnder */
90
+ export function resizeEchartsUnder(el: HTMLElement, echartsLib?: CompensateOptions['echartsLib']): number
91
+ export function createCompensate(
92
+ el: HTMLElement,
93
+ options?: CompensateOptions | (() => CompensateOptions)
94
+ ): CompensateInstance
95
+ /** 全局注入 echarts,供 resizeCanvas 探测(无需每个调用传模块) */
96
+ export function setCompensateEcharts(echarts: CompensateOptions['echartsLib'] | null): void
32
97
 
33
98
  // ===== Vue =====
34
99
 
35
100
  import type { Ref, Directive } from 'vue'
36
101
 
37
- export const vFitScreen: Directive<HTMLElement, ScalerOptions>
102
+ export const vFitScreen: Directive<HTMLElement, ScalerOptions | undefined>
103
+ /**
104
+ * 第三方组件反缩放。修饰符 `.resize-canvas` 或 `{ resizeCanvas: true }` 通知子树 canvas 重绘。
105
+ */
106
+ export const vCompensate: Directive<
107
+ HTMLElement,
108
+ CompensateOptions | 'resize-canvas' | 'resizeCanvas' | 'echarts' | undefined
109
+ >
110
+ export const vFitCompensate: Directive<
111
+ HTMLElement,
112
+ CompensateOptions | 'resize-canvas' | 'resizeCanvas' | 'echarts' | undefined
113
+ >
38
114
 
39
115
  export function useFitScreen(
40
116
  targetRef: Ref<HTMLElement | null> | HTMLElement,