uview-plus 3.8.119 → 3.8.125

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/changelog.md CHANGED
@@ -1,3 +1,59 @@
1
+ ## 3.8.125
2
+ fix: 修复签名组件在 App 端无法绘制、笔迹画出后闪退的问题
3
+
4
+ u-signature 在 App 端(APP-PLUS 旧版 canvas)存在无法落笔、笔迹一闪即消失的问题,根因是画布上下文异步就绪未做握手、触摸坐标未换算到画布坐标系、以及每次 draw 都会刷新绘图命令队列导致已画笔迹被清空。
5
+
6
+ - 修复笔迹画出后立即消失:touchMove 改为「增量线段」绘制(beginPath + moveTo 上一点 + lineTo 当前点 + stroke),并使用 draw(true) 保留已绘制内容,不再每次 draw(false) 清空命令队列
7
+ - 修复 App 端坐标错乱、笔迹落到画布外:getCanvasPoint 优先使用画布相对坐标,缺失时用视口坐标减去画布位置进行换算
8
+ - 修复画布上下文异步就绪时首笔丢失:新增画布 ready / initCanvas 握手,缓存画布位置并在画布就绪后再响应绘制
9
+ - 收尾不再 closePath,避免手写笔迹出现回连直线;撤销回放逻辑保持兼容
10
+ - 覆盖 APP-PLUS / APP-HARMONY / APP-NVUE,微信小程序与 H5 行为保持不变
11
+
12
+ ## 3.8.124
13
+ fix: App 端图标字体默认使用本地字体,新增通用 Vite 插件入口 UpVite
14
+
15
+ u-icon 的 useAppStaticIconFont 原先写死 false,只有启用 UniUpRoot 的项目才会在构建期被改成 true;未启用 Root 组件的 App 项目仍然走远程 CDN 字体,弱网或离线时图标会丢失。本地字体是随包能力,不该由是否使用 Root 组件决定。
16
+
17
+ - App / App-nvue 默认优先加载随包分发的 _www/static/app-plus/uview-plus/upicon.ttf
18
+ - App-Vue 在 uni.loadFontFace 失败后回退 config.iconUrl,每个页面只回退一次;App-nvue 因 dom.addRule 没有失败回调,注册前用 plus.io.resolveLocalFileSystemURL 探测字体文件,缺失时回退远程
19
+ - 新增通用 Vite 插件入口 UpVite(libs/vite/index.js),App 本地图标字体是它的第一个 feature;后续组件库新增构建期能力只需在插件内追加,业务项目不需要再改自己的 vite.config
20
+ - UniUpRoot 复用同一份图标字体 feature 并新增 appStaticIconFont: false 选项,只装 Root 插件的项目行为完全不变
21
+ - 不使用 Root 组件的项目单独引入 UpVite() 即可复制字体并移除 App 远程 @font-face
22
+ - 新增 verify:app-local-icon-font 回归校验,覆盖 static 路径、两端兜底、插件复制行为与关闭开关
23
+ - H5 与小程序行为不变,仍使用 config.iconUrl
24
+
25
+ ## 3.8.123
26
+ fix: 修复 u-slider 区间模式 v-model:rangeValue 不生效 (#554)
27
+
28
+ u-slider 区间模式(isRange)拖动滑块时只抛出 update:modelValue,未抛出 update:rangeValue,导致 v-model:rangeValue 双向绑定无法更新。此外 rangeValue 的 default 为数组字面量,所有实例共用同一引用。
29
+
30
+ - 拖动结束时正确抛出 update:rangeValue 事件,同时兼容此前监听 update:modelValue 获取区间值的用法
31
+ - 值未变化时跳过事件抛出,避免父级对数组做拷贝或归一化时形成更新回环
32
+ - rangeValue 的 default 改为工厂函数,避免实例间共享数组引用
33
+ - 补充 isRange、rangeValue 及 onUpdate:rangeValue 类型声明
34
+ - 新增 verify:slider-range-value-model 回归校验脚本
35
+
36
+ ## 3.8.122
37
+ fix: 修复 u-row-notice 横向滚动空格丢失
38
+
39
+ u-row-notice 每 20 个字符切分渲染到独立的 text 标签,若第 21 个字符为空格,会落在第二段行首被 CSS 折叠空白吃掉,导致横向滚动时两个字粘连。现将滚动文本 white-space 改为 pre,在保留原文空格的同时禁止换行,避免行首行尾空白被丢弃,并通过回归校验覆盖分段空格边界场景。
40
+
41
+ ## 3.8.121
42
+ fix: 修复 up-button 真机首次点击后失效
43
+
44
+ 默认 throttleTime 为 0 时直接同步执行点击回调,不再依赖异步 setTimeout 释放全局节流锁,避免真机定时器未释放时后续点击被拦截。新增零延迟连续调用回归校验。
45
+
46
+ ## 3.8.120
47
+ fix(types): 组件类型可直接从包名导入,模板提示补齐 u-/u-- 前缀
48
+
49
+ 本版仅完善 TypeScript 类型与 IDE 模板提示,不改变组件运行时行为和 API。
50
+
51
+ - `uview-plus` 包入口重新导出 126 个公开组件类型,Props、Slots、Ref 可直接从包名导入,不再依赖 `uview-plus/types/comps/*` 内部路径
52
+ - Ref 类型统一按 `typeof` 导出,避免将源文件中的 const 声明当作类型使用时报 TS2749
53
+ - `GlobalComponents` 同时登记 `up-`、`u-`、`u--` 三种 easycom 前缀,模板中的 `u-button`、`u--button` 写法也能获得组件类型提示
54
+ - 保留一份组件清单,通过模板字面量键改写生成三种前缀,避免清单重复维护
55
+ - 新增 `verify:types-barrel-exports` 回归校验,防止入口类型导出与模板组件登记清单漂移
56
+
1
57
  ## 3.8.119
2
58
  fix: 修复 datetime-picker format 兼容性与 tabbar 边框切页丢失
3
59
 
@@ -2725,3 +2781,13 @@ fix: #261u-input在直接修改v-model的绑定值时,每隔一次会无法出
2725
2781
 
2726
2782
  - feat: tabbar 新增多种风格预设、动态图标切换与轻量选中动画
2727
2783
  - improvment: tabbar 补充高颜值本地图标示例并优化发布型底栏视觉
2784
+ ## 3.8.121
2785
+ fix: 修复 up-button 真机首次点击后失效
2786
+
2787
+ 用户反馈 up-button 在网页端正常,但在真机上点击一次后无法再次触发 click 事件。组件默认 throttleTime 为 0,原节流实现仍会先将全局节流标志置为 true,再依赖 setTimeout 回调异步释放;当真机运行环境没有按预期执行该定时回调时,全局标志会一直保持锁定状态,后续所有使用节流的点击都会被拦截。
2788
+
2789
+ - throttle 的 wait 小于等于 0 时直接同步执行回调并返回,不再挂起全局锁
2790
+ - 保留大于 0 时原有的立即节流与非立即节流行为,API 不变
2791
+ - 新增 `verify:button-throttle` 回归校验,覆盖零延迟连续调用场景
2792
+
2793
+ ## 3.8.120
@@ -2,7 +2,10 @@ import config from '../../libs/config/config';
2
2
 
3
3
  const iconFontFamily = 'uicon-iconfont';
4
4
  const appIconFontUrl = '_www/static/app-plus/uview-plus/upicon.ttf';
5
- const useAppStaticIconFont = false;
5
+ // App 端默认优先加载随包分发的本地字体,图标不再依赖 CDN。
6
+ // 项目没有本地字体(未启用本地字体构建插件、或没拷贝 static/app-plus/uview-plus/upicon.ttf)时:
7
+ // App-Vue 在 uni.loadFontFace 失败后回退 config.iconUrl,App-nvue 先探测字体文件是否存在再决定字体地址。
8
+ const useAppStaticIconFont = true;
6
9
 
7
10
  let params = {
8
11
  loaded: false,
@@ -12,6 +15,8 @@ let params = {
12
15
  // #ifdef APP-VUE
13
16
  const appVueLoadedPages = new WeakSet();
14
17
  const appVueLoadingPages = new WeakSet();
18
+ // 本地字体加载失败后回退远程字体,每个页面只回退一次,避免反复请求不存在的本地字体
19
+ const appVueFallbackPages = new WeakSet();
15
20
  // #endif
16
21
 
17
22
  const getCurrentAppVuePage = () => {
@@ -44,7 +49,6 @@ const getAppIconUrl = () => {
44
49
  }
45
50
  if (typeof plus !== 'undefined' && plus.io && typeof plus.io.convertLocalFileSystemURL === 'function') {
46
51
  const iconFontPath = plus.io.convertLocalFileSystemURL(appIconFontUrl);
47
- // if (iconFontPath) return iconFontPath;
48
52
  return iconFontPath && iconFontPath.startsWith('file://') ? iconFontPath : 'file://' + iconFontPath;
49
53
  }
50
54
  return appIconFontUrl;
@@ -75,6 +79,69 @@ const markFontLoaded = () => {
75
79
  }
76
80
  };
77
81
 
82
+ // #ifdef APP-NVUE
83
+ const addNvueFontRule = (family, url) => {
84
+ const domModule = weex.requireModule('dom');
85
+ domModule.addRule('fontFace', {
86
+ 'fontFamily': family,
87
+ 'src': `url('${url}')`
88
+ });
89
+ };
90
+
91
+ // nvue的dom.addRule没有失败回调,注册前先探测本地字体文件是否存在,不存在时回退远程字体
92
+ const registerNvueFontFace = (iconUrl) => {
93
+ const addFontFace = (url) => {
94
+ addNvueFontRule(iconFontFamily, url);
95
+ if (config.customIcon.family) {
96
+ addNvueFontRule(config.customIcon.family, config.customIcon.url);
97
+ }
98
+ markFontLoaded();
99
+ };
100
+ if (!useAppStaticIconFont || iconUrl === config.iconUrl) {
101
+ addFontFace(iconUrl);
102
+ return;
103
+ }
104
+ if (typeof plus === 'undefined' || !plus.io || typeof plus.io.resolveLocalFileSystemURL !== 'function') {
105
+ addFontFace(iconUrl);
106
+ return;
107
+ }
108
+ const localFontPath = typeof plus.io.convertLocalFileSystemURL === 'function'
109
+ ? plus.io.convertLocalFileSystemURL(appIconFontUrl)
110
+ : appIconFontUrl;
111
+ try {
112
+ plus.io.resolveLocalFileSystemURL(localFontPath, () => addFontFace(iconUrl), () => addFontFace(config.iconUrl));
113
+ } catch (e) {
114
+ addFontFace(iconUrl);
115
+ }
116
+ };
117
+ // #endif
118
+
119
+ // #ifdef APP-VUE
120
+ const registerAppVueFontFace = (iconUrl, appVuePage) => {
121
+ uni.loadFontFace({
122
+ global: true, // 是否全局生效。微信小程序 '2.10.0'起支持全局生效,需在 app.vue 中调用。
123
+ family: iconFontFamily,
124
+ source: 'url("' + iconUrl + '")',
125
+ success() {
126
+ appVueLoadingPages.delete(appVuePage);
127
+ appVueLoadedPages.add(appVuePage);
128
+ markFontLoaded();
129
+ // console.log('内置字体图标加载成功');
130
+ },
131
+ fail() {
132
+ // 本地字体缺失时回退远程字体,保证未启用本地字体的老项目仍可显示图标
133
+ if (iconUrl !== config.iconUrl && !appVueFallbackPages.has(appVuePage)) {
134
+ appVueFallbackPages.add(appVuePage);
135
+ registerAppVueFontFace(config.iconUrl, appVuePage);
136
+ return;
137
+ }
138
+ appVueLoadingPages.delete(appVuePage);
139
+ // console.error('内置字体图标加载出错');
140
+ }
141
+ });
142
+ };
143
+ // #endif
144
+
78
145
  // 加载字体方法
79
146
  const loadFont = () => {
80
147
  // #ifdef APP-VUE
@@ -94,42 +161,27 @@ const loadFont = () => {
94
161
  // #ifdef APP-NVUE
95
162
  // nvue通过weex的dom模块引入字体,相关文档地址如下:
96
163
  // https://weex.apache.org/zh/docs/modules/dom.html#addrule
97
- const domModule = weex.requireModule('dom');
98
- domModule.addRule('fontFace', {
99
- 'fontFamily': iconFontFamily,
100
- 'src': `url('${iconUrl}')`
101
- });
102
- if (config.customIcon.family) {
103
- domModule.addRule('fontFace', {
104
- 'fontFamily': config.customIcon.family,
105
- 'src': `url('${config.customIcon.url}')`
106
- });
107
- }
108
- markFontLoaded();
164
+ registerNvueFontFace(iconUrl);
109
165
  // #endif
110
- // #ifdef APP-VUE || H5 || MP-WEIXIN || MP-ALIPAY
166
+ // #ifdef APP-VUE
167
+ registerAppVueFontFace(iconUrl, appVuePage);
168
+ // #endif
169
+ // #ifdef H5 || MP-WEIXIN || MP-ALIPAY
111
170
  uni.loadFontFace({
112
171
  global: true, // 是否全局生效。微信小程序 '2.10.0'起支持全局生效,需在 app.vue 中调用。
113
172
  family: iconFontFamily,
114
173
  source: 'url("' + iconUrl + '")',
115
174
  success() {
116
- // #ifdef APP-VUE
117
- appVueLoadingPages.delete(appVuePage);
118
- appVueLoadedPages.add(appVuePage);
119
- // #endif
120
175
  markFontLoaded();
121
176
  // console.log('内置字体图标加载成功');
122
177
  },
123
178
  fail() {
124
- // #ifdef APP-VUE
125
- appVueLoadingPages.delete(appVuePage);
126
- // #endif
127
- // #ifndef APP-VUE
128
179
  params.loading = false;
129
- // #endif
130
180
  // console.error('内置字体图标加载出错');
131
181
  }
132
182
  });
183
+ // #endif
184
+ // #ifdef APP-VUE || H5 || MP-WEIXIN || MP-ALIPAY
133
185
  if (config.customIcon.family) {
134
186
  uni.loadFontFace({
135
187
  global: true, // 是否全局生效。微信小程序 '2.10.0'起支持全局生效,需在 app.vue 中调用。
@@ -144,14 +196,6 @@ const loadFont = () => {
144
196
  });
145
197
  }
146
198
  // #endif
147
- // #ifdef APP-NVUE
148
- // if (this.customFontFamily) {
149
- // domModule.addRule('fontFace', {
150
- // 'fontFamily': `${this.customPrefix}-${this.customFontFamily}`,
151
- // 'src': `url('${this.customFontUrl}')`
152
- // })
153
- // }
154
- // #endif
155
199
  return true;
156
200
  };
157
201
 
@@ -127,7 +127,9 @@
127
127
  // 文字内容的样式
128
128
  textStyle() {
129
129
  let style = {}
130
- style.whiteSpace = 'nowrap !important'
130
+ // innerText把文本切分到多个text标签,切分点正好是空格时,nowrap会把它
131
+ // 当做行首/行尾空白丢弃,所以用pre保留原文空白,pre同样不会换行
132
+ style.whiteSpace = 'pre !important'
131
133
  style.color = this.color
132
134
  style.fontSize = addUnit(this.fontSize)
133
135
  return style
@@ -312,7 +314,8 @@
312
314
  // 这一句很重要,为了能让滚动左右连接起来
313
315
  padding-left: 100%;
314
316
  word-break: keep-all;
315
- white-space: nowrap;
317
+ // pre而非nowrap:切分后的空格落在text首尾时不能被折叠掉
318
+ white-space: pre;
316
319
  animation: u-loop-animation 10s linear infinite both;
317
320
  /* #endif */
318
321
  @include flex(row);
@@ -10,6 +10,7 @@
10
10
  @touchstart="touchStart"
11
11
  @touchmove="touchMove"
12
12
  @touchend="touchEnd"
13
+ @ready="onCanvasReady"
13
14
  :disable-scroll="true"
14
15
  class="u-signature__canvas"
15
16
  :style="{
@@ -75,6 +76,7 @@
75
76
 
76
77
  <script>
77
78
  import { t } from '../../libs/i18n'
79
+ import { upGetRect } from '../../libs/function/index.js'
78
80
  export default {
79
81
  name: 'u-signature',
80
82
  props: {
@@ -132,9 +134,11 @@
132
134
  ],
133
135
  showBrushSettings: false,
134
136
  showColorSettings: false,
135
- lastPoint: null, // 保存上一个点的坐标
136
- canvasInstance: null // 缓存canvas实例
137
- }
137
+ lastPoint: null, // 保存上一个点的坐标
138
+ canvasInstance: null, // 缓存canvas实例
139
+ canvasReady: false, // up-canvas 是否已完成初始化(APP/NVUE 需异步)
140
+ canvasRect: { left: 0, top: 0 } // 画布在视口中的位置,用于坐标换算
141
+ }
138
142
  },
139
143
  computed: {
140
144
  resolvedBgColor() {
@@ -150,10 +154,9 @@
150
154
  }
151
155
  },
152
156
  mounted() {
153
- // 初始化时获取canvas实例
157
+ // 等待 up-canvas 完成初始化(APP / NVUE 画布上下文需异步就绪)
154
158
  this.$nextTick(() => {
155
- this.getCanvasInstance();
156
- this.clearCanvas();
159
+ this.initCanvasInstance();
157
160
  });
158
161
  },
159
162
  watch: {
@@ -192,110 +195,168 @@
192
195
  },
193
196
 
194
197
  // 获取签名画布实例
195
- getCanvasInstance() {
196
- if (this.canvasInstance) {
197
- return this.canvasInstance;
198
+ getCanvasInstance() {
199
+ if (this.canvasInstance) {
200
+ return this.canvasInstance;
201
+ }
202
+
203
+ const canvasRef = this.$refs.signatureCanvas;
204
+ if (canvasRef) {
205
+ this.canvasInstance = canvasRef;
206
+ // 实例已就绪则同步标记,避免首笔被 touchStart 拦截
207
+ if (canvasRef.ctx) {
208
+ this.canvasReady = true;
198
209
  }
199
-
200
- const canvasRef = this.$refs.signatureCanvas;
201
- if (canvasRef) {
202
- this.canvasInstance = canvasRef;
203
- return canvasRef;
210
+ return canvasRef;
211
+ }
212
+ return null;
213
+ },
214
+
215
+ // 主动等待 up-canvas 完成初始化(部分平台画布上下文为异步)
216
+ async initCanvasInstance() {
217
+ const ref = this.getCanvasInstance();
218
+ if (!ref) return;
219
+ if (typeof ref.initCanvas === 'function') {
220
+ try {
221
+ const ok = await ref.initCanvas(true);
222
+ if (ok) {
223
+ this.onCanvasReady();
224
+ return;
225
+ }
226
+ } catch (e) {
227
+ // 忽略,等待 @ready 兜底
204
228
  }
205
- return null;
206
- },
229
+ }
230
+ // 若 initCanvas 暂未就绪,up-canvas 的 @ready 事件会触发 onCanvasReady
231
+ },
232
+
233
+ // up-canvas 初始化完成回调(@ready 或 initCanvas 成功后触发)
234
+ onCanvasReady() {
235
+ if (this.canvasReady) return;
236
+ this.canvasReady = true;
237
+ this.getCanvasInstance();
238
+ this.refreshCanvasRect();
239
+ this.clearCanvas();
240
+ },
241
+
242
+ // 缓存画布在视口中的位置,用于将视口坐标换算为画布坐标
243
+ refreshCanvasRect() {
244
+ if (typeof upGetRect !== 'function') return;
245
+ upGetRect('#' + this.canvasId, false, this)
246
+ .then((rect) => {
247
+ if (rect && (rect.left || rect.top || rect.width)) {
248
+ this.canvasRect = {
249
+ left: rect.left || 0,
250
+ top: rect.top || 0
251
+ };
252
+ }
253
+ })
254
+ .catch(() => {});
255
+ },
207
256
 
208
- touchStart(e) {
209
- if (!this.canvasInstance || !this.canvasInstance.ctx) {
210
- this.getCanvasInstance();
257
+ touchStart(e) {
258
+ if (!this.canvasReady || !this.canvasInstance || !this.canvasInstance.ctx) {
259
+ // 画布尚未就绪:尝试初始化一次,待 @ready 后下一次触摸即可绘制
260
+ this.getCanvasInstance();
261
+ if (this.canvasInstance && typeof this.canvasInstance.initCanvas === 'function') {
262
+ this.canvasInstance.initCanvas(true)
263
+ .then(() => {
264
+ this.canvasReady = true;
265
+ this.refreshCanvasRect();
266
+ })
267
+ .catch(() => {});
211
268
  }
212
-
213
- if (!this.canvasInstance || !this.canvasInstance.ctx) return;
214
-
215
- this.isDrawing = true;
216
- this.isEmpty = false;
217
- this.currentPath = [];
218
-
219
- const { x, y } = this.getCanvasPoint(e);
220
-
221
- // 设置线条样式
222
- this.canvasInstance.setLineStyle(this.lineColor, this.lineWidth);
223
-
224
- // 开始路径
225
- this.canvasInstance.beginPath();
226
- this.canvasInstance.moveTo(x, y);
227
-
228
- // 记录起始点
229
- this.currentPath.push({
230
- x,
231
- y,
232
- type: 'start',
233
- color: this.lineColor,
234
- width: this.lineWidth
235
- });
236
-
237
- // 保存上一个点
238
- this.lastPoint = { x, y };
239
-
240
- // 阻止默认事件以提高性能
241
- e.preventDefault();
242
- },
269
+ return;
270
+ }
243
271
 
244
- touchMove(e) {
245
- if (!this.isDrawing || !this.canvasInstance || !this.canvasInstance.ctx) return;
246
-
247
- // 阻止默认事件以提高性能
248
- e.preventDefault();
249
-
250
- const { x, y } = this.getCanvasPoint(e);
251
-
252
- // 从上一个点画线到当前点
253
- this.canvasInstance.lineTo(x, y);
254
- this.canvasInstance.stroke(); // 实时绘制当前线段
255
- this.currentPath.push({
256
- x,
257
- y,
258
- type: 'move'
259
- });
260
- this.canvasInstance.draw(false);
261
-
262
- // 更新上一个点
263
- this.lastPoint = { x, y };
264
- },
272
+ this.isDrawing = true;
273
+ this.isEmpty = false;
274
+ this.currentPath = [];
265
275
 
266
- touchEnd(e) {
267
- if (!this.isDrawing || !this.canvasInstance || !this.canvasInstance.ctx) return;
268
-
269
- this.isDrawing = false;
270
- this.canvasInstance.closePath();
271
- this.lastPoint = null;
272
-
273
- // 将当前路径加入栈中用于回退
274
- if (this.currentPath.length > 0) {
275
- this.pathStack.push([...this.currentPath]);
276
- }
277
-
278
- // 最后统一执行一次绘制
279
- this.canvasInstance.draw(true);
280
- },
276
+ const { x, y } = this.getCanvasPoint(e);
281
277
 
282
- // 同步获取canvas坐标点(兼容处理)
283
- getCanvasPoint(e) {
284
- // #ifdef MP-WEIXIN
285
- const touch = e.touches && e.touches[0] ? e.touches[0] : e.mp.touches[0];
286
- // #endif
287
- // #ifndef MP-WEIXIN
288
- const touch = e.touches[0];
289
- // #endif
290
-
291
- // 计算相对于canvas的坐标
292
- // 由于无法直接获取canvas位置,这里简化处理
293
- // 实际应用中可能需要通过uni.createSelectorQuery获取canvas位置
294
- return {
295
- x: touch.x,
296
- y: touch.y
297
- };
298
- },
278
+ // 设置线条样式(每次起笔确保样式生效,兼容旧版 canvas 的 draw 后状态重置)
279
+ this.canvasInstance.setLineStyle(this.lineColor, this.lineWidth);
280
+
281
+ // 仅记录起点,真正的绘制在 touchMove 中以“增量线段”方式完成
282
+ this.lastPoint = { x, y };
283
+ this.currentPath.push({
284
+ x,
285
+ y,
286
+ type: 'start',
287
+ color: this.lineColor,
288
+ width: this.lineWidth
289
+ });
290
+
291
+ e.preventDefault();
292
+ },
293
+
294
+ touchMove(e) {
295
+ if (!this.isDrawing || !this.canvasInstance || !this.canvasInstance.ctx) return;
296
+
297
+ e.preventDefault();
298
+
299
+ const { x, y } = this.getCanvasPoint(e);
300
+
301
+ // 增量绘制:以上一个点为起点画到当前点。
302
+ // 旧版 canvas(APP-PLUS)每次 draw 会刷新命令队列,必须以“单段”方式绘制;
303
+ // 2D canvas 亦适用(draw 为 no-op,stroke 即时生效并保留已有像素)。
304
+ this.canvasInstance.setLineStyle(this.lineColor, this.lineWidth);
305
+ this.canvasInstance.beginPath();
306
+ this.canvasInstance.moveTo(this.lastPoint.x, this.lastPoint.y);
307
+ this.canvasInstance.lineTo(x, y);
308
+ this.canvasInstance.stroke();
309
+ this.canvasInstance.draw(true); // reserve:保留已绘制内容,避免笔迹闪退
310
+
311
+ this.currentPath.push({
312
+ x,
313
+ y,
314
+ type: 'move'
315
+ });
316
+ this.lastPoint = { x, y };
317
+ },
318
+
319
+ touchEnd(e) {
320
+ if (!this.isDrawing || !this.canvasInstance || !this.canvasInstance.ctx) return;
321
+
322
+ this.isDrawing = false;
323
+ this.lastPoint = null;
324
+
325
+ // 将当前路径加入栈中用于回退
326
+ if (this.currentPath.length > 0) {
327
+ this.pathStack.push([...this.currentPath]);
328
+ }
329
+
330
+ // 收尾绘制(保留已有内容)
331
+ this.canvasInstance.draw(true);
332
+ },
333
+
334
+ // 同步获取canvas坐标点(兼容各平台)
335
+ getCanvasPoint(e) {
336
+ const touch =
337
+ (e.touches && e.touches[0]) ||
338
+ (e.changedTouches && e.changedTouches[0]) ||
339
+ null;
340
+ if (!touch) return { x: 0, y: 0 };
341
+
342
+ // 2D canvas(MP/H5)与 NVUE webview 会在事件上直接挂载画布相对坐标
343
+ if (typeof touch.x === 'number' && typeof touch.y === 'number') {
344
+ return { x: touch.x, y: touch.y };
345
+ }
346
+
347
+ // 旧版 canvas(如 APP-PLUS)的 touch 仅提供视口坐标,需减去画布位置
348
+ const rect = this.canvasRect || { left: 0, top: 0 };
349
+ const clientX = touch.clientX !== undefined
350
+ ? touch.clientX
351
+ : (touch.pageX !== undefined ? touch.pageX : 0);
352
+ const clientY = touch.clientY !== undefined
353
+ ? touch.clientY
354
+ : (touch.pageY !== undefined ? touch.pageY : 0);
355
+ return {
356
+ x: clientX - rect.left,
357
+ y: clientY - rect.top
358
+ };
359
+ },
299
360
 
300
361
  // 选择颜色
301
362
  selectColor(color) {
@@ -42,7 +42,8 @@ export const props = defineMixin({
42
42
  // 双滑块时值
43
43
  rangeValue: {
44
44
  type: [Array],
45
- default: [0, 0]
45
+ // 必须用工厂函数,否则所有实例共用同一个数组
46
+ default: () => [0, 0]
46
47
  },
47
48
  // 滑块右侧已选择部分的背景色
48
49
  activeColor: {
@@ -126,7 +126,7 @@
126
126
  export default {
127
127
  name: 'u-slider',
128
128
  mixins: [mpMixin, mixin, props],
129
- emits: ["start", "changing", "change", "update:modelValue"],
129
+ emits: ["start", "changing", "change", "update:modelValue", "update:rangeValue"],
130
130
  data() {
131
131
  return {
132
132
  startX: 0,
@@ -462,8 +462,14 @@
462
462
  }
463
463
  // 修改value值
464
464
  if (this.isRange) {
465
- this.rangeValue[index] = valueFormat;
466
- this.$emit("update:modelValue", this.rangeValue);
465
+ // 值没有变化时不再抛出事件,避免父级对数组做拷贝或归一化时形成更新回环
466
+ if (this.rangeValue[index] !== valueFormat) {
467
+ this.rangeValue[index] = valueFormat;
468
+ // 区间模式由rangeValue双向绑定,必须抛出update:rangeValue,v-model:rangeValue才能生效
469
+ this.$emit("update:rangeValue", this.rangeValue);
470
+ // 兼容此前监听update:modelValue获取区间值的用法
471
+ this.$emit("update:modelValue", this.rangeValue);
472
+ }
467
473
  } else {
468
474
  // #ifdef VUE3
469
475
  this.$emit("update:modelValue", valueFormat);
@@ -8,8 +8,12 @@ let flag;
8
8
  * @param {Boolean} immediate 是否立即执行
9
9
  * @return null
10
10
  */
11
- export function throttle(func, wait = 500, immediate = true) {
12
- if (immediate) {
11
+ export function throttle(func, wait = 500, immediate = true) {
12
+ if (wait <= 0) {
13
+ typeof func === 'function' && func()
14
+ return
15
+ }
16
+ if (immediate) {
13
17
  if (!flag) {
14
18
  flag = true
15
19
  // 如果是立即执行,则在wait毫秒内开始时执行