one-screen-scale 1.1.0 → 1.3.1
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 +356 -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 +107 -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,356 @@
|
|
|
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
|
+
#### 什么时候用
|
|
63
|
+
|
|
64
|
+
规则页、协议、长表单这种内容比一屏高、需要上下滚动的页面。手机上看是正常宽度;PC 上视口更宽时,只把页面缩到一条手机宽(默认视觉宽 450),两边留白,不要把字拉得很大。
|
|
65
|
+
|
|
66
|
+
`contain` 不适合这种页:它要整页塞进一屏,长文会被缩得很小。
|
|
67
|
+
|
|
68
|
+
#### 插件做了什么,页面还要做什么
|
|
69
|
+
|
|
70
|
+
指令做两件事:
|
|
71
|
+
|
|
72
|
+
1. 按视口把根节点缩小,并把根节点的宽高写进行内样式。
|
|
73
|
+
2. 滚动条会跟着缩小。Chrome 里只把滚动条改窄、不写滑块颜色,滑块会直接消失。所以指令会给根节点和里面的滚动区域一起补上宽度和滑块颜色,看起来大约 6px。**滚动条样式不用再写。**
|
|
74
|
+
|
|
75
|
+
指令不知道标题要不要钉在上面、正文从哪里开始滚。这两件事是页面布局,必须自己写。只加 `v-fit-screen="{ mode: 'fixed-width' }"`、不写下边的结构,页面不会滚。
|
|
76
|
+
|
|
77
|
+
#### 怎么写
|
|
78
|
+
|
|
79
|
+
根节点做成纵向弹性盒,自己不滚动。标题高度固定。剩下的高度全部交给滚动区,由滚动区 `overflow-y: auto` 滚动。
|
|
80
|
+
|
|
81
|
+
```vue
|
|
82
|
+
<template>
|
|
83
|
+
<div v-fit-screen="{ mode: 'fixed-width' }" class="rules-page">
|
|
84
|
+
<header class="rules-header">标题</header>
|
|
85
|
+
<div class="rules-scroll">
|
|
86
|
+
<main>很长的正文...</main>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</template>
|
|
90
|
+
|
|
91
|
+
<style scoped>
|
|
92
|
+
.rules-page {
|
|
93
|
+
display: flex; /* 纵向排列标题和正文 */
|
|
94
|
+
flex-direction: column;
|
|
95
|
+
overflow: hidden; /* 整页不滚,避免和内部滚动条抢 */
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.rules-header {
|
|
99
|
+
flex-shrink: 0; /* 标题保持自己的高度,不被正文挤扁 */
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.rules-scroll {
|
|
103
|
+
flex: 1; /* 拿走标题下面的全部剩余高度 */
|
|
104
|
+
min-height: 0; /* 允许它比内容矮,否则 flex 子项不会出现滚动 */
|
|
105
|
+
overflow-y: auto; /* 正文超出这块区域时,在这里面滚 */
|
|
106
|
+
overflow-x: hidden;
|
|
107
|
+
}
|
|
108
|
+
</style>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
背景色、圆角按设计另写。三行滚动布局(`flex: 1`、`min-height: 0`、`overflow-y: auto`)不能省。
|
|
112
|
+
|
|
113
|
+
## 安装
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
npm install one-screen-scale
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## 快速开始
|
|
120
|
+
|
|
121
|
+
### Vue 3 — 指令(最简)
|
|
122
|
+
|
|
123
|
+
```vue
|
|
124
|
+
<template>
|
|
125
|
+
<!-- 一屏:自动注入 min-height:100vh + width:100vw -->
|
|
126
|
+
<div v-fit-screen class="page">
|
|
127
|
+
<h1>活动标题</h1>
|
|
128
|
+
<p>页面内容...</p>
|
|
129
|
+
</div>
|
|
130
|
+
</template>
|
|
131
|
+
|
|
132
|
+
<script setup>
|
|
133
|
+
import { vFitScreen } from 'one-screen-scale'
|
|
134
|
+
</script>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
长滚动的完整结构见上面「fixed-width — 长滚动」:根节点 `display: flex; flex-direction: column; overflow: hidden`,滚动区 `flex: 1; min-height: 0; overflow-y: auto`。只写指令、不写这几行,页面不会滚。
|
|
138
|
+
|
|
139
|
+
### Vue 3 — `v-compensate`(第三方组件反缩放)
|
|
140
|
+
|
|
141
|
+
页面 `transform: scale` 后,ECharts 等常会尺寸偏小或量错。在槽位内再包一层即可(**JS 行内**写入):
|
|
142
|
+
|
|
143
|
+
`width/height: 100% * pageScale` + `transform: scale(inverse)`,scale 读祖先 `--one-screen-scale`。
|
|
144
|
+
|
|
145
|
+
```vue
|
|
146
|
+
<template>
|
|
147
|
+
<div v-fit-screen class="page">
|
|
148
|
+
<div class="chart-slot">
|
|
149
|
+
<div v-compensate.resize-canvas class="chart-inner">
|
|
150
|
+
<div ref="chartEl" class="chart"></div>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</template>
|
|
155
|
+
|
|
156
|
+
<script setup>
|
|
157
|
+
import * as echarts from 'echarts'
|
|
158
|
+
import { vFitScreen, vCompensate, setCompensateEcharts } from 'one-screen-scale'
|
|
159
|
+
|
|
160
|
+
// 可选:全局注入 echarts,供 resizeCanvas 自动探测
|
|
161
|
+
setCompensateEcharts(echarts)
|
|
162
|
+
</script>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
```css
|
|
166
|
+
.chart-slot { width: 100%; height: 300px; overflow: hidden; } /* 设计稿槽位,需明确宽高 */
|
|
167
|
+
.chart-inner, .chart { width: 100%; height: 100%; }
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
> **适用**:任意被页面 scale 搞小的区域,尤其 **canvas**——先反缩放,再用 `offsetWidth` 量盒(勿用 `getBoundingClientRect`)。
|
|
171
|
+
> **`{ resizeCanvas: true }` / `.resize-canvas`**:补偿后通知子树 canvas 重绘(探测 ECharts / Chart.js,并在每个 `<canvas>` 上派发 `one-screen-scale-canvas-resize`)。
|
|
172
|
+
> 只抵消页面 `v-fit-screen` 的 scale;**不处理**浏览器 Ctrl+/-。
|
|
173
|
+
> **不适用**:`el-slider` 等依赖指针坐标 / 易被 overflow 裁切的控件。
|
|
174
|
+
|
|
175
|
+
### Vue 3 — Composable(需要拿到 scale 数值时)
|
|
176
|
+
|
|
177
|
+
```vue
|
|
178
|
+
<script setup>
|
|
179
|
+
import { ref } from 'vue'
|
|
180
|
+
import { useFitScreen } from 'one-screen-scale'
|
|
181
|
+
|
|
182
|
+
const pageRef = ref(null)
|
|
183
|
+
const { scale, inverseScale } = useFitScreen(pageRef)
|
|
184
|
+
</script>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### React
|
|
188
|
+
|
|
189
|
+
```jsx
|
|
190
|
+
import { useRef } from 'react'
|
|
191
|
+
import { useFitScreen, useCompensate } from 'one-screen-scale/react'
|
|
192
|
+
|
|
193
|
+
function App() {
|
|
194
|
+
const pageRef = useRef(null)
|
|
195
|
+
const chartInnerRef = useRef(null)
|
|
196
|
+
const { scale, inverseScale } = useFitScreen(pageRef)
|
|
197
|
+
// 需要通知子树 canvas 重绘时加 { resizeCanvas: true }
|
|
198
|
+
useCompensate(chartInnerRef, { resizeCanvas: true })
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<div ref={pageRef} className="page">
|
|
202
|
+
<div className="chart-slot" style={{ width: 350, height: 300 }}>
|
|
203
|
+
<div ref={chartInnerRef} style={{ width: '100%', height: '100%' }}>
|
|
204
|
+
<div id="chart" style={{ width: '100%', height: '100%' }} />
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### 纯 JS / HTML
|
|
213
|
+
|
|
214
|
+
```js
|
|
215
|
+
import { createScaler, createCompensate } from 'one-screen-scale'
|
|
216
|
+
|
|
217
|
+
const page = document.querySelector('.page')
|
|
218
|
+
const scaler = createScaler(page, { mode: 'contain' })
|
|
219
|
+
scaler.start()
|
|
220
|
+
|
|
221
|
+
// 第三方组件反缩放(对应 Vue v-compensate / React useCompensate)
|
|
222
|
+
const inner = document.querySelector('.chart-inner')
|
|
223
|
+
const compensate = createCompensate(inner, { resizeCanvas: true })
|
|
224
|
+
|
|
225
|
+
// 销毁
|
|
226
|
+
compensate.stop()
|
|
227
|
+
scaler.stop() // 移除注入的 class,并还原本库写过的 inline 样式
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## 两种模式
|
|
231
|
+
|
|
232
|
+
| 模式 | 算法 | 效果 | 场景 |
|
|
233
|
+
|------|------|------|------|
|
|
234
|
+
| `contain`(默认) | `min(scaleX, scaleY)` | 内容完整可见,不溢出 | 活动页、落地页 |
|
|
235
|
+
| `fixed-width` | `visualWidth / innerWidth`(视口 ≤ visualWidth 时为 1) | 固定手机观感宽度,可滚动 | 规则页、长表单 |
|
|
236
|
+
|
|
237
|
+
```js
|
|
238
|
+
// contain(默认)
|
|
239
|
+
createScaler(el)
|
|
240
|
+
|
|
241
|
+
// fixed-width
|
|
242
|
+
createScaler(el, { mode: 'fixed-width', visualWidth: 450 })
|
|
243
|
+
|
|
244
|
+
// 自己写根节点宽高时关闭注入
|
|
245
|
+
createScaler(el, { injectBaseStyles: false })
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## API
|
|
249
|
+
|
|
250
|
+
### 核心函数(框架无关)
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
calculateScale(options: ScaleOptions): number
|
|
254
|
+
|
|
255
|
+
applyScale(el: Element | string, scale: number, origin?: string): void
|
|
256
|
+
|
|
257
|
+
getPageScale(el?: Element | string): number
|
|
258
|
+
getInverseScale(el?: Element | string): number
|
|
259
|
+
|
|
260
|
+
createScaler(el: Element | string, options?: ScalerOptions): ScalerInstance
|
|
261
|
+
|
|
262
|
+
ensureBaseStyleSheet(): HTMLStyleElement | null
|
|
263
|
+
applyBaseStyles(el: HTMLElement, mode?: 'contain' | 'fixed-width'): string | null
|
|
264
|
+
removeBaseStyles(el: HTMLElement): void
|
|
265
|
+
applyCompensateStyles(el: HTMLElement): string | null
|
|
266
|
+
removeCompensateStyles(el: HTMLElement): void
|
|
267
|
+
|
|
268
|
+
SCALE_CHANGE_EVENT // 'one-screen-scale-change',页面 scale 变化时 window 上触发
|
|
269
|
+
|
|
270
|
+
// 组件反缩放(仅抵消页面 scale)
|
|
271
|
+
resolvePageScale(fromEl?): number
|
|
272
|
+
applyCompensateTransform(el): { scale, inverseScale }
|
|
273
|
+
clearCompensateTransform(el): void
|
|
274
|
+
createCompensate(el, options?: CompensateOptions): { refresh, stop }
|
|
275
|
+
resizeCanvasUnder(el, echartsLib?): number
|
|
276
|
+
setCompensateEcharts(echarts): void // 可选,供 resizeCanvas 探测 ECharts
|
|
277
|
+
CANVAS_RESIZE_EVENT // 'one-screen-scale-canvas-resize'
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### ScalerInstance
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
{
|
|
284
|
+
start(): number // 注入基础样式 + 启动缩放 + 监听 resize
|
|
285
|
+
stop(): void // 停止监听,移除注入 class,还原 inline
|
|
286
|
+
refresh(): number // 手动重算
|
|
287
|
+
getScale(): number
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### ScalerOptions
|
|
292
|
+
|
|
293
|
+
| 参数 | 类型 | 默认值 | 说明 |
|
|
294
|
+
|------|------|--------|------|
|
|
295
|
+
| `mode` | `'contain' \| 'fixed-width'` | `'contain'` | 缩放模式 |
|
|
296
|
+
| `visualWidth` | `number` | `450` | fixed-width 模式目标视觉宽度 |
|
|
297
|
+
| `origin` | `string` | `'top center'` | transform-origin |
|
|
298
|
+
| `autoResize` | `boolean` | `true` | 自动监听窗口 resize |
|
|
299
|
+
| `injectBaseStyles` | `boolean` | `true` | 是否自动注入模式所需宽高样式 |
|
|
300
|
+
| `observeContent` | `boolean` | `true` | 内容尺寸变化(如图片加载撑高)时自动重算,避免偶发 scale 偏大 |
|
|
301
|
+
|
|
302
|
+
### Vue
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
vFitScreen: Directive<HTMLElement, ScalerOptions>
|
|
306
|
+
vCompensate: Directive<HTMLElement> // 第三方反缩放
|
|
307
|
+
vFitCompensate: Directive<HTMLElement> // 同上别名
|
|
308
|
+
|
|
309
|
+
useFitScreen(ref: Ref, options?: ScalerOptions): {
|
|
310
|
+
scale: Ref<number>
|
|
311
|
+
inverseScale: Ref<number>
|
|
312
|
+
refresh: () => void
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### React(`one-screen-scale/react`)
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
useFitScreen(ref, options?: ScalerOptions): { scale, inverseScale, refresh }
|
|
320
|
+
useCompensate(ref, options?: CompensateOptions): { refresh } // 对应 v-compensate
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### CompensateOptions
|
|
324
|
+
|
|
325
|
+
| 参数 | 类型 | 默认值 | 说明 |
|
|
326
|
+
|------|------|--------|------|
|
|
327
|
+
| `resizeCanvas` | `boolean` | — | **可选**。补偿后通知子树 canvas 重绘 |
|
|
328
|
+
| `onRefresh` | `(detail) => void` | — | 每次写入反缩放后回调 |
|
|
329
|
+
| `echarts` / `echartsLib` | — | — | 旧名,等价于开启 `resizeCanvas`;对象形式可传 echarts 模块 |
|
|
330
|
+
|
|
331
|
+
也可 `setCompensateEcharts(echarts)` 全局注入,方便自动探测。
|
|
332
|
+
|
|
333
|
+
## Canvas 模糊 / 偏小
|
|
334
|
+
|
|
335
|
+
CSS `transform: scale()` 会把 **所有 canvas** 一起缩小。先反缩放,再重绘:
|
|
336
|
+
|
|
337
|
+
| 环境 | 反缩放 | 通知 canvas 重绘 |
|
|
338
|
+
|------|--------|------------------|
|
|
339
|
+
| Vue | `v-compensate` | `v-compensate.resize-canvas` |
|
|
340
|
+
| React | `useCompensate(ref)` | `useCompensate(ref, { resizeCanvas: true })` |
|
|
341
|
+
| 纯 JS | `createCompensate(el)` | `createCompensate(el, { resizeCanvas: true })` |
|
|
342
|
+
|
|
343
|
+
`resizeCanvas` 会:探测已加载的 ECharts / Chart.js;并在每个 `<canvas>` 上派发事件,自研库可监听:
|
|
344
|
+
|
|
345
|
+
```js
|
|
346
|
+
canvas.addEventListener('one-screen-scale-canvas-resize', (e) => {
|
|
347
|
+
const { width, height } = e.detail
|
|
348
|
+
myChart.resize(width, height)
|
|
349
|
+
})
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
手写等价:`width/height: 100% * pageScale` + `transform: scale(1/pageScale)`,量尺寸用 `offsetWidth`。
|
|
353
|
+
|
|
354
|
+
## License
|
|
355
|
+
|
|
356
|
+
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/*\n * fixed-width 整页被 scale 后,滚动条会一起变细。\n * WebKit 只要改了宽度却不写滑块,滑块就会消失,所以宽度和颜色一起注入。\n * 这段不经过业务的 px-to-viewport。根节点和内部滚动区域都覆盖。\n */\n.${n["fixed-width"]}::-webkit-scrollbar,\n.${n["fixed-width"]} *::-webkit-scrollbar {\n width: calc(6px * var(--one-screen-inverse-scale, 1));\n height: calc(6px * var(--one-screen-inverse-scale, 1));\n}\n.${n["fixed-width"]}::-webkit-scrollbar-thumb,\n.${n["fixed-width"]} *::-webkit-scrollbar-thumb {\n background: rgba(18, 51, 105, 0.28);\n border-radius: 999px;\n}\n.${n["fixed-width"]}::-webkit-scrollbar-track,\n.${n["fixed-width"]} *::-webkit-scrollbar-track {\n background: transparent;\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 d(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 f(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:d=!0,injectBaseStyles:f=!0,observeContent:h=!0}=t;let m=1,p=null,v=null,y=null,w=0,g=0,S=null,C=!1,b={w:0,h:0},x=0;const _={width:n.style.width,height:n.style.height,transform:n.style.transform,transformOrigin:n.style.transformOrigin};function E(){C=!0;const e=function(){const e=n.offsetWidth,t=n.offsetHeight,s=document.documentElement.clientWidth||window.innerWidth,i=window.innerHeight;return b={w:e,h:t},c({sourceWidth:e,sourceHeight:t,targetWidth:s,targetHeight:i,mode:r,visualWidth:o})}();if(m=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=x;return cancelAnimationFrame(g),g=requestAnimationFrame(()=>{t===x&&(C=!1)}),m}function L(){if(C)return;cancelAnimationFrame(w);const e=x;w=requestAnimationFrame(()=>{if(e!==x)return;const t=n.offsetWidth,r=n.offsetHeight;t===b.w&&r===b.h&&n.style.transform||E()})}function z(){E()}return{start:function(){x+=1;const e=x;return C=!1,f&&s(n,r),E(),d&&(p=z,window.addEventListener("resize",p)),h&&"undefined"!=typeof window&&("undefined"!=typeof ResizeObserver&&(S=new ResizeObserver(()=>L()),S.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===x&&L()}).catch(()=>{}),"complete"===document.readyState?L():(y=()=>L(),window.addEventListener("load",y,{once:!0})),requestAnimationFrame(()=>{requestAnimationFrame(()=>L())})),m},stop:function(){x+=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),S&&(S.disconnect(),S=null),cancelAnimationFrame(w),cancelAnimationFrame(g),w=0,g=0,f&&i(n),n.style.width=_.width,n.style.height=_.height,n.style.transform=_.transform,n.style.transformOrigin=_.transformOrigin,n.style.removeProperty("--one-screen-scale"),n.style.removeProperty("--one-screen-inverse-scale")},refresh:E,getScale:()=>m}}const h="one-screen-scale-canvas-resize";let m=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 g(e){e&&(e.style.transform="",e.style.transformOrigin="",e.style.width="",e.style.height="",e.style.overflow="",delete e.__oneScreenCompensate)}function S(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}=S(e);try{return n.resize({width:r,height:o}),!0}catch{return!1}}function b(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 x(e,t){if(!e)return 0;const n=t||m||("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}=S(n);(C(t,r)||C(n,r))&&(o+=1),b(t)&&(o+=1);try{t.dispatchEvent(new CustomEvent(h,{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 _(e={}){return Boolean(e.resizeCanvas||e.echarts||e.echartsLib)}function E(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=_(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(_(n)){const r=E(n);0===x(e,r)&&(c(),[50,200].forEach(n=>{const r=setTimeout(()=>{if(a)return;const n=L(t);x(e,E(n))},n);i.push(r)}))}},d=()=>{a||(cancelAnimationFrame(r),r=requestAnimationFrame(()=>{r=0,a||u()}))};u(),"undefined"!=typeof window&&(window.addEventListener(l,d),window.addEventListener("resize",d)),"undefined"!=typeof ResizeObserver&&(o=new ResizeObserver(d),e.parentElement&&o.observe(e.parentElement),o.observe(e)),n&&"undefined"!=typeof MutationObserver&&(s=new MutationObserver(d),s.observe(e,{childList:!0,subtree:!0}));return{refresh:u,stop:()=>{a=!0,cancelAnimationFrame(r),c(),r=0,"undefined"!=typeof window&&(window.removeEventListener(l,d),window.removeEventListener("resize",d)),o?.disconnect(),o=null,s?.disconnect(),s=null,g(e)}}}function A(e){return e.value&&"object"==typeof e.value?e.value:{}}function F(e,t){const n=A(t),r=f(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=h,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=g,exports.createCompensate=z,exports.createScaler=f,exports.ensureBaseStyleSheet=o,exports.getInverseScale=function(e=".share-page"){const t=d(e);return t>0?1/t:1},exports.getPageScale=d,exports.removeBaseStyles=i,exports.removeCompensateStyles=function(e){e&&void 0!==e.classList&&e.classList.remove(n.compensate)},exports.resizeCanvasUnder=x,exports.resizeEchartsUnder=function(e,t){return x(e,t)},exports.resolvePageScale=y,exports.setCompensateEcharts=function(e){m=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=f(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
|