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.
- package/LICENSE +21 -21
- package/README.md +181 -38
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +78 -2
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/react.cjs.js +1 -1
- package/dist/react.cjs.js.map +1 -1
- package/dist/react.d.ts +30 -0
- package/dist/react.esm.js +1 -1
- package/dist/react.esm.js.map +1 -1
- package/package.json +2 -2
- package/src/baseStyles.js +91 -0
- package/src/compensate.js +320 -0
- package/src/composables.js +69 -67
- package/src/core.js +185 -24
- package/src/directives.js +126 -16
- package/src/index.d.ts +78 -2
- package/src/index.js +21 -2
- package/src/react.d.ts +30 -0
- package/src/react.js +110 -17
package/src/composables.js
CHANGED
|
@@ -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,
|
|
7
|
-
|
|
8
|
-
export function useFitScreen(targetRef, options = {}) {
|
|
9
|
-
const scale = ref(1)
|
|
10
|
-
const inverseScale = ref(1)
|
|
11
|
-
|
|
12
|
-
let scaler = null
|
|
13
|
-
let
|
|
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 =
|
|
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
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
143
|
-
window.addEventListener('resize',
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
* 并对每个 <canvas> 派发 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,
|