one-screen-scale 1.1.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/README.md +323 -186
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +122 -46
- 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 +49 -19
- package/dist/react.esm.js +1 -1
- package/dist/react.esm.js.map +1 -1
- package/package.json +71 -71
- package/src/baseStyles.js +91 -0
- package/src/compensate.js +320 -0
- package/src/composables.js +13 -11
- package/src/core.js +317 -160
- package/src/directives.js +144 -34
- package/src/index.d.ts +122 -46
- package/src/index.js +33 -14
- package/src/react.d.ts +49 -19
- package/src/react.js +139 -46
package/README.md
CHANGED
|
@@ -1,186 +1,323 @@
|
|
|
1
|
-
# one-screen-scale
|
|
2
|
-
|
|
3
|
-
基于 CSS `transform: scale()` 的一屏自适应方案。框架无关核心 + Vue 3 / React 开箱即用。
|
|
4
|
-
|
|
5
|
-
## 为什么用这个
|
|
6
|
-
|
|
7
|
-
- 🎯 **真正的"一屏"** — 内容不溢出、不滚动(contain 模式)
|
|
8
|
-
- 🧩 **零依赖核心** — 纯 JS,不绑定任何框架
|
|
9
|
-
- ⚡ **指令 / Hook 开箱即用** — Vue 3 和 React
|
|
10
|
-
- 🖥️ **Canvas 友好** — 通过反缩放补偿,ECharts 等 canvas 内容保持清晰
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
1
|
+
# one-screen-scale
|
|
2
|
+
|
|
3
|
+
基于 CSS `transform: scale()` 的一屏自适应方案。框架无关核心 + Vue 3 / React 开箱即用。
|
|
4
|
+
|
|
5
|
+
## 为什么用这个
|
|
6
|
+
|
|
7
|
+
- 🎯 **真正的"一屏"** — 内容不溢出、不滚动(contain 模式)
|
|
8
|
+
- 🧩 **零依赖核心** — 纯 JS,不绑定任何框架
|
|
9
|
+
- ⚡ **指令 / Hook 开箱即用** — Vue 3 和 React 都支持;挂载时自动注入基础宽高样式
|
|
10
|
+
- 🖥️ **Canvas 友好** — 通过反缩放补偿,ECharts 等 canvas 内容保持清晰
|
|
11
|
+
|
|
12
|
+
## 主要面向场景
|
|
13
|
+
|
|
14
|
+
面向已经用 **postcss-px-to-viewport**(或同类方案)做移动端适配的 H5 页面:
|
|
15
|
+
|
|
16
|
+
| 模式 | 用途 | 效果 |
|
|
17
|
+
|------|------|------|
|
|
18
|
+
| `contain`(默认) | 活动页 / 一屏落地页 | 任意设备完整展示一屏;高屏左右留白;极小视口仍能看到完整页;**PC 与手机看到的是同一张 H5** |
|
|
19
|
+
| `fixed-width` | 规则页 / 长表单 | 固定视觉宽度,内容可纵向滚动 |
|
|
20
|
+
|
|
21
|
+
不必再单独开发一套 PC 页。
|
|
22
|
+
|
|
23
|
+
## 页面怎么配合
|
|
24
|
+
|
|
25
|
+
指令会**自动注入**下列基础样式(可用 `injectBaseStyles: false` 关闭):
|
|
26
|
+
|
|
27
|
+
**contain — 一屏**
|
|
28
|
+
|
|
29
|
+
```css
|
|
30
|
+
width: 100vw;
|
|
31
|
+
min-height: 100vh;
|
|
32
|
+
min-height: 100dvh;
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
算法用元素的 `offsetWidth / offsetHeight` 和视口比:`scale = min(视口宽/内容宽, 视口高/内容高)`。
|
|
36
|
+
所以 **内容高度必须能被正确量出来**,PC 上(px→vw 后)通常会高于视口,scale 才会小于 1。
|
|
37
|
+
|
|
38
|
+
### ⚠️ contain 使用禁忌(否则 PC 看起来像「没缩放」)
|
|
39
|
+
|
|
40
|
+
1. **禁止用 `height: 100vh` / `100dvh` 锁死根节点高度**
|
|
41
|
+
只能用插件注入的 **`min-height`**。写成 `height: 100vh` 后,`offsetHeight ≈ 视口高` → `scaleY ≈ 1` → 整页几乎不缩。
|
|
42
|
+
|
|
43
|
+
2. **禁止把撑高页面的大图 / 主视觉设为 `position: absolute`(或脱离文档流)**
|
|
44
|
+
绝对定位不参与父级高度计算。常见错误:棋盘/海报 `position: absolute` + 外层 `flex: 1; min-height: 0` 去「填满一屏」→ 根节点高度仍被锁在视口上。
|
|
45
|
+
**正确做法:大图留在文档流里**(`position: relative` / 默认 static,`width: 100%; height: auto`),用图片自身比例撑开整页高度。
|
|
46
|
+
|
|
47
|
+
3. **避免「整页 flex 填视口、子项 `flex:1; min-height:0`」当唯一高度来源**
|
|
48
|
+
那会把布局收成刚好一屏,同样量不出「内容高于视口」。一屏裁切用 `overflow: hidden` 即可,高度仍交给内容。
|
|
49
|
+
|
|
50
|
+
**对照**
|
|
51
|
+
|
|
52
|
+
| 写法 | 结果 |
|
|
53
|
+
|------|------|
|
|
54
|
+
| `min-height: 100vh` + 大图在文档流 | ✅ PC/手机构图一致,contain 正常缩小 |
|
|
55
|
+
| `height: 100vh` 锁死 | ❌ PC scale≈1 |
|
|
56
|
+
| 大图 `absolute` + flex 填满视口 | ❌ 量不到真实内容高,PC 不缩 |
|
|
57
|
+
|
|
58
|
+
图片异步加载导致偶发量矮:插件默认 `observeContent: true`,会在尺寸变化后自动 `refresh`。
|
|
59
|
+
|
|
60
|
+
**fixed-width — 长滚动**
|
|
61
|
+
|
|
62
|
+
暂不自动注入基础 CSS;`width` / `height` 由 `refresh()` 行内补偿写入。业务自己管内部滚动等。
|
|
63
|
+
滚动条被 scale 变细时,可用根节点上的 `--one-screen-inverse-scale` 做反缩放,例如:
|
|
64
|
+
|
|
65
|
+
```css
|
|
66
|
+
/* 若项目用了 px-to-viewport,这里要用大写 Px,避免被转成 vw 后再 ×反缩放过粗 */
|
|
67
|
+
.scroll::-webkit-scrollbar {
|
|
68
|
+
width: calc(6Px * var(--one-screen-inverse-scale, 1));
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## 安装
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm install one-screen-scale
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## 快速开始
|
|
79
|
+
|
|
80
|
+
### Vue 3 — 指令(最简)
|
|
81
|
+
|
|
82
|
+
```vue
|
|
83
|
+
<template>
|
|
84
|
+
<!-- 一屏:自动注入 min-height:100vh + width:100vw -->
|
|
85
|
+
<div v-fit-screen class="page">
|
|
86
|
+
<h1>活动标题</h1>
|
|
87
|
+
<p>页面内容...</p>
|
|
88
|
+
</div>
|
|
89
|
+
</template>
|
|
90
|
+
|
|
91
|
+
<script setup>
|
|
92
|
+
import { vFitScreen } from 'one-screen-scale'
|
|
93
|
+
</script>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```vue
|
|
97
|
+
<template>
|
|
98
|
+
<!-- 长滚动 -->
|
|
99
|
+
<div v-fit-screen="{ mode: 'fixed-width' }" class="rules-page">
|
|
100
|
+
<header>...</header>
|
|
101
|
+
<main class="rules-body"><!-- overflow-y: auto --></main>
|
|
102
|
+
</div>
|
|
103
|
+
</template>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Vue 3 — `v-compensate`(第三方组件反缩放)
|
|
107
|
+
|
|
108
|
+
页面 `transform: scale` 后,ECharts 等常会尺寸偏小或量错。在槽位内再包一层即可(**JS 行内**写入):
|
|
109
|
+
|
|
110
|
+
`width/height: 100% * pageScale` + `transform: scale(inverse)`,scale 读祖先 `--one-screen-scale`。
|
|
111
|
+
|
|
112
|
+
```vue
|
|
113
|
+
<template>
|
|
114
|
+
<div v-fit-screen class="page">
|
|
115
|
+
<div class="chart-slot">
|
|
116
|
+
<div v-compensate.resize-canvas class="chart-inner">
|
|
117
|
+
<div ref="chartEl" class="chart"></div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</template>
|
|
122
|
+
|
|
123
|
+
<script setup>
|
|
124
|
+
import * as echarts from 'echarts'
|
|
125
|
+
import { vFitScreen, vCompensate, setCompensateEcharts } from 'one-screen-scale'
|
|
126
|
+
|
|
127
|
+
// 可选:全局注入 echarts,供 resizeCanvas 自动探测
|
|
128
|
+
setCompensateEcharts(echarts)
|
|
129
|
+
</script>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
```css
|
|
133
|
+
.chart-slot { width: 100%; height: 300px; overflow: hidden; } /* 设计稿槽位,需明确宽高 */
|
|
134
|
+
.chart-inner, .chart { width: 100%; height: 100%; }
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
> **适用**:任意被页面 scale 搞小的区域,尤其 **canvas**——先反缩放,再用 `offsetWidth` 量盒(勿用 `getBoundingClientRect`)。
|
|
138
|
+
> **`{ resizeCanvas: true }` / `.resize-canvas`**:补偿后通知子树 canvas 重绘(探测 ECharts / Chart.js,并在每个 `<canvas>` 上派发 `one-screen-scale-canvas-resize`)。
|
|
139
|
+
> 只抵消页面 `v-fit-screen` 的 scale;**不处理**浏览器 Ctrl+/-。
|
|
140
|
+
> **不适用**:`el-slider` 等依赖指针坐标 / 易被 overflow 裁切的控件。
|
|
141
|
+
|
|
142
|
+
### Vue 3 — Composable(需要拿到 scale 数值时)
|
|
143
|
+
|
|
144
|
+
```vue
|
|
145
|
+
<script setup>
|
|
146
|
+
import { ref } from 'vue'
|
|
147
|
+
import { useFitScreen } from 'one-screen-scale'
|
|
148
|
+
|
|
149
|
+
const pageRef = ref(null)
|
|
150
|
+
const { scale, inverseScale } = useFitScreen(pageRef)
|
|
151
|
+
</script>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### React
|
|
155
|
+
|
|
156
|
+
```jsx
|
|
157
|
+
import { useRef } from 'react'
|
|
158
|
+
import { useFitScreen, useCompensate } from 'one-screen-scale/react'
|
|
159
|
+
|
|
160
|
+
function App() {
|
|
161
|
+
const pageRef = useRef(null)
|
|
162
|
+
const chartInnerRef = useRef(null)
|
|
163
|
+
const { scale, inverseScale } = useFitScreen(pageRef)
|
|
164
|
+
// 需要通知子树 canvas 重绘时加 { resizeCanvas: true }
|
|
165
|
+
useCompensate(chartInnerRef, { resizeCanvas: true })
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<div ref={pageRef} className="page">
|
|
169
|
+
<div className="chart-slot" style={{ width: 350, height: 300 }}>
|
|
170
|
+
<div ref={chartInnerRef} style={{ width: '100%', height: '100%' }}>
|
|
171
|
+
<div id="chart" style={{ width: '100%', height: '100%' }} />
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### 纯 JS / HTML
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
import { createScaler, createCompensate } from 'one-screen-scale'
|
|
183
|
+
|
|
184
|
+
const page = document.querySelector('.page')
|
|
185
|
+
const scaler = createScaler(page, { mode: 'contain' })
|
|
186
|
+
scaler.start()
|
|
187
|
+
|
|
188
|
+
// 第三方组件反缩放(对应 Vue v-compensate / React useCompensate)
|
|
189
|
+
const inner = document.querySelector('.chart-inner')
|
|
190
|
+
const compensate = createCompensate(inner, { resizeCanvas: true })
|
|
191
|
+
|
|
192
|
+
// 销毁
|
|
193
|
+
compensate.stop()
|
|
194
|
+
scaler.stop() // 移除注入的 class,并还原本库写过的 inline 样式
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## 两种模式
|
|
198
|
+
|
|
199
|
+
| 模式 | 算法 | 效果 | 场景 |
|
|
200
|
+
|------|------|------|------|
|
|
201
|
+
| `contain`(默认) | `min(scaleX, scaleY)` | 内容完整可见,不溢出 | 活动页、落地页 |
|
|
202
|
+
| `fixed-width` | `visualWidth / innerWidth`(视口 ≤ visualWidth 时为 1) | 固定手机观感宽度,可滚动 | 规则页、长表单 |
|
|
203
|
+
|
|
204
|
+
```js
|
|
205
|
+
// contain(默认)
|
|
206
|
+
createScaler(el)
|
|
207
|
+
|
|
208
|
+
// fixed-width
|
|
209
|
+
createScaler(el, { mode: 'fixed-width', visualWidth: 450 })
|
|
210
|
+
|
|
211
|
+
// 自己写根节点宽高时关闭注入
|
|
212
|
+
createScaler(el, { injectBaseStyles: false })
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## API
|
|
216
|
+
|
|
217
|
+
### 核心函数(框架无关)
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
calculateScale(options: ScaleOptions): number
|
|
221
|
+
|
|
222
|
+
applyScale(el: Element | string, scale: number, origin?: string): void
|
|
223
|
+
|
|
224
|
+
getPageScale(el?: Element | string): number
|
|
225
|
+
getInverseScale(el?: Element | string): number
|
|
226
|
+
|
|
227
|
+
createScaler(el: Element | string, options?: ScalerOptions): ScalerInstance
|
|
228
|
+
|
|
229
|
+
ensureBaseStyleSheet(): HTMLStyleElement | null
|
|
230
|
+
applyBaseStyles(el: HTMLElement, mode?: 'contain' | 'fixed-width'): string | null
|
|
231
|
+
removeBaseStyles(el: HTMLElement): void
|
|
232
|
+
applyCompensateStyles(el: HTMLElement): string | null
|
|
233
|
+
removeCompensateStyles(el: HTMLElement): void
|
|
234
|
+
|
|
235
|
+
SCALE_CHANGE_EVENT // 'one-screen-scale-change',页面 scale 变化时 window 上触发
|
|
236
|
+
|
|
237
|
+
// 组件反缩放(仅抵消页面 scale)
|
|
238
|
+
resolvePageScale(fromEl?): number
|
|
239
|
+
applyCompensateTransform(el): { scale, inverseScale }
|
|
240
|
+
clearCompensateTransform(el): void
|
|
241
|
+
createCompensate(el, options?: CompensateOptions): { refresh, stop }
|
|
242
|
+
resizeCanvasUnder(el, echartsLib?): number
|
|
243
|
+
setCompensateEcharts(echarts): void // 可选,供 resizeCanvas 探测 ECharts
|
|
244
|
+
CANVAS_RESIZE_EVENT // 'one-screen-scale-canvas-resize'
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### ScalerInstance
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
{
|
|
251
|
+
start(): number // 注入基础样式 + 启动缩放 + 监听 resize
|
|
252
|
+
stop(): void // 停止监听,移除注入 class,还原 inline
|
|
253
|
+
refresh(): number // 手动重算
|
|
254
|
+
getScale(): number
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### ScalerOptions
|
|
259
|
+
|
|
260
|
+
| 参数 | 类型 | 默认值 | 说明 |
|
|
261
|
+
|------|------|--------|------|
|
|
262
|
+
| `mode` | `'contain' \| 'fixed-width'` | `'contain'` | 缩放模式 |
|
|
263
|
+
| `visualWidth` | `number` | `450` | fixed-width 模式目标视觉宽度 |
|
|
264
|
+
| `origin` | `string` | `'top center'` | transform-origin |
|
|
265
|
+
| `autoResize` | `boolean` | `true` | 自动监听窗口 resize |
|
|
266
|
+
| `injectBaseStyles` | `boolean` | `true` | 是否自动注入模式所需宽高样式 |
|
|
267
|
+
| `observeContent` | `boolean` | `true` | 内容尺寸变化(如图片加载撑高)时自动重算,避免偶发 scale 偏大 |
|
|
268
|
+
|
|
269
|
+
### Vue
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
vFitScreen: Directive<HTMLElement, ScalerOptions>
|
|
273
|
+
vCompensate: Directive<HTMLElement> // 第三方反缩放
|
|
274
|
+
vFitCompensate: Directive<HTMLElement> // 同上别名
|
|
275
|
+
|
|
276
|
+
useFitScreen(ref: Ref, options?: ScalerOptions): {
|
|
277
|
+
scale: Ref<number>
|
|
278
|
+
inverseScale: Ref<number>
|
|
279
|
+
refresh: () => void
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### React(`one-screen-scale/react`)
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
useFitScreen(ref, options?: ScalerOptions): { scale, inverseScale, refresh }
|
|
287
|
+
useCompensate(ref, options?: CompensateOptions): { refresh } // 对应 v-compensate
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### CompensateOptions
|
|
291
|
+
|
|
292
|
+
| 参数 | 类型 | 默认值 | 说明 |
|
|
293
|
+
|------|------|--------|------|
|
|
294
|
+
| `resizeCanvas` | `boolean` | — | **可选**。补偿后通知子树 canvas 重绘 |
|
|
295
|
+
| `onRefresh` | `(detail) => void` | — | 每次写入反缩放后回调 |
|
|
296
|
+
| `echarts` / `echartsLib` | — | — | 旧名,等价于开启 `resizeCanvas`;对象形式可传 echarts 模块 |
|
|
297
|
+
|
|
298
|
+
也可 `setCompensateEcharts(echarts)` 全局注入,方便自动探测。
|
|
299
|
+
|
|
300
|
+
## Canvas 模糊 / 偏小
|
|
301
|
+
|
|
302
|
+
CSS `transform: scale()` 会把 **所有 canvas** 一起缩小。先反缩放,再重绘:
|
|
303
|
+
|
|
304
|
+
| 环境 | 反缩放 | 通知 canvas 重绘 |
|
|
305
|
+
|------|--------|------------------|
|
|
306
|
+
| Vue | `v-compensate` | `v-compensate.resize-canvas` |
|
|
307
|
+
| React | `useCompensate(ref)` | `useCompensate(ref, { resizeCanvas: true })` |
|
|
308
|
+
| 纯 JS | `createCompensate(el)` | `createCompensate(el, { resizeCanvas: true })` |
|
|
309
|
+
|
|
310
|
+
`resizeCanvas` 会:探测已加载的 ECharts / Chart.js;并在每个 `<canvas>` 上派发事件,自研库可监听:
|
|
311
|
+
|
|
312
|
+
```js
|
|
313
|
+
canvas.addEventListener('one-screen-scale-canvas-resize', (e) => {
|
|
314
|
+
const { width, height } = e.detail
|
|
315
|
+
myChart.resize(width, height)
|
|
316
|
+
})
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
手写等价:`width/height: 100% * pageScale` + `transform: scale(1/pageScale)`,量尺寸用 `offsetWidth`。
|
|
320
|
+
|
|
321
|
+
## License
|
|
322
|
+
|
|
323
|
+
MIT
|
package/dist/index.cjs.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var e=require("vue");function t(e){return e?"string"==typeof e?document.querySelector(e):e instanceof HTMLElement?e:e.value instanceof HTMLElement?e.value:e.current instanceof HTMLElement?e.current:null:null}function n({sourceWidth:e,sourceHeight:t,targetWidth:n=("undefined"!=typeof window?window.innerWidth:1920),targetHeight:r=("undefined"!=typeof window?window.innerHeight:1080),mode:i="contain",visualWidth:o=450}){if(!e||!t||e<=0||t<=0)return 1;let s;if("fixed-width"===i)s=n>o?o/n:1;else{const i=n/e,o=r/t;s=Math.min(i,o)}return s}function r(e,n,r="top center"){const i=t(e);i&&(i.style.transform=`scale(${n})`,i.style.transformOrigin=r)}function i(e){const n=t(e);if(!n)return 1;const r=window.getComputedStyle(n).transform;if(!r||"none"===r)return 1;const i=r.match(/matrix\(([^,]+),\s*[^,]+,\s*[^,]+,\s*([^,]+)/);if(i)return(parseFloat(i[1])+parseFloat(i[2]))/2;const o=r.match(/matrix3d\(([^,]+),[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,([^,]+)\)/);if(o)return(parseFloat(o[1])+parseFloat(o[2]))/2;const s=r.match(/scale\(([^)]+)\)/);if(s){const e=s[1].split(",").map(e=>parseFloat(e.trim()));return e.length>=2?(e[0]+e[1])/2:e[0]}return 1}function o(e=".share-page"){const t=i(e);return t>0?1/t:1}function s(e,i={}){const o=t(e);if(!o)throw new Error("[one-screen-scale] element not found");const{mode:s="contain",visualWidth:c=450,origin:u="top center",autoResize:a=!0}=i;let l=1,d=null;function f(){const e=n({sourceWidth:o.offsetWidth,sourceHeight:o.offsetHeight,targetWidth:document.documentElement.clientWidth||window.innerWidth,targetHeight:window.innerHeight,mode:s,visualWidth:c});if(e!==l&&(l=e,r(o,e,u)),"fixed-width"===s&&e>0){const t=document.documentElement.clientWidth||window.innerWidth;o.style.width=`${t}px`,o.style.height=`${Math.round(window.innerHeight/e)}px`}return l}return{start:function(){return f(),a&&(d=()=>f(),window.addEventListener("resize",d)),l},stop:function(){d&&(window.removeEventListener("resize",d),d=null)},refresh:f,getScale:()=>l}}const c={mounted(e,t){const n=s(e,t.value||{});n.start(),e.__fitScreenScaler=n},updated(e,t){const n=e.__fitScreenScaler;n&&(t.value,n.refresh())},unmounted(e){const t=e.__fitScreenScaler;t&&(t.stop(),delete e.__fitScreenScaler)}};exports.applyScale=r,exports.calculateScale=n,exports.createScaler=s,exports.getInverseScale=o,exports.getPageScale=i,exports.useFitScreen=function(t,n={}){const r=e.ref(1),i=e.ref(1);let c=null,u=null;function a(e){if(!e)return;c=s(e,n);const t=c.start();r.value=t,i.value=o(e),u=()=>{r.value=c?.getScale?.()??1,i.value=o(e)},window.addEventListener("resize",u)}return e.onMounted(()=>{a(t?.value??t)}),t&&"object"==typeof t&&"value"in t&&e.watch(()=>t.value,e=>{e&&!c&&a(e)}),e.onUnmounted(()=>{u&&(window.removeEventListener("resize",u),u=null),c?.stop(),c=null}),{scale:r,inverseScale:i,refresh:()=>{const e=t?.value??t;if(!e||!c)return;const n=c.refresh();r.value=n,i.value=o(e)}}},exports.vFitScreen=c;
|
|
1
|
+
"use strict";var e=require("vue");const t="one-screen-scale-base-styles",n={contain:"one-screen-scale--contain","fixed-width":"one-screen-scale--fixed-width",compensate:"one-screen-scale--compensate"},r=`\n.${n.contain} {\n width: 100vw;\n min-height: 100vh;\n min-height: 100dvh;\n}\n/* fixed-width:占位 class,样式先不注入 */\n.${n["fixed-width"]} {\n}\n/*\n * 纯 CSS 反缩放(可选)。v-compensate 指令已改为 JS 行内写入同一公式,更稳。\n * ECharts/Canvas:请用 offsetWidth 量布局盒,勿用 getBoundingClientRect。\n */\n.${n.compensate} {\n transform: scale(var(--one-screen-inverse-scale, 1));\n transform-origin: top left;\n width: calc(100% * var(--one-screen-scale, 1));\n height: calc(100% * var(--one-screen-scale, 1));\n}\n`.trim();function o(){if("undefined"==typeof document)return null;let e=document.getElementById(t);return e?(e.textContent=r,e):(e=document.createElement("style"),e.id=t,e.textContent=r,document.head.appendChild(e),e)}function s(e,t="contain"){if(!e||void 0===e.classList)return null;o();const r=n[t]||n.contain;return[n.contain,n["fixed-width"]].forEach(t=>{t!==r&&e.classList.remove(t)}),e.classList.add(r),r}function i(e){e&&void 0!==e.classList&&e.classList.remove(n.contain,n["fixed-width"])}function a(e){return e?"string"==typeof e?document.querySelector(e):e instanceof HTMLElement?e:e.value instanceof HTMLElement?e.value:e.current instanceof HTMLElement?e.current:null:null}function c({sourceWidth:e,sourceHeight:t,targetWidth:n=("undefined"!=typeof window?window.innerWidth:1920),targetHeight:r=("undefined"!=typeof window?window.innerHeight:1080),mode:o="contain",visualWidth:s=450}){if(!e||!t||e<=0||t<=0)return 1;let i;if("fixed-width"===o)i=n>s?s/n:1;else{const o=n/e,s=r/t;i=Math.min(o,s)}return i}const l="one-screen-scale-change";function u(e,t,n="top center"){const r=a(e);if(!r)return;r.style.transform=`scale(${t})`,r.style.transformOrigin=n;const o=t>0?1/t:1;r.style.setProperty("--one-screen-scale",String(t)),r.style.setProperty("--one-screen-inverse-scale",String(o)),"undefined"!=typeof window&&window.dispatchEvent(new CustomEvent(l,{detail:{scale:t,inverseScale:o,el:r}}))}function f(e){const t=a(e);if(!t)return 1;const n=window.getComputedStyle(t).transform;if(!n||"none"===n)return 1;const r=n.match(/matrix\(([^,]+),\s*[^,]+,\s*[^,]+,\s*([^,]+)/);if(r)return(parseFloat(r[1])+parseFloat(r[2]))/2;const o=n.match(/matrix3d\(([^,]+),[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,([^,]+)\)/);if(o)return(parseFloat(o[1])+parseFloat(o[2]))/2;const s=n.match(/scale\(([^)]+)\)/);if(s){const e=s[1].split(",").map(e=>parseFloat(e.trim()));return e.length>=2?(e[0]+e[1])/2:e[0]}return 1}function d(e,t={}){const n=a(e);if(!n)throw new Error("[one-screen-scale] element not found");const{mode:r="contain",visualWidth:o=450,origin:l="top center",autoResize:f=!0,injectBaseStyles:d=!0,observeContent:m=!0}=t;let h=1,p=null,v=null,y=null,w=0,S=0,g=null,C=!1,_={w:0,h:0},E=0;const x={width:n.style.width,height:n.style.height,transform:n.style.transform,transformOrigin:n.style.transformOrigin};function b(){C=!0;const e=function(){const e=n.offsetWidth,t=n.offsetHeight,s=document.documentElement.clientWidth||window.innerWidth,i=window.innerHeight;return _={w:e,h:t},c({sourceWidth:e,sourceHeight:t,targetWidth:s,targetHeight:i,mode:r,visualWidth:o})}();if(h=e,u(n,e,l),"fixed-width"===r&&e>0){const t=`${document.documentElement.clientWidth||window.innerWidth}px`,r=`${Math.round(window.innerHeight/e)}px`;n.style.width!==t&&(n.style.width=t),n.style.height!==r&&(n.style.height=r)}const t=E;return cancelAnimationFrame(S),S=requestAnimationFrame(()=>{t===E&&(C=!1)}),h}function L(){if(C)return;cancelAnimationFrame(w);const e=E;w=requestAnimationFrame(()=>{if(e!==E)return;const t=n.offsetWidth,r=n.offsetHeight;t===_.w&&r===_.h&&n.style.transform||b()})}function z(){b()}return{start:function(){E+=1;const e=E;return C=!1,d&&s(n,r),b(),f&&(p=z,window.addEventListener("resize",p)),m&&"undefined"!=typeof window&&("undefined"!=typeof ResizeObserver&&(g=new ResizeObserver(()=>L()),g.observe(n)),v=e=>{const t=e.target;!t||"IMG"!==t.tagName&&"VIDEO"!==t.tagName&&"IFRAME"!==t.tagName||L()},n.addEventListener("load",v,!0),n.addEventListener("error",v,!0),document.fonts&&document.fonts.ready&&document.fonts.ready.then(()=>{e===E&&L()}).catch(()=>{}),"complete"===document.readyState?L():(y=()=>L(),window.addEventListener("load",y,{once:!0})),requestAnimationFrame(()=>{requestAnimationFrame(()=>L())})),h},stop:function(){E+=1,p&&(window.removeEventListener("resize",p),p=null),v&&(n.removeEventListener("load",v,!0),n.removeEventListener("error",v,!0),v=null),y&&(window.removeEventListener("load",y),y=null),g&&(g.disconnect(),g=null),cancelAnimationFrame(w),cancelAnimationFrame(S),w=0,S=0,d&&i(n),n.style.width=x.width,n.style.height=x.height,n.style.transform=x.transform,n.style.transformOrigin=x.transformOrigin,n.style.removeProperty("--one-screen-scale"),n.style.removeProperty("--one-screen-inverse-scale")},refresh:b,getScale:()=>h}}const m="one-screen-scale-canvas-resize";let h=null;function p(e){if(!e||"undefined"==typeof getComputedStyle)return NaN;const t=getComputedStyle(e).getPropertyValue("--one-screen-scale").trim();if(!t)return NaN;const n=parseFloat(t);return n>0?n:NaN}function v(e){if(!e||"undefined"==typeof getComputedStyle)return NaN;const t=getComputedStyle(e).transform;if(!t||"none"===t)return NaN;const n=t.match(/matrix\(([^,]+),\s*[^,]+,\s*[^,]+,\s*([^,]+)/);if(n)return(parseFloat(n[1])+parseFloat(n[2]))/2;const r=t.match(/matrix3d\(([^,]+),[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,([^,]+)\)/);if(r)return(parseFloat(r[1])+parseFloat(r[2]))/2;const o=t.match(/scale\(([^)]+)\)/);if(o){const e=o[1].split(",").map(e=>parseFloat(e.trim()));return e.length>=2?(e[0]+e[1])/2:e[0]}return NaN}function y(e){let t=e;for(;t&&1===t.nodeType;){const e=p(t);if(e>0)return e;t=t.parentElement}for(t=e;t&&1===t.nodeType;){if(!t.__oneScreenCompensate){const e=v(t);if(e>0&&1!==e)return e}t=t.parentElement}return 1}function w(e){if(!e)return{scale:1,inverseScale:1};const t=y(e),n=t>0?1/t:1;return e.style.transform=`scale(${n})`,e.style.transformOrigin="top left",e.style.width=100*t+"%",e.style.height=100*t+"%",e.style.overflow="visible",e.__oneScreenCompensate=!0,{scale:t,inverseScale:n}}function S(e){e&&(e.style.transform="",e.style.transformOrigin="",e.style.width="",e.style.height="",e.style.overflow="",delete e.__oneScreenCompensate)}function g(e){return{width:Math.max(1,Math.round(e?.offsetWidth||0)),height:Math.max(1,Math.round(e?.offsetHeight||0))}}function C(e,t){if(!e||"function"!=typeof t)return!1;let n;try{n=t(e)}catch{return!1}if(!n||"function"!=typeof n.resize)return!1;const{width:r,height:o}=g(e);try{return n.resize({width:r,height:o}),!0}catch{return!1}}function _(e){const t="undefined"!=typeof globalThis?globalThis.Chart:null,n=t?.getChart;if("function"!=typeof n)return!1;let r;try{r=n(e)}catch{return!1}if(!r||"function"!=typeof r.resize)return!1;try{return r.resize(),!0}catch{return!1}}function E(e,t){if(!e)return 0;const n=t||h||("undefined"!=typeof globalThis?globalThis.echarts:null)||null;const r=n?.getInstanceByDom;let o=0;const s=e.querySelectorAll?e.querySelectorAll("canvas"):[];for(const t of s){const n=t.parentElement||t,{width:s,height:i}=g(n);(C(t,r)||C(n,r))&&(o+=1),_(t)&&(o+=1);try{t.dispatchEvent(new CustomEvent(m,{bubbles:!0,detail:{width:s,height:i,canvas:t,el:e}}))}catch{}}if("function"==typeof r){const t=[e,...e.querySelectorAll("div")];for(const e of t)C(e,r)&&(o+=1)}return o}function x(e={}){return Boolean(e.resizeCanvas||e.echarts||e.echartsLib)}function b(e={}){return"object"==typeof e.echarts?e.echarts:e.echartsLib?e.echartsLib:void 0}function L(e){const t="function"==typeof e?e():e;return t&&"object"==typeof t?t:{}}function z(e,t={}){if(!e)throw new Error("[one-screen-scale] compensate element required");const n=x(L(t));let r=0,o=null,s=null,i=[],a=!1;const c=()=>{i.forEach(e=>clearTimeout(e)),i=[]},u=()=>{if(a)return;const n=L(t),{scale:r,inverseScale:o}=w(e);if("function"==typeof n.onRefresh)try{n.onRefresh({scale:r,inverseScale:o,el:e})}catch{}if(x(n)){const r=b(n);0===E(e,r)&&(c(),[50,200].forEach(n=>{const r=setTimeout(()=>{if(a)return;const n=L(t);E(e,b(n))},n);i.push(r)}))}},f=()=>{a||(cancelAnimationFrame(r),r=requestAnimationFrame(()=>{r=0,a||u()}))};u(),"undefined"!=typeof window&&(window.addEventListener(l,f),window.addEventListener("resize",f)),"undefined"!=typeof ResizeObserver&&(o=new ResizeObserver(f),e.parentElement&&o.observe(e.parentElement),o.observe(e)),n&&"undefined"!=typeof MutationObserver&&(s=new MutationObserver(f),s.observe(e,{childList:!0,subtree:!0}));return{refresh:u,stop:()=>{a=!0,cancelAnimationFrame(r),c(),r=0,"undefined"!=typeof window&&(window.removeEventListener(l,f),window.removeEventListener("resize",f)),o?.disconnect(),o=null,s?.disconnect(),s=null,S(e)}}}function A(e){return e.value&&"object"==typeof e.value?e.value:{}}function F(e,t){const n=A(t),r=d(e,n);r.start(),e.__fitScreenScaler=r,e.__fitScreenOptionsKey=JSON.stringify({mode:n.mode||"contain",visualWidth:n.visualWidth,origin:n.origin,autoResize:n.autoResize,injectBaseStyles:n.injectBaseStyles,observeContent:n.observeContent})}function O(e){const t=e.__fitScreenScaler;t&&(t.stop(),delete e.__fitScreenScaler,delete e.__fitScreenOptionsKey)}const N={mounted(e,t){F(e,t)},updated(e,t){const n=A(t),r=JSON.stringify({mode:n.mode||"contain",visualWidth:n.visualWidth,origin:n.origin,autoResize:n.autoResize,injectBaseStyles:n.injectBaseStyles,observeContent:n.observeContent});if(e.__fitScreenOptionsKey!==r)return O(e),void F(e,t);e.__fitScreenScaler?.refresh()},unmounted(e){O(e)}};function W(e){const t=e?.value,n=e?.modifiers||{},r=t&&"object"==typeof t?{...t}:{};return(n.resizeCanvas||n["resize-canvas"]||n.echarts||"resize-canvas"===t||"resizeCanvas"===t||"echarts"===t)&&(r.resizeCanvas=r.resizeCanvas||!0),r}function R(e){return JSON.stringify({resizeCanvas:Boolean(e.resizeCanvas||e.echarts||e.echartsLib),hasLib:Boolean(e.echarts&&"object"==typeof e.echarts||e.echartsLib),hasOnRefresh:"function"==typeof e.onRefresh})}function B(e){const t=z(e,()=>e.__oneScreenCompensateOptions||{});e.__oneScreenCompensateApi=t,e.__oneScreenCompensateKey=R(e.__oneScreenCompensateOptions||{})}const T={mounted(e,t){e.__oneScreenCompensateOptions=W(t),B(e)},updated(e,t){const n=W(t),r=R(n);if(e.__oneScreenCompensateOptions=n,e.__oneScreenCompensateKey!==r)return e.__oneScreenCompensateApi?.stop(),void B(e);e.__oneScreenCompensateApi?.refresh()},unmounted(e){e.__oneScreenCompensateApi?.stop(),delete e.__oneScreenCompensateApi,delete e.__oneScreenCompensateKey,delete e.__oneScreenCompensateOptions}},H=T;exports.BASE_CLASS=n,exports.CANVAS_RESIZE_EVENT=m,exports.SCALE_CHANGE_EVENT=l,exports.applyBaseStyles=s,exports.applyCompensateStyles=function(e){return e&&void 0!==e.classList?(o(),e.classList.add(n.compensate),n.compensate):null},exports.applyCompensateTransform=w,exports.applyScale=u,exports.calculateScale=c,exports.clearCompensateTransform=S,exports.createCompensate=z,exports.createScaler=d,exports.ensureBaseStyleSheet=o,exports.getInverseScale=function(e=".share-page"){const t=f(e);return t>0?1/t:1},exports.getPageScale=f,exports.removeBaseStyles=i,exports.removeCompensateStyles=function(e){e&&void 0!==e.classList&&e.classList.remove(n.compensate)},exports.resizeCanvasUnder=E,exports.resizeEchartsUnder=function(e,t){return E(e,t)},exports.resolvePageScale=y,exports.setCompensateEcharts=function(e){h=e||null},exports.useFitScreen=function(t,n={}){const r=e.ref(1),o=e.ref(1);let s=null,i=null;function a(e){if(!e)return;s=d(e,n);const t=s.start();r.value=t,o.value=t>0?1/t:1,i=t=>{t.detail?.el===e&&(r.value=t.detail.scale,o.value=t.detail.inverseScale)},window.addEventListener(l,i)}return e.onMounted(()=>{a(t?.value??t)}),t&&"object"==typeof t&&"value"in t&&e.watch(()=>t.value,e=>{e&&!s&&a(e)}),e.onUnmounted(()=>{i&&(window.removeEventListener(l,i),i=null),s?.stop(),s=null}),{scale:r,inverseScale:o,refresh:()=>{if(!(t?.value??t)||!s)return;const e=s.refresh();r.value=e,o.value=e>0?1/e:1}}},exports.vCompensate=T,exports.vFitCompensate=H,exports.vFitScreen=N;
|
|
2
2
|
//# sourceMappingURL=index.cjs.js.map
|