uview-plus 3.8.112 → 3.8.117

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,65 @@
1
+ ## 3.8.117
2
+ fix: 修复瀑布流分发锁死、u-image 置于 u-swiper 内无法滑动、u-list-item 挂载竞态
3
+
4
+ 本版为三个问题的集中修复,均不涉及组件 API 变更。
5
+
6
+ fix: 修复 u-waterfall 在 App 切换 tabbar 后不再消费新数据(#1056)
7
+
8
+ App 端切换 tabbar 时,原生层会挂起隐藏页面的 setTimeout,`await sleep()` 永不 resolve,`runDistributionQueue` 的 finally 块无法执行,`distributionRunning` 永久为 true;切回页面后新数据只能入队,永远不会被分发。
9
+
10
+ - `clear()` 强制重置 `distributionRunning` 与 `distributionPromise`
11
+ - `redistributeData()` 在调用 clear 之前先解锁
12
+ - 新增 `verify:waterfall-lock-reset` 回归校验
13
+
14
+ fix: 修复 u-image 置于 u-swiper 内导致手势滑动失效(#894)
15
+
16
+ u-transition 根节点绑定的 `@touchmove="noop"` 会执行 `e.stopPropagation()`,使 touchmove 无法冒泡到父级;u-image 以 u-transition 作为根节点,放进 u-swiper 的 slot 后 swiper 收不到 touchmove,滑动手势失效。该拦截对淡入、滑入等过渡动画本身没有作用,仅会阻断父组件的正常手势,故直接移除。
17
+
18
+ fix: 修复 u-list-item 在 parent 未初始化时调用 updateOffsetFromChild 报错(#875)
19
+
20
+ 列表数据由后台异步返回、u-list-item 数量随之变化时,`uList.innerScrollTop` 的 watch 可能在 `mounted()` 之前触发;此时 `this.parent` 仍是 `created()` 中赋值的空对象,调用 `this.parent.updateOffsetFromChild` 会抛出 "is not a function"。
21
+
22
+ - watch 回调中增加守卫,parent 未就绪时直接返回
23
+ - 新增 `verify:list-item-parent-race` 回归校验
24
+
25
+ ## 3.8.116
26
+ fix: 修复组件卸载后查询节点导致 APP 与鸿蒙端报错(#1057)
27
+
28
+ 页面切换时 APP 与鸿蒙端会出现大面积 `Uncaught TypeError: Cannot read properties of undefined (reading '$') at uni-app-view.umd.js`,功能不受影响但日志被淹没,H5 端不出现。
29
+
30
+ 原因是组件卸载后仍在执行的异步回调继续发起节点查询:APP 端 service 层取的是已失效的 `$el.nodeId`,视图层按该 id 查映射表时,对应条目在元素移除时已被删除,读到 undefined 再取 `.$` 即抛错。H5 端直接取 `$el`,没有这层 nodeId 映射,因此不复现。
31
+
32
+ - 在 `upGetRect` 与 `upCreateIntersectionObserver` 中集中拦截,覆盖全部三十余处调用方,卸载后不再把组件交给原生查询
33
+ - 卸载后 `upGetRect` 返回零尺寸节点信息,`upCreateIntersectionObserver` 返回空观察器,均保持原有调用形态,不需要调用方改写
34
+ - 卸载标记由公共 mixin 的 `beforeUnmount` 同步置位,早于 Vue 内部异步置位的 `isUnmounted`;未套用公共 mixin 的组件退回 Vue 自身标记
35
+ - 守卫置于条件编译之外,确保 app-harmony 端同样生效
36
+ - 新增 `verify:unmounted-node-query` 回归校验,同时断言挂载期间必须正常发起查询、卸载后必须完全不发起,并逐平台校验守卫存在
37
+
38
+ 该问题在 tabs、sticky、subsection、waterfall、index-list 等所有依赖节点测量的组件上成因相同,本次为统一修复。
39
+
40
+ ## 3.8.115
41
+ feat: u-select 遮罩默认可见并支持点击遮罩关闭开关
42
+
43
+ - overlayOpacity 默认由 0.01 调整为 0.3,遮罩真正可见,仍比 up-overlay 自身的 0.5 更浅
44
+ - overlayOpacity 类型放宽为 String / Number,可直接以普通属性方式传值
45
+ - 展开状态下把触发区抬到遮罩之上,避免下拉面板亮着而它的锚点被压暗
46
+ - 新增 closeOnClickOverlay 属性(默认 true),与 u-popup 命名一致;此前点击遮罩关闭为硬编码行为,现在可显式禁用
47
+ - 遮罩标签统一为 up-overlay,并补充 verify:select-overlay 回归校验
48
+
49
+ nvue 端对 z-index 支持有限,若被忽略则触发区可能落在遮罩之下,该端建议按需关闭 overlay。
50
+
51
+ ## 3.8.114
52
+ fix: 修复 tabbar 中间按钮圆弧越界(#1052)
53
+
54
+ - 根据 tabbar 与中间按钮的实际位置计算圆弧裁剪高度
55
+ - 兼容文字、偏移、边框和动画场景,并补充回归校验
56
+
57
+ ## 3.8.113(2026-08-27)
58
+ fix: 修复启用uniUpRoot,在root组件存在components选项时组件插入错误
59
+
60
+ fix: 修复打包APK运行时icon组件未正确获取字体路径
61
+
62
+
1
63
  ## 3.8.112
2
64
  feat: navbar 新增 iOS 大标题模式
3
65
 
@@ -348,7 +348,7 @@
348
348
  overflow: hidden;
349
349
  height: 800rpx;
350
350
 
351
- >view {
351
+ >.u-flex {
352
352
  width: 150%;
353
353
  transition: transform 0.3s ease-in-out 0s;
354
354
  transform: translateX(0);
@@ -7,25 +7,25 @@
7
7
  <view class="oper-wrapper" :style="{ display: styleDisplay }">
8
8
  <view class="oper">
9
9
  <view class="btn-wrapper" v-if="showOper">
10
- <view @click="select" hover-class="hover" :style="{ width: btnWidth }">
10
+ <view class="u-cropper__action" @click="select" hover-class="hover" :style="{ width: btnWidth }">
11
11
  <text>{{ t("up.common.re-select") }}</text>
12
12
  </view>
13
- <view @click="close" hover-class="hover" :style="{ width: btnWidth }">
13
+ <view class="u-cropper__action" @click="close" hover-class="hover" :style="{ width: btnWidth }">
14
14
  <text>{{ t("up.common.close") }}</text>
15
15
  </view>
16
- <view @click="rotate" hover-class="hover" :style="{ width: btnWidth, display: btnDsp }">
16
+ <view class="u-cropper__action" @click="rotate" hover-class="hover" :style="{ width: btnWidth, display: btnDsp }">
17
17
  <text>{{ t("up.common.rotate") }}</text>
18
18
  </view>
19
- <view @click="preview" hover-class="hover" :style="{ width: btnWidth }">
19
+ <view class="u-cropper__action" @click="preview" hover-class="hover" :style="{ width: btnWidth }">
20
20
  <text>{{ t("up.common.preview") }}</text>
21
21
  </view>
22
- <view @click="confirm" hover-class="hover" :style="{ width: btnWidth }">
22
+ <view class="u-cropper__action" @click="confirm" hover-class="hover" :style="{ width: btnWidth }">
23
23
  <text>{{ t("up.common.confirm") }}</text>
24
24
  </view>
25
25
  </view>
26
26
  <view class="clr-wrapper" v-else>
27
27
  <slider class="my-slider" @change="colorChange" block-size="25" value="0" min="-100" max="100" activeColor="red" backgroundColor="green" block-color="grey" show-value></slider>
28
- <view @click="prvUpload" hover-class="hover" :style="{ width: btnWidth }">
28
+ <view class="u-cropper__action" @click="prvUpload" hover-class="hover" :style="{ width: btnWidth }">
29
29
  <text>{{ t("up.common.confirm") }}</text>
30
30
  </view>
31
31
  </view>
@@ -1267,7 +1267,7 @@
1267
1267
  justify-content: space-between;
1268
1268
  }
1269
1269
 
1270
- .btn-wrapper view {
1270
+ .btn-wrapper .u-cropper__action {
1271
1271
  display: flex;
1272
1272
  align-items: center;
1273
1273
  justify-content: center;
@@ -1288,7 +1288,7 @@
1288
1288
  flex-grow: 1;
1289
1289
  }
1290
1290
 
1291
- .clr-wrapper view {
1291
+ .clr-wrapper .u-cropper__action {
1292
1292
  display: flex;
1293
1293
  align-items: center;
1294
1294
  justify-content: center;
@@ -159,7 +159,7 @@ export default {
159
159
  bottom: 0px;
160
160
  display: flex;
161
161
  flex-direction: column;
162
- >view {
162
+ >.u-float-button__item {
163
163
  margin: 5px 0px;
164
164
  }
165
165
  }
@@ -16,7 +16,7 @@
16
16
  <view class="up-goods-sku__header">
17
17
  <slot name="header">
18
18
  <view class="up-goods-sku__header__image">
19
- <image :src="goodsInfo.image || goodsInfo.picture" mode="aspectFill"></image>
19
+ <image class="up-goods-sku__header__image-element" :src="goodsInfo.image || goodsInfo.picture" mode="aspectFill"></image>
20
20
  </view>
21
21
  <view class="up-goods-sku__header__info">
22
22
  <view class="up-goods-sku__header__info__price">
@@ -333,7 +333,7 @@
333
333
  overflow: hidden;
334
334
  margin-right: 20rpx;
335
335
 
336
- image {
336
+ &-element {
337
337
  width: 100%;
338
338
  height: 100%;
339
339
  }
@@ -44,7 +44,8 @@ const getAppIconUrl = () => {
44
44
  }
45
45
  if (typeof plus !== 'undefined' && plus.io && typeof plus.io.convertLocalFileSystemURL === 'function') {
46
46
  const iconFontPath = plus.io.convertLocalFileSystemURL(appIconFontUrl);
47
- if (iconFontPath) return iconFontPath;
47
+ // if (iconFontPath) return iconFontPath;
48
+ return iconFontPath && iconFontPath.startsWith('file://') ? iconFontPath : 'file://' + iconFontPath;
48
49
  }
49
50
  return appIconFontUrl;
50
51
  // #endif
@@ -49,6 +49,10 @@
49
49
  watch: {
50
50
  // #ifndef APP-NVUE
51
51
  'uList.innerScrollTop'(n) {
52
+ // 确保parent已正确初始化,避免在mounted之前触发watch导致报错
53
+ if (!this.parent || !this.parent.updateOffsetFromChild) {
54
+ return
55
+ }
52
56
  const preLoadScreen = this.uList.preLoadScreen
53
57
  const windowHeight = this.sys.windowHeight
54
58
  if(n <= windowHeight * preLoadScreen) {
@@ -1,3 +1,16 @@
1
+ /* #ifdef MP-WEIXIN */
2
+ :host {
3
+ --up-navbar-bg-color: #ffffff;
4
+ }
5
+
6
+ @media (prefers-color-scheme: dark) {
7
+ :host {
8
+ --up-navbar-bg-color: #1c1c1e;
9
+ }
10
+ }
11
+ /* #endif */
12
+
13
+ /* #ifndef MP-WEIXIN */
1
14
  :root,
2
15
  page,
3
16
  body {
@@ -24,3 +37,4 @@ body {
24
37
  --up-navbar-bg-color: #1c1c1e;
25
38
  --up-navbar-glass-bg-color: rgba(28, 28, 30, 0.82);
26
39
  }
40
+ /* #endif */
@@ -1,3 +1,18 @@
1
+ /* #ifdef MP-WEIXIN */
2
+ :host {
3
+ --up-notice-bar-color: #f9ae3d;
4
+ --up-notice-bar-bg-color: #fdf6ec;
5
+ }
6
+
7
+ @media (prefers-color-scheme: dark) {
8
+ :host {
9
+ --up-notice-bar-color: #f9ae3d;
10
+ --up-notice-bar-bg-color: #3d2f1b;
11
+ }
12
+ }
13
+ /* #endif */
14
+
15
+ /* #ifndef MP-WEIXIN */
1
16
  :root,
2
17
  page,
3
18
  body {
@@ -23,3 +38,4 @@ body {
23
38
  --up-notice-bar-color: #f9ae3d;
24
39
  --up-notice-bar-bg-color: #3d2f1b;
25
40
  }
41
+ /* #endif */
@@ -53,14 +53,14 @@
53
53
  <!-- #endif -->
54
54
  <view v-else-if="(n.name==='table'&&n.c)||n.name==='li'" :id="n.attrs.id" :class="'_'+n.name+' '+n.attrs.class" :style="n.attrs.style">
55
55
  <node v-if="n.name==='li'" :childs="n.children" :opts="opts" />
56
- <view v-else v-for="(tbody, x) in n.children" v-bind:key="x" :class="'_'+tbody.name+' '+tbody.attrs.class" :style="tbody.attrs.style">
56
+ <view v-else v-for="(tbody, x) in (n && n.children || [])" v-bind:key="x" :class="'_'+tbody.name+' '+tbody.attrs.class" :style="tbody.attrs.style">
57
57
  <node v-if="tbody.name==='td'||tbody.name==='th'" :childs="tbody.children" :opts="opts" />
58
- <block v-else v-for="(tr, y) in tbody.children" v-bind:key="y">
58
+ <block v-else v-for="(tr, y) in (tbody && tbody.children || [])" v-bind:key="y">
59
59
  <view v-if="tr.name==='td'||tr.name==='th'" :class="'_'+tr.name+' '+tr.attrs.class" :style="tr.attrs.style">
60
60
  <node :childs="tr.children" :opts="opts" />
61
61
  </view>
62
62
  <view v-else :class="'_'+tr.name+' '+tr.attrs.class" :style="tr.attrs.style">
63
- <view v-for="(td, z) in tr.children" v-bind:key="z" :class="'_'+td.name+' '+td.attrs.class" :style="td.attrs.style">
63
+ <view v-for="(td, z) in (tr && tr.children || [])" v-bind:key="z" :class="'_'+td.name+' '+td.attrs.class" :style="td.attrs.style">
64
64
  <node :childs="td.children" :opts="opts" />
65
65
  </view>
66
66
  </view>
@@ -70,7 +70,7 @@
70
70
  <!-- insert -->
71
71
  <!-- 富文本 -->
72
72
  <!-- #ifdef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->
73
- <rich-text v-else-if="!n.c&&!handler.isInline(n.name, n.attrs.style)" :id="n.attrs.id" :style="n.f" :user-select="opts[4]" :nodes="[n]" />
73
+ <rich-text v-else-if="!n.c&&!isInlineTag(n.name, n.attrs.style)" :id="n.attrs.id" :style="n.f" :user-select="opts[4]" :nodes="[n]" />
74
74
  <!-- #endif -->
75
75
  <!-- #ifndef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->
76
76
  <rich-text v-else-if="!n.c" :id="n.attrs.id" :style="'display:inline;'+n.f" :preview="false" :selectable="opts[4]" :user-select="opts[4]" :nodes="[n]" />
@@ -185,6 +185,25 @@ export default {
185
185
  // #ifdef MP-WEIXIN
186
186
  toJSON () { return this },
187
187
  // #endif
188
+ isInlineTag (tagName, style) {
189
+ return {
190
+ abbr: true,
191
+ b: true,
192
+ big: true,
193
+ code: true,
194
+ del: true,
195
+ em: true,
196
+ i: true,
197
+ ins: true,
198
+ label: true,
199
+ q: true,
200
+ small: true,
201
+ span: true,
202
+ strong: true,
203
+ sub: true,
204
+ sup: true
205
+ }[tagName] || (style || '').indexOf('display:inline') !== -1
206
+ },
188
207
  /**
189
208
  * @description 规范化链接地址
190
209
  * @param {any} href
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <view class="u-select">
3
3
  <view :class="['u-select__content', disabled && 'disabled']">
4
- <view :class="['u-select__label', border && 'u-select__label--border']" :style="selectLabelStyle" @click="openSelect">
4
+ <view :class="['u-select__label', border && 'u-select__label--border']" :style="selectLabelStyle" @click="labelClick">
5
5
  <slot name="text" :currentLabel="currentLabel">
6
6
  <text class="u-select__text" :style="{ color: resolvedTextColor }" v-if="showOptionsLabel">
7
7
  {{ currentLabel }}
@@ -14,8 +14,8 @@
14
14
  <up-icon name="arrow-down" :size="iconSize" :color="resolvedIconColor"></up-icon>
15
15
  </slot>
16
16
  </view>
17
- <u-overlay :show="isOpen" @click="overlayClick" v-if="overlay" :zIndex="zIndex" :duration="duration + 50"
18
- :customStyle="overlayStyle" :opacity="overlayOpacity" @touchmove.stop.prevent="noop"></u-overlay>
17
+ <up-overlay :show="isOpen" @click="overlayClick" v-if="overlay" :zIndex="zIndex" :duration="duration + 50"
18
+ :customStyle="overlayStyle" :opacity="overlayOpacity" @touchmove.stop.prevent="noop"></up-overlay>
19
19
  <view class="u-select__options__wrap" :style="optionsWrapStyle">
20
20
  <view class="u-select__options" :style="optionsStyle" v-if="isOpen">
21
21
  <slot name="options">
@@ -59,8 +59,8 @@
59
59
  default: true
60
60
  },
61
61
  overlayOpacity: {
62
- type: Number,
63
- default: 0.01
62
+ type: [String, Number],
63
+ default: 0.3
64
64
  },
65
65
  overlayStyle: {
66
66
  type: Object,
@@ -68,6 +68,11 @@
68
68
  return {}
69
69
  }
70
70
  },
71
+ // 点击遮罩是否关闭下拉面板
72
+ closeOnClickOverlay: {
73
+ type: Boolean,
74
+ default: true
75
+ },
71
76
  duration: {
72
77
  type: Number,
73
78
  default: 300
@@ -157,11 +162,17 @@
157
162
  return this.optionsWidth;
158
163
  },
159
164
  selectLabelStyle() {
160
- if (!this.border) return {};
161
- return {
162
- borderColor: this.upThemeVar('--up-border-color', '#dadbde'),
163
- backgroundColor: this.upThemeVar('--up-card-bg-color', '#ffffff')
164
- };
165
+ const style = {};
166
+ if (this.border) {
167
+ style.borderColor = this.upThemeVar('--up-border-color', '#dadbde');
168
+ style.backgroundColor = this.upThemeVar('--up-card-bg-color', '#ffffff');
169
+ }
170
+ // 遮罩可见时把触发区抬到遮罩之上,避免下拉面板亮着而它的锚点被压暗
171
+ if (this.isOpen && this.overlay) {
172
+ style.position = 'relative';
173
+ style.zIndex = this.zIndex + 1;
174
+ }
175
+ return style;
165
176
  },
166
177
  optionsWrapStyle() {
167
178
  const style = {
@@ -195,6 +206,14 @@
195
206
  }
196
207
  },
197
208
  methods: {
209
+ labelClick() {
210
+ // 触发区被抬到遮罩之上后,遮罩不再代收这次点击,需要自己处理收起
211
+ if (this.isOpen) {
212
+ this.closeSelect();
213
+ return;
214
+ }
215
+ this.openSelect();
216
+ },
198
217
  openSelect() {
199
218
  if (this.disabled) return;
200
219
  this.isOpen = true;
@@ -208,7 +227,8 @@
208
227
  this.isOpen = false;
209
228
  },
210
229
  overlayClick() {
211
- this.isOpen = false;
230
+ if (!this.closeOnClickOverlay) return;
231
+ this.closeSelect();
212
232
  },
213
233
  selectItem(item) {
214
234
  this.isOpen = false;
@@ -1,3 +1,26 @@
1
+ /* #ifdef MP-WEIXIN */
2
+ :host {
3
+ --up-subsection-bg-color: #eeeeef;
4
+ --up-subsection-bar-color: #ffffff;
5
+ --up-subsection-inactive-color: #303133;
6
+ --up-subsection-disabled-text-color: #c8c9cc;
7
+ --up-subsection-disabled-border-color: #d4d4d4;
8
+ --up-subsection-disabled-bar-color: #f5f5f5;
9
+ }
10
+
11
+ @media (prefers-color-scheme: dark) {
12
+ :host {
13
+ --up-subsection-bg-color: #2b2c30;
14
+ --up-subsection-bar-color: #3a3b40;
15
+ --up-subsection-inactive-color: #d1d5db;
16
+ --up-subsection-disabled-text-color: #6b7280;
17
+ --up-subsection-disabled-border-color: #3a3a3c;
18
+ --up-subsection-disabled-bar-color: #3a3a3c;
19
+ }
20
+ }
21
+ /* #endif */
22
+
23
+ /* #ifndef MP-WEIXIN */
1
24
  :root,
2
25
  page,
3
26
  body {
@@ -39,3 +62,4 @@ body {
39
62
  --up-subsection-disabled-border-color: #3a3a3c;
40
63
  --up-subsection-disabled-bar-color: #3a3a3c;
41
64
  }
65
+ /* #endif */
@@ -1,3 +1,26 @@
1
+ /* #ifdef MP-WEIXIN */
2
+ :host {
3
+ --up-switch-active-color: #3c9cff;
4
+ --up-switch-inactive-color: #ffffff;
5
+ --up-switch-dot-active-color: #ffffff;
6
+ --up-switch-dot-inactive-color: #ffffff;
7
+ --up-switch-loading-inactive-color: #aaabad;
8
+ --up-switch-border-color: rgba(0, 0, 0, 0.12);
9
+ }
10
+
11
+ @media (prefers-color-scheme: dark) {
12
+ :host {
13
+ --up-switch-active-color: #3c9cff;
14
+ --up-switch-inactive-color: #3a3a3c;
15
+ --up-switch-dot-active-color: #ffffff;
16
+ --up-switch-dot-inactive-color: #d1d5db;
17
+ --up-switch-loading-inactive-color: #9ca3af;
18
+ --up-switch-border-color: #4b5563;
19
+ }
20
+ }
21
+ /* #endif */
22
+
23
+ /* #ifndef MP-WEIXIN */
1
24
  :root,
2
25
  page,
3
26
  body {
@@ -39,3 +62,4 @@ body {
39
62
  --up-switch-loading-inactive-color: #9ca3af;
40
63
  --up-switch-border-color: #4b5563;
41
64
  }
65
+ /* #endif */
@@ -99,9 +99,14 @@
99
99
  this.itemShape,
100
100
  this.iconScale,
101
101
  this.textMode,
102
- this.borderColor
102
+ this.borderColor,
103
+ this.border,
104
+ this.customStyle
103
105
  ]
104
106
  },
107
+ midButtonBorderTopOffset() {
108
+ return this.border ? 0.25 : 0
109
+ },
105
110
  updatePlaceholder() {
106
111
  return [this.fixed, this.placeholder]
107
112
  }
@@ -0,0 +1,19 @@
1
+ const MID_BUTTON_SIZE = 64
2
+
3
+ export function clampMidButtonBorderClipHeight(value) {
4
+ const height = Number(value)
5
+ if (!Number.isFinite(height)) return 0
6
+ return Math.min(Math.max(height, 0), MID_BUTTON_SIZE)
7
+ }
8
+
9
+ export function calculateMidButtonBorderClipHeight({
10
+ contentTop,
11
+ circleTop,
12
+ borderTopOffset = 0
13
+ }) {
14
+ const targetTop = Number(contentTop)
15
+ const currentTop = Number(circleTop)
16
+ const borderOffset = Number(borderTopOffset)
17
+ if (![targetTop, currentTop, borderOffset].every(Number.isFinite)) return 0
18
+ return clampMidButtonBorderClipHeight(targetTop + borderOffset - currentTop)
19
+ }
@@ -8,38 +8,41 @@
8
8
  <view class="u-tabbar-item__content" :class="contentClassNames">
9
9
  <view class="u-tabbar-item__bubble" :class="bubbleClassNames">
10
10
  <view
11
+ ref="u-tabbar-item__icon--mid-button"
11
12
  class="u-tabbar-item__icon"
12
13
  :class="iconClassNames"
13
14
  >
14
- <view v-if="isMidButton" class="u-tabbar-item__mid-button-border">
15
+ <view v-if="isMidButton" class="u-tabbar-item__mid-button-border" :style="midButtonBorderStyle">
15
16
  <view class="u-tabbar-item__mid-button-border-circle" :style="midButtonBorderCircleStyle"></view>
16
17
  </view>
17
18
  <view v-if="isMidButton" class="u-tabbar-item__mid-button-inner"></view>
18
- <up-icon
19
- v-if="resolvedIconName"
20
- :name="resolvedIconName"
21
- :color="isMidButton ? resolvedMidButtonIconColor : (isActive ? resolvedActiveColor : resolvedInactiveColor)"
22
- :size="isMidButton ? midButtonIconSize : 24"
23
- :customStyle="midButtonIconStyle"
24
- ></up-icon>
25
- <template v-else>
26
- <slot
27
- v-if="isActive"
28
- name="active-icon"
29
- />
30
- <slot
31
- v-else
32
- name="inactive-icon"
33
- />
34
- </template>
35
- <u-badge
36
- absolute
37
- :offset="[0, dot ? '34rpx' : badge > 9 ? '14rpx' : '20rpx']"
38
- :customStyle="badgeStyle"
39
- :isDot="dot"
40
- :value="badge || (dot ? 1 : null)"
41
- :show="dot || badge > 0"
42
- ></u-badge>
19
+ <view class="u-tabbar-item__icon-content" :class="iconContentClassNames">
20
+ <up-icon
21
+ v-if="resolvedIconName"
22
+ :name="resolvedIconName"
23
+ :color="isMidButton ? resolvedMidButtonIconColor : (isActive ? resolvedActiveColor : resolvedInactiveColor)"
24
+ :size="isMidButton ? midButtonIconSize : 24"
25
+ :customStyle="midButtonIconStyle"
26
+ ></up-icon>
27
+ <template v-else>
28
+ <slot
29
+ v-if="isActive"
30
+ name="active-icon"
31
+ />
32
+ <slot
33
+ v-else
34
+ name="inactive-icon"
35
+ />
36
+ </template>
37
+ <u-badge
38
+ absolute
39
+ :offset="[0, dot ? '34rpx' : badge > 9 ? '14rpx' : '20rpx']"
40
+ :customStyle="badgeStyle"
41
+ :isDot="dot"
42
+ :value="badge || (dot ? 1 : null)"
43
+ :show="dot || badge > 0"
44
+ ></u-badge>
45
+ </view>
43
46
  </view>
44
47
 
45
48
  <slot name="text">
@@ -62,7 +65,8 @@
62
65
  import { props } from './props';
63
66
  import { mpMixin } from '../../libs/mixin/mpMixin';
64
67
  import { mixin } from '../../libs/mixin/mixin';
65
- import { addStyle, error } from '../../libs/function/index';
68
+ import { addStyle, error, sleep } from '../../libs/function/index';
69
+ import { calculateMidButtonBorderClipHeight } from './midButtonGeometry';
66
70
  /**
67
71
  * TabbarItem 底部导航栏子组件
68
72
  * @description 此组件提供了自定义tabbar的能力。
@@ -73,6 +77,13 @@
73
77
  * @property {Boolean} dot 是否显示圆点,将会覆盖badge参数(默认 false )
74
78
  * @property {String} text 描述文本
75
79
  * @property {Object | String} badgeStyle 控制徽标的位置,对象或者字符串形式,可以设置top和right属性(默认 'top: 6px;right:2px;' )
80
+ * @property {String} mode 模式,默认普通模式,midButton中间按钮模式
81
+ * @property {String} midButtonBgColor 中间按钮背景色
82
+ * @property {String} midButtonIconColor 中间按钮图标颜色
83
+ * @property {String | Number} midButtonIconSize 中间按钮图标大小(默认 26 )
84
+ * @property {String} midButtonBoxShadow 中间按钮外层阴影
85
+ * @property {String} midButtonInnerBoxShadow 中间按钮内层阴影
86
+ * @property {String | Number} midButtonOffsetY 中间按钮垂直偏移,负值为上移(默认 -10 )
76
87
  * @property {Object} customStyle 定义需要用到的外部样式
77
88
  *
78
89
  * @example <u-tabbar :value="value2" :placeholder="false" @change="name => value2 = name" :fixed="false" :safeAreaInsetBottom="false"><u-tabbar-item text="首页" icon="home" dot ></u-tabbar-item></u-tabbar>
@@ -83,6 +94,7 @@
83
94
  data() {
84
95
  return {
85
96
  isActive: false, // 是否处于激活状态
97
+ midButtonBorderClipHeightValue: 0,
86
98
  parentData: {
87
99
  value: null,
88
100
  activeColor: '',
@@ -94,7 +106,9 @@
94
106
  itemShape: 'default',
95
107
  iconScale: 1.1,
96
108
  textMode: 'always',
97
- borderColor: ''
109
+ borderColor: '',
110
+ border: true,
111
+ midButtonBorderTopOffset: 0.25
98
112
  }
99
113
  }
100
114
  },
@@ -154,9 +168,12 @@
154
168
  return `${this.resolvedMidButtonOffsetY}px`
155
169
  },
156
170
  midButtonBorderClipHeight() {
157
- const clipBaseHeight = this.hasMidButtonText ? 15.5 : 7
158
- const clipHeight = clipBaseHeight - this.resolvedMidButtonOffsetY
159
- return `${Math.min(Math.max(clipHeight, 0), 64)}px`
171
+ return `${this.midButtonBorderClipHeightValue}px`
172
+ },
173
+ midButtonBorderStyle() {
174
+ return {
175
+ height: this.midButtonBorderClipHeight
176
+ }
160
177
  },
161
178
  midButtonBorderCircleStyle() {
162
179
  return this.isMidButton && this.parentData.borderColor
@@ -180,7 +197,15 @@
180
197
  return [
181
198
  this.isMidButton ? 'u-tabbar-item__icon--mid-button' : '',
182
199
  `u-tabbar-item__icon--${this.resolvedStyleType}`,
183
- this.isActive && this.resolvedAnimationType !== 'none' ? `u-tabbar-item__icon--anim-${this.resolvedAnimationType}` : ''
200
+ this.isActive && this.resolvedAnimationType !== 'none' && !this.isMidButton ? `u-tabbar-item__icon--anim-${this.resolvedAnimationType}` : ''
201
+ ]
202
+ },
203
+ iconContentClassNames() {
204
+ return [
205
+ this.isMidButton ? 'u-tabbar-item__icon-content--mid-button' : '',
206
+ this.isMidButton && this.isActive && this.resolvedAnimationType !== 'none'
207
+ ? `u-tabbar-item__icon-content--anim-${this.resolvedAnimationType}`
208
+ : ''
184
209
  ]
185
210
  },
186
211
  contentClassNames() {
@@ -213,6 +238,20 @@
213
238
  created() {
214
239
  this.init()
215
240
  },
241
+ mounted() {
242
+ this.scheduleMidButtonBorderMeasure()
243
+ },
244
+ watch: {
245
+ mode() {
246
+ this.scheduleMidButtonBorderMeasure()
247
+ },
248
+ text() {
249
+ this.scheduleMidButtonBorderMeasure()
250
+ },
251
+ midButtonOffsetY() {
252
+ this.scheduleMidButtonBorderMeasure()
253
+ }
254
+ },
216
255
  emits: ["click", "change"],
217
256
  methods: {
218
257
  addStyle,
@@ -235,6 +274,28 @@
235
274
  updateFromParent() {
236
275
  // 重新初始化
237
276
  this.init()
277
+ this.scheduleMidButtonBorderMeasure()
278
+ },
279
+ scheduleMidButtonBorderMeasure() {
280
+ if (!this.isMidButton) return
281
+ this.$nextTick(() => this.updateMidButtonBorderClip())
282
+ },
283
+ async updateMidButtonBorderClip() {
284
+ if (!this.isMidButton || !this.parent) return
285
+ await sleep(20)
286
+ const [contentRect, circleRect] = await Promise.all([
287
+ this.parent.$uGetRect('.u-tabbar__content'),
288
+ this.$uGetRect('.u-tabbar-item__icon--mid-button')
289
+ ])
290
+ if ([contentRect, circleRect].some(rect => !rect || !Number.isFinite(rect.top) || rect.height <= 0)) return
291
+ const clipHeight = calculateMidButtonBorderClipHeight({
292
+ contentTop: contentRect.top,
293
+ circleTop: circleRect.top,
294
+ borderTopOffset: this.parentData.border ? this.parentData.midButtonBorderTopOffset : 0
295
+ })
296
+ if (clipHeight !== this.midButtonBorderClipHeightValue) {
297
+ this.midButtonBorderClipHeightValue = clipHeight
298
+ }
238
299
  },
239
300
  clickHandler() {
240
301
  this.$nextTick(() => {
@@ -281,6 +342,19 @@
281
342
  width: 100%;
282
343
  }
283
344
 
345
+ &-content {
346
+ @include flex;
347
+ align-items: center;
348
+ justify-content: center;
349
+
350
+ &--mid-button {
351
+ position: relative;
352
+ z-index: 2;
353
+ width: 100%;
354
+ height: 100%;
355
+ }
356
+ }
357
+
284
358
  &--anim-scale {
285
359
  transform: scale(var(--up-tabbar-icon-scale, 1.1));
286
360
  }
@@ -402,7 +476,7 @@
402
476
  transform: none;
403
477
  }
404
478
 
405
- .u-tabbar-item--lift.u-tabbar-item--active .u-tabbar-item__icon {
479
+ .u-tabbar-item--lift.u-tabbar-item--active .u-tabbar-item__icon:not(.u-tabbar-item__icon--mid-button) {
406
480
  transform: translateY(-6rpx) scale(1.04);
407
481
  }
408
482
 
@@ -439,7 +513,7 @@
439
513
  left: 0;
440
514
  top: 0;
441
515
  width: 64px;
442
- height: v-bind('midButtonBorderClipHeight');
516
+ height: 0;
443
517
  box-sizing: border-box;
444
518
  background: transparent;
445
519
  overflow: hidden;
@@ -447,15 +521,17 @@
447
521
  z-index: 0;
448
522
  }
449
523
 
450
- /* #ifdef APP-NVUE */
451
- .u-tabbar-item__mid-button-border {
452
- height: 15.5px;
524
+ .u-tabbar-item__icon-content--anim-scale {
525
+ transform: scale(var(--up-tabbar-icon-scale, 1.1));
453
526
  }
454
527
 
455
- .u-tabbar-item--mid-button-no-text .u-tabbar-item__mid-button-border {
456
- height: 7px;
528
+ .u-tabbar-item__icon-content--anim-lift {
529
+ transform: translateY(-6rpx) scale(var(--up-tabbar-icon-scale, 1.08));
530
+ }
531
+
532
+ .u-tabbar-item__icon-content--anim-swing {
533
+ transform: rotate(-10deg) scale(var(--up-tabbar-icon-scale, 1.08));
457
534
  }
458
- /* #endif */
459
535
 
460
536
  .u-tabbar-item__mid-button-border-circle {
461
537
  width: 64px;
@@ -497,7 +573,8 @@
497
573
  transform: translateY(-4rpx);
498
574
  }
499
575
 
500
- .u-tabbar-item--anim-pulse .u-tabbar-item__icon {
576
+ .u-tabbar-item--anim-pulse .u-tabbar-item__icon:not(.u-tabbar-item__icon--mid-button),
577
+ .u-tabbar-item__icon-content--anim-pulse {
501
578
  animation: u-tabbar-item-pulse 1.4s ease-in-out infinite;
502
579
  }
503
580
 
@@ -1,3 +1,16 @@
1
+ /* #ifdef MP-WEIXIN */
2
+ :host {
3
+ --up-tag-close-bg-color: #c8c9cc;
4
+ }
5
+
6
+ @media (prefers-color-scheme: dark) {
7
+ :host {
8
+ --up-tag-close-bg-color: #4b5563;
9
+ }
10
+ }
11
+ /* #endif */
12
+
13
+ /* #ifndef MP-WEIXIN */
1
14
  :root,
2
15
  page,
3
16
  body {
@@ -19,3 +32,4 @@ body {
19
32
  [data-up-theme='dark'] {
20
33
  --up-tag-close-bg-color: #4b5563;
21
34
  }
35
+ /* #endif */
@@ -6,7 +6,6 @@
6
6
  @tap="clickHandler"
7
7
  :class="classes"
8
8
  :style="[mergeStyle]"
9
- @touchmove="noop"
10
9
  >
11
10
  <slot />
12
11
  </view>
@@ -92,14 +92,14 @@
92
92
  return {
93
93
  columnList: [[]], // 存储每列的数据
94
94
  children: [],
95
- // 用于标记是否已经初始化
96
- initialized: false,
97
- windowWidth: 375,
98
- windowHeight: 0,
99
- distributionQueue: [],
100
- distributionRunning: false,
101
- distributionPromise: null,
102
- distributionGeneration: 0
95
+ // 用于标记是否已经初始化
96
+ initialized: false,
97
+ windowWidth: 375,
98
+ windowHeight: 0,
99
+ distributionQueue: [],
100
+ distributionRunning: false,
101
+ distributionPromise: null,
102
+ distributionGeneration: 0
103
103
  }
104
104
  },
105
105
  watch: {
@@ -111,27 +111,27 @@
111
111
  if (this.columnList.length == 1) {
112
112
  this.initColumnList()
113
113
  }
114
- if (!this.isPureAppend(nVal, oVal)) {
115
- this.redistributeData(nVal);
116
- return;
117
- }
118
- // 取差值,即这一次数组变化新增的部分
119
- const startIndex = Array.isArray(oVal) && oVal.length > 0 ? oVal.length : 0;
120
- // 纯追加时只将新增数据加入共享分配队列
121
- this.handleData(nVal.slice(startIndex));
114
+ if (!this.isPureAppend(nVal, oVal)) {
115
+ this.redistributeData(nVal);
116
+ return;
117
+ }
118
+ // 取差值,即这一次数组变化新增的部分
119
+ const startIndex = Array.isArray(oVal) && oVal.length > 0 ? oVal.length : 0;
120
+ // 纯追加时只将新增数据加入共享分配队列
121
+ this.handleData(nVal.slice(startIndex));
122
122
  }
123
123
  },
124
124
  immediate: true
125
125
  },
126
- columns: {
127
- handler() {
128
- // 重新分配数据
129
- if (this.copyFlowList.length > 0) {
130
- this.redistributeData(this.copyFlowList);
131
- } else {
132
- this.clear(false);
133
- }
134
- },
126
+ columns: {
127
+ handler() {
128
+ // 重新分配数据
129
+ if (this.copyFlowList.length > 0) {
130
+ this.redistributeData(this.copyFlowList);
131
+ } else {
132
+ this.clear(false);
133
+ }
134
+ },
135
135
  immediate: false
136
136
  }
137
137
  },
@@ -219,144 +219,150 @@
219
219
  const newColumnsCount = this.getColumnsCount();
220
220
  const oldColumnsCount = this.columnList.length;
221
221
 
222
- // 只有列数发生变化时才重新分配数据
223
- if (newColumnsCount !== oldColumnsCount) {
224
- this.redistributeData(this.copyFlowList);
225
- }
226
- }, 300);
227
- },
228
-
229
- // 重新分配所有数据
230
- redistributeData(data = this.copyFlowList) {
231
- this.clear(false);
232
- // 保存所有数据
233
- const allData = this.cloneData(data || []);
234
- // 重新分配数据
235
- return this.handleData(allData);
236
- },
237
-
238
- // 判断新数据是否只在原数组末尾追加
239
- isPureAppend(newData, oldData) {
240
- if (!Array.isArray(oldData) || oldData.length === 0) return true;
241
- if (!Array.isArray(newData) || newData.length < oldData.length) return false;
242
- return oldData.every((item, index) => {
243
- return JSON.stringify(item) === JSON.stringify(newData[index]);
244
- });
245
- },
246
-
247
- // 将新增数据加入共享队列,确保只有一个分配循环运行
248
- handleData(newData) {
249
- if (!newData || newData.length === 0) {
250
- return this.distributionPromise || Promise.resolve();
251
- }
252
- this.distributionQueue.push({
253
- generation: this.distributionGeneration,
254
- data: this.cloneData(newData)
255
- });
256
- if (!this.distributionRunning) {
257
- this.distributionPromise = this.runDistributionQueue();
258
- }
259
- return this.distributionPromise;
260
- },
261
-
262
- // 串行消费所有待分配数据
263
- async runDistributionQueue() {
264
- if (this.distributionRunning) return;
265
- this.distributionRunning = true;
266
- try {
267
- while (this.distributionQueue.length > 0) {
268
- const task = this.distributionQueue.shift();
269
- if (task.generation !== this.distributionGeneration) continue;
270
- await this.distributeData(task.data, task.generation);
271
- }
272
- } finally {
273
- this.distributionRunning = false;
274
- this.distributionPromise = null;
275
- if (this.distributionQueue.length > 0) {
276
- this.distributionPromise = this.runDistributionQueue();
277
- }
278
- }
279
- },
280
-
281
- // 逐项测量并分配数据,代数变化时立即退出
282
- async distributeData(newData, generation) {
283
- let columnHeights = new Array(this.columnList.length).fill(0);
284
- for (const item of newData) {
285
- if (generation !== this.distributionGeneration) return;
286
- columnHeights = await this.getColumnHeights();
287
- if (generation !== this.distributionGeneration) return;
288
-
289
- const minHeightIndex = this.getMinHeightColumnIndex(columnHeights);
290
- this.columnList[minHeightIndex].push(item);
291
-
292
- await sleep(this.addTime);
293
- if (generation !== this.distributionGeneration) return;
294
- await this.$nextTick();
295
- if (generation !== this.distributionGeneration) return;
296
- try {
297
- const rect = await this.$uGetRect(`#u-column-${minHeightIndex}`);
298
- if (generation !== this.distributionGeneration) return;
299
- if (rect.height) {
300
- columnHeights[minHeightIndex] = rect.height;
301
- this.$emit('after-add-one', {
302
- ...item,
303
- height: rect.height
304
- });
305
- }
306
- } catch (e) {
307
- // 获取不到列高时,下一个元素会重新测量所有列
308
- }
309
- }
310
- if (generation !== this.distributionGeneration) return;
311
- this.$emit('after-add-all', {
312
- columnHeights: columnHeights,
313
- newData: newData
314
- });
315
- },
316
-
317
- // 获取当前所有列的真实高度
318
- async getColumnHeights() {
319
- const columnHeights = new Array(this.columnList.length).fill(0);
320
- for (let i = 0; i < this.columnList.length; i++) {
321
- try {
322
- const rect = await this.$uGetRect(`#u-column-${i}`);
323
- columnHeights[i] = rect.height || 0;
324
- } catch (e) {
325
- columnHeights[i] = 0;
326
- }
327
- }
328
- return columnHeights;
329
- },
330
-
331
- // 获取最短列;高度相同或暂不可测时按列数据量打散,避免全部落到第一列
332
- getMinHeightColumnIndex(columnHeights) {
333
- let minIndex = 0;
334
- for (let i = 1; i < columnHeights.length; i++) {
335
- const currentHeight = Number(columnHeights[i]) || 0;
336
- const minHeight = Number(columnHeights[minIndex]) || 0;
337
- if (currentHeight < minHeight) {
338
- minIndex = i;
339
- } else if (currentHeight === minHeight) {
340
- const currentLength = this.columnList[i] ? this.columnList[i].length : 0;
341
- const minLength = this.columnList[minIndex] ? this.columnList[minIndex].length : 0;
342
- if (currentLength < minLength) {
343
- minIndex = i;
344
- }
345
- }
346
- }
347
- return minIndex;
348
- },
349
-
350
- // 复制而不是引用对象和数组
351
- cloneData(data) {
352
- return JSON.parse(JSON.stringify(data));
222
+ // 只有列数发生变化时才重新分配数据
223
+ if (newColumnsCount !== oldColumnsCount) {
224
+ this.redistributeData(this.copyFlowList);
225
+ }
226
+ }, 300);
227
+ },
228
+
229
+ // 重新分配所有数据
230
+ redistributeData(data = this.copyFlowList) {
231
+ // 强制重置锁状态,确保能重新开始分配
232
+ this.distributionRunning = false;
233
+ this.distributionPromise = null;
234
+ this.clear(false);
235
+ // 保存所有数据
236
+ const allData = this.cloneData(data || []);
237
+ // 重新分配数据
238
+ return this.handleData(allData);
239
+ },
240
+
241
+ // 判断新数据是否只在原数组末尾追加
242
+ isPureAppend(newData, oldData) {
243
+ if (!Array.isArray(oldData) || oldData.length === 0) return true;
244
+ if (!Array.isArray(newData) || newData.length < oldData.length) return false;
245
+ return oldData.every((item, index) => {
246
+ return JSON.stringify(item) === JSON.stringify(newData[index]);
247
+ });
248
+ },
249
+
250
+ // 将新增数据加入共享队列,确保只有一个分配循环运行
251
+ handleData(newData) {
252
+ if (!newData || newData.length === 0) {
253
+ return this.distributionPromise || Promise.resolve();
254
+ }
255
+ this.distributionQueue.push({
256
+ generation: this.distributionGeneration,
257
+ data: this.cloneData(newData)
258
+ });
259
+ if (!this.distributionRunning) {
260
+ this.distributionPromise = this.runDistributionQueue();
261
+ }
262
+ return this.distributionPromise;
263
+ },
264
+
265
+ // 串行消费所有待分配数据
266
+ async runDistributionQueue() {
267
+ if (this.distributionRunning) return;
268
+ this.distributionRunning = true;
269
+ try {
270
+ while (this.distributionQueue.length > 0) {
271
+ const task = this.distributionQueue.shift();
272
+ if (task.generation !== this.distributionGeneration) continue;
273
+ await this.distributeData(task.data, task.generation);
274
+ }
275
+ } finally {
276
+ this.distributionRunning = false;
277
+ this.distributionPromise = null;
278
+ if (this.distributionQueue.length > 0) {
279
+ this.distributionPromise = this.runDistributionQueue();
280
+ }
281
+ }
282
+ },
283
+
284
+ // 逐项测量并分配数据,代数变化时立即退出
285
+ async distributeData(newData, generation) {
286
+ let columnHeights = new Array(this.columnList.length).fill(0);
287
+ for (const item of newData) {
288
+ if (generation !== this.distributionGeneration) return;
289
+ columnHeights = await this.getColumnHeights();
290
+ if (generation !== this.distributionGeneration) return;
291
+
292
+ const minHeightIndex = this.getMinHeightColumnIndex(columnHeights);
293
+ this.columnList[minHeightIndex].push(item);
294
+
295
+ await sleep(this.addTime);
296
+ if (generation !== this.distributionGeneration) return;
297
+ await this.$nextTick();
298
+ if (generation !== this.distributionGeneration) return;
299
+ try {
300
+ const rect = await this.$uGetRect(`#u-column-${minHeightIndex}`);
301
+ if (generation !== this.distributionGeneration) return;
302
+ if (rect.height) {
303
+ columnHeights[minHeightIndex] = rect.height;
304
+ this.$emit('after-add-one', {
305
+ ...item,
306
+ height: rect.height
307
+ });
308
+ }
309
+ } catch (e) {
310
+ // 获取不到列高时,下一个元素会重新测量所有列
311
+ }
312
+ }
313
+ if (generation !== this.distributionGeneration) return;
314
+ this.$emit('after-add-all', {
315
+ columnHeights: columnHeights,
316
+ newData: newData
317
+ });
318
+ },
319
+
320
+ // 获取当前所有列的真实高度
321
+ async getColumnHeights() {
322
+ const columnHeights = new Array(this.columnList.length).fill(0);
323
+ for (let i = 0; i < this.columnList.length; i++) {
324
+ try {
325
+ const rect = await this.$uGetRect(`#u-column-${i}`);
326
+ columnHeights[i] = rect.height || 0;
327
+ } catch (e) {
328
+ columnHeights[i] = 0;
329
+ }
330
+ }
331
+ return columnHeights;
332
+ },
333
+
334
+ // 获取最短列;高度相同或暂不可测时按列数据量打散,避免全部落到第一列
335
+ getMinHeightColumnIndex(columnHeights) {
336
+ let minIndex = 0;
337
+ for (let i = 1; i < columnHeights.length; i++) {
338
+ const currentHeight = Number(columnHeights[i]) || 0;
339
+ const minHeight = Number(columnHeights[minIndex]) || 0;
340
+ if (currentHeight < minHeight) {
341
+ minIndex = i;
342
+ } else if (currentHeight === minHeight) {
343
+ const currentLength = this.columnList[i] ? this.columnList[i].length : 0;
344
+ const minLength = this.columnList[minIndex] ? this.columnList[minIndex].length : 0;
345
+ if (currentLength < minLength) {
346
+ minIndex = i;
347
+ }
348
+ }
349
+ }
350
+ return minIndex;
351
+ },
352
+
353
+ // 复制而不是引用对象和数组
354
+ cloneData(data) {
355
+ return JSON.parse(JSON.stringify(data));
353
356
  },
354
357
 
355
- // 清空数据列表
356
- clear(bak = true) {
357
- this.distributionGeneration += 1;
358
- this.distributionQueue.splice(0);
359
- this.initColumnList();
358
+ // 清空数据列表
359
+ clear(bak = true) {
360
+ this.distributionGeneration += 1;
361
+ this.distributionQueue.splice(0);
362
+ // 强制重置分配锁状态,避免页面隐藏导致的永久锁死
363
+ this.distributionRunning = false;
364
+ this.distributionPromise = null;
365
+ this.initColumnList();
360
366
  // 同时清除父组件列表中的数据
361
367
  if (bak) {
362
368
  // #ifdef VUE2
@@ -465,4 +471,4 @@
465
471
  max-width: 100%;
466
472
  /* #endif */
467
473
  }
468
- </style>
474
+ </style>
@@ -17,6 +17,20 @@ function emptyNodeInfo() {
17
17
  }
18
18
  }
19
19
 
20
+ // 组件卸载后仍把它交给原生查询,APP 端会拿已失效的 nodeId 去查视图层映射表,
21
+ // 读到 undefined 再取 .$ 就抛 Cannot read properties of undefined (reading '$')
22
+ function isCompUnmounted(comp) {
23
+ if (!comp) {
24
+ return false
25
+ }
26
+ // 公共 mixin 的 beforeUnmount 里同步置位
27
+ if (comp.__upUnmounted) {
28
+ return true
29
+ }
30
+ // 未应用公共 mixin 时退回 Vue 自身标记:公开实例挂在 $ 上,内部实例直接持有
31
+ return !!(comp.$ ? comp.$.isUnmounted : comp.isUnmounted)
32
+ }
33
+
20
34
  // #ifdef APP-NVUE
21
35
  const dom = (typeof uni !== 'undefined' && uni && typeof uni.requireNativePlugin === 'function')
22
36
  ? uni.requireNativePlugin('dom')
@@ -37,6 +51,10 @@ function getNvueRect(ref) {
37
51
 
38
52
  // 查询节点信息
39
53
  export function upGetRect(selector, all = false, comp = null) {
54
+ // 判定必须放在条件编译之外,否则 app-harmony 上整段会被裁掉
55
+ if (isCompUnmounted(comp)) {
56
+ return Promise.resolve(all ? [] : emptyNodeInfo())
57
+ }
40
58
  return new Promise((resolve) => {
41
59
  // #ifndef APP-NVUE
42
60
  const query = uni.createSelectorQuery()
@@ -70,6 +88,17 @@ export function upGetRect(selector, all = false, comp = null) {
70
88
  })
71
89
  }
72
90
 
91
+ // 卸载后替代真实观察器,保持链式调用不报错
92
+ function noopIntersectionObserver() {
93
+ const observer = {
94
+ relativeTo: () => observer,
95
+ relativeToViewport: () => observer,
96
+ observe: () => {},
97
+ disconnect: () => {}
98
+ }
99
+ return observer
100
+ }
101
+
73
102
  // 创建交叉观察器
74
103
  // 小程序端 uni.createIntersectionObserver(vm) 会把 Vue 实例代理直接透传给原生 API,
75
104
  // 原生内部的 Object.keys(vm) 会触发告警:
@@ -77,6 +106,10 @@ export function upGetRect(selector, all = false, comp = null) {
77
106
  // (uni-app 只给 createSelectorQuery 做了 $scope 解包,没给 createIntersectionObserver 做)
78
107
  // 组件实例上的同名方法内部使用 $scope(原生组件实例),不会把代理外泄,因此优先使用
79
108
  export function upCreateIntersectionObserver(comp, options) {
109
+ // 和 upGetRect 同理:APP 端 addIntersectionObserver 也走 window.__$__(id).$ 查表
110
+ if (isCompUnmounted(comp)) {
111
+ return noopIntersectionObserver()
112
+ }
80
113
  // 各家小程序与 H5 都在组件实例上暴露了该方法,内部已完成实例解包
81
114
  if (comp && typeof comp.createIntersectionObserver === 'function') {
82
115
  return options ? comp.createIntersectionObserver(options) : comp.createIntersectionObserver()
@@ -251,6 +251,9 @@ export const mixin = defineMixin({
251
251
  uni.$emit('uOnReachBottom')
252
252
  },
253
253
  beforeUnmount() {
254
+ // mixin 的钩子先于组件自身钩子同步执行,比 Vue 内部的 $.isUnmounted
255
+ // (在 post-render 队列里异步置位)更早,异步回调据此拦截节点查询
256
+ this.__upUnmounted = true
254
257
  // 判断当前页面是否存在parent和chldren,一般在checkbox和checkbox-group父子联动的场景会有此情况
255
258
  // 组件销毁时,移除子组件在父组件children数组中的实例,释放资源,避免数据混乱
256
259
  if (this.parent && test.array(this.parent.children)) {
package/libs/root/root.js CHANGED
@@ -7,7 +7,9 @@ function ensureRootToastHostComponent(ms, scriptContent) {
7
7
  if (componentsMatch && /\bUpRootToastHost\b/.test(componentsMatch[1])) return
8
8
 
9
9
  if (componentsMatch) {
10
- ms.appendLeft(componentsMatch.index + componentsMatch[0].length, ' UpRootToastHost,')
10
+ // ms.appendLeft(componentsMatch.index + componentsMatch[0].length, ' UpRootToastHost,')
11
+ const openBraceIndex = componentsMatch.index + componentsMatch[0].indexOf('{')
12
+ ms.appendLeft(openBraceIndex + 1, ' UpRootToastHost,')
11
13
  return
12
14
  }
13
15
 
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "id": "uview-plus",
3
3
  "name": "uview-plus",
4
4
  "displayName": "零云®uview-plus3.0重磅发布,全面的Vue3鸿蒙跨端移动组件库,组件丰富维护更新稳定。",
5
- "version": "3.8.112",
5
+ "version": "3.8.117",
6
6
  "description": "零云®uview-plus已兼容vue3支持多语言,120+全面的组件和便捷的工具会让您信手拈来。近期新增拖动排序、条码、图片裁剪、下拉刷新、虚拟列表、签名、Markdown等。",
7
7
  "keywords": [
8
8
  "uview",