uview-ultra 4.5.48 → 4.5.71

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.
Files changed (40) hide show
  1. package/changelog.md +1650 -1435
  2. package/components/up-button/up-button.uvue +9 -8
  3. package/components/up-button/up-button.vue +4 -4
  4. package/components/up-cell/props.js +6 -0
  5. package/components/up-cell/up-cell.uvue +21 -8
  6. package/components/up-cell/up-cell.vue +9 -2
  7. package/components/up-cell-group/up-cell-group.uvue +7 -0
  8. package/components/up-checkbox/up-checkbox.uvue +4 -3
  9. package/components/up-empty/up-empty.uvue +4 -0
  10. package/components/up-empty/up-empty.vue +3 -0
  11. package/components/up-goods-sku/up-goods-sku.uvue +32 -3
  12. package/components/up-goods-sku/up-goods-sku.vue +19 -3
  13. package/components/up-icon/icons.uts +1 -1
  14. package/components/up-navbar/navbar.uts +2 -0
  15. package/components/up-navbar/up-navbar.uvue +199 -28
  16. package/components/up-navbar/up-navbar.vue +14 -1
  17. package/components/up-popup/props.js +10 -0
  18. package/components/up-popup/up-popup.uvue +89 -1
  19. package/components/up-popup/up-popup.vue +69 -1
  20. package/components/up-radio/up-radio.uvue +6 -4
  21. package/components/up-radio-group/radioGroup.js +1 -1
  22. package/components/up-radio-group/radioGroup.uts +1 -1
  23. package/components/up-root/props.js +28 -0
  24. package/components/up-root/props.uts +28 -0
  25. package/components/up-root/root.js +14 -0
  26. package/components/up-root/root.uts +14 -0
  27. package/components/up-root/up-root.uvue +57 -0
  28. package/components/up-root/up-root.vue +35 -0
  29. package/components/up-root-toast-host/up-root-toast-host.uvue +27 -0
  30. package/components/up-switch/switch.uts +2 -1
  31. package/components/up-tag/up-tag.uvue +1 -1
  32. package/components/up-tag/up-tag.vue +1 -1
  33. package/components/up-transition/up-transition.uvue +10 -0
  34. package/components/up-transition/up-transition.vue +7 -0
  35. package/components/up-transition/vue.ani-style.scss +39 -10
  36. package/index.uts +47 -0
  37. package/libs/root/rootState.uts +74 -0
  38. package/libs/theme/theme.js +12 -2
  39. package/libs/theme/theme.uts +578 -0
  40. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <view class="up-navbar" :class="[customClass]">
2
+ <view class="up-navbar" :class="[customClass]" :style="rootStyle">
3
3
  <!-- ios 模式:in-flow 层,恒定渲染,随页面原生滚动被带走 -->
4
4
  <view v-if="isIosMode" class="up-navbar__flow">
5
5
  <view :style="flowSpacerStyle"></view>
@@ -14,26 +14,33 @@
14
14
  >{{ title }}</text>
15
15
  </view>
16
16
  </view>
17
- <!-- default 模式:原有占位块,行为保持不变 -->
17
+ <!--
18
+ default 模式:占位块。
19
+ 注意高度是「根节点 + 这里」两份同一个值(见 rootStyle 的注释):H5 / 小程序
20
+ 靠这一块撑开,App 端靠根节点那一份,两边都不会塌。
21
+ -->
18
22
  <view
19
23
  class="up-navbar__placeholder"
20
- v-if="!isIosMode && fixed && placeholder"
24
+ v-if="placeholderActive"
21
25
  :style="placeholderStyle"
22
26
  ></view>
23
27
  <view :class="[(isIosMode || fixed) ? 'up-navbar--fixed' : '']">
24
- <!-- ios 模式的磨砂层:随滚动淡入,位于内容之下 -->
28
+ <!-- 磨砂层:随滚动淡入,位于内容之下。ios 模式与 frosted 模式共用 -->
25
29
  <view
26
- v-if="isIosMode"
30
+ v-if="showGlass"
27
31
  class="up-navbar__glass"
32
+ :class="[frostedGlass ? 'up-navbar__glass--frosted' : '', frostedBorder ? 'up-navbar__glass--border' : '']"
28
33
  :style="glassStyle"
29
34
  ></view>
30
- <up-status-bar
35
+ <!-- 状态栏占位:高度可被 statusBarInsetTop 覆盖(iOS standalone 传 0) -->
36
+ <view
31
37
  v-if="safeAreaInsetTop"
32
- :bgColor="statusBarBgColor"
33
- ></up-status-bar>
38
+ class="up-navbar__statusbar"
39
+ :style="statusBarSpacerStyle"
40
+ ></view>
34
41
  <view
35
42
  class="up-navbar__content"
36
- :class="[(border && !isIosMode) ? 'up-border-bottom' : '']"
43
+ :class="[(border && !showGlass) ? 'up-border-bottom' : '']"
37
44
  :style="contentStyle"
38
45
  >
39
46
  <view
@@ -102,7 +109,7 @@
102
109
  * @property {Boolean} safeAreaInsetTop 是否开启顶部安全区适配 (默认 true )
103
110
  * @property {Boolean} placeholder 固定在顶部时,是否生成一个等高元素,以防止塌陷 (默认 false )
104
111
  * @property {Boolean} fixed 导航栏是否固定在顶部 (默认 false )
105
- * @property {Boolean} border 导航栏底部是否显示下边框 (默认 false )
112
+ * @property {Boolean} border 导航栏底部是否显示下边框(磨砂层的分隔线也服从它)(默认 false )
106
113
  * @property {String} leftIcon 左边返回图标的名称,只能为uView自带的图标 (默认 'arrow-left' )
107
114
  * @property {String} leftText 左边的提示文字
108
115
  * @property {String} rightText 右边的提示文字
@@ -111,6 +118,8 @@
111
118
  * @property {String} bgColor 导航栏背景设置 (默认 '#ffffff' )
112
119
  * @property {String | Number} titleWidth 导航栏标题的最大宽度,内容超出会以省略号隐藏 (默认 '400rpx' )
113
120
  * @property {String | Number} height 导航栏高度(不包括状态栏高度在内,内部自动加上)(默认 '44px' )
121
+ * @property {Boolean} frosted 滚动时自动磨砂(高仿 iOS 材质;ios 模式自带,这个是给 H5 / Android 用的)(默认 false )
122
+ * @property {String | Number} statusBarInsetTop 状态栏占位高度覆盖(px):-1 用系统值、0 不占位(iOS standalone 传 0)(默认 -1 )
114
123
  * @property {String | Number} leftIconSize 左侧返回图标的大小(默认 20px )
115
124
  * @property {String | Number} leftIconColor 左侧返回图标的颜色(默认 #303133 )
116
125
  * @property {Boolean} autoBack 点击左侧区域(返回图标),是否自动返回上一页(默认 false )
@@ -171,10 +180,11 @@
171
180
  type: [String, Number],
172
181
  default: defProps.getAny('navbar.title')
173
182
  },
174
- // 背景颜色
183
+ // 背景颜色。`|| '#ffffff'` 是兜底:`defProps` 在某些构建下取不到该键会返回空值,
184
+ // 内联背景一旦为空,`.up-navbar__content` 的调试遗留色就会露出来(曾表现为顶栏变蓝)。
175
185
  bgColor: {
176
186
  type: String,
177
- default: defProps.getString('navbar.bgColor')
187
+ default: defProps.getString('navbar.bgColor') || '#ffffff'
178
188
  },
179
189
  // 标题的宽度
180
190
  titleWidth: {
@@ -211,11 +221,24 @@
211
221
  type: String,
212
222
  default: defProps.getString('navbar.mode')
213
223
  },
214
- // 页面滚动距离,仅 ios 模式使用,由页面 onPageScroll 传入
224
+ // 页面滚动距离,由页面 onPageScroll 传入;ios 模式与 frosted 模式都用它
215
225
  scrollTop: {
216
226
  type: [String, Number],
217
227
  default: defProps.getAny('navbar.scrollTop')
218
228
  },
229
+ // 滚动时自动磨砂:滚过阈值后淡入一层半透明模糊(高仿 iOS 材质)。
230
+ // ios 模式本来就有磨砂层,这个开关是给 H5 / Android 这些没有系统玻璃带的端用的。
231
+ frosted: {
232
+ type: Boolean,
233
+ default: defProps.getBoolean('navbar.frosted')
234
+ },
235
+ // 状态栏占位高度覆盖(px)。-1 = 用系统值;0 = 不占位。
236
+ // iOS standalone(PWA,meta=black)下 webview 本就从状态栏下方开始,必须传 0,
237
+ // 否则导航行被推出系统的 Liquid Glass 滚动边缘带。
238
+ statusBarInsetTop: {
239
+ type: [String, Number],
240
+ default: defProps.getAny('navbar.statusBarInsetTop')
241
+ },
219
242
  // 微信小程序右侧区域是否避让原生胶囊
220
243
  avoidCapsule: {
221
244
  type: Boolean,
@@ -228,8 +251,53 @@
228
251
  })
229
252
 
230
253
  const emit = defineEmits(['leftClick', 'rightClick'])
231
- const statusBarHeight = ref(uni.getSystemInfoSync().statusBarHeight ?? 0)
232
- const placeholderHeight = ref(addUnit(parseInt(getPx(props.height)) + statusBarHeight.value, 'px'))
254
+ const systemStatusBarHeight = ref(uni.getSystemInfoSync().statusBarHeight ?? 0)
255
+
256
+ // #ifdef H5 || WEB
257
+ // web 端 `statusBarHeight` 恒 0(浏览器视口不含系统状态栏),但 iOS Safari
258
+ // (viewport-fit=cover,地址栏收起)的页面视口顶部包含状态栏区域——真实占位
259
+ // 高度是 CSS `env(safe-area-inset-top)` 的像素值。探针元素读一次实际值:
260
+ // iPhone Safari ≈ 59,桌面 / Android 浏览器 env 为 0、行为不变。App 端原生
261
+ // statusBarHeight 是准的,不进这段。
262
+ try {
263
+ const doc: any = (globalThis as any).document
264
+ const probe = doc.createElement('div')
265
+ probe.setAttribute(
266
+ 'style',
267
+ 'position:absolute;visibility:hidden;pointer-events:none;height:env(safe-area-inset-top);padding:0;border:0;'
268
+ )
269
+ doc.body.appendChild(probe)
270
+ const measured = probe.getBoundingClientRect().height
271
+ doc.body.removeChild(probe)
272
+ if (typeof measured === 'number' && measured > 0) {
273
+ systemStatusBarHeight.value = measured
274
+ }
275
+ } catch (e) {
276
+ // 探测失败保持 0:与既有行为一致
277
+ }
278
+ // #endif
279
+
280
+ /**
281
+ * 实际使用的状态栏占位高度。
282
+ *
283
+ * `statusBarInsetTop >= 0` 时以它为准,否则用系统值。iOS standalone(PWA,meta=black)
284
+ * 下 webview 本就从系统状态栏下方开始,占位必须传 0 —— 再填一段会把导航行推出
285
+ * iOS 26/27 在页面顶部画的 Liquid Glass 滚动边缘带(约 75pt)。
286
+ */
287
+ const effectiveStatusBarHeight = computed((): number => {
288
+ // 非数字 / undefined 会得到 NaN,`NaN >= 0` 为 false,自然回落到系统值。
289
+ const override = Number(props.statusBarInsetTop)
290
+ if (override >= 0) return override
291
+ return systemStatusBarHeight.value
292
+ })
293
+
294
+ const placeholderHeight = computed((): string => {
295
+ // `height` 取不到时 `parseInt` 会得到 NaN,占位高度就是无效值 → 塌成 0 →
296
+ // fixed 层不占流,内容直接顶到导航栏上。兜 uview 的默认 44。
297
+ const raw = parseInt(getPx(props.height))
298
+ const height = raw > 0 ? raw : 44
299
+ return addUnit(height + effectiveStatusBarHeight.value, 'px')
300
+ })
233
301
 
234
302
  // iOS 大标题行高,同时是压缩进度的分母
235
303
  const LARGE_TITLE_HEIGHT = 52
@@ -254,6 +322,31 @@
254
322
  return props.mode == 'ios'
255
323
  })
256
324
 
325
+ /** 是否处于「fixed + 等高占位」档。 */
326
+ const placeholderActive = computed((): boolean => {
327
+ return !isIosMode.value && props.fixed && props.placeholder
328
+ })
329
+
330
+ /**
331
+ * 根节点样式:占位高度**同时**落在根节点上。
332
+ *
333
+ * 只把高度给内部的 `.up-navbar__placeholder` 是不够的 —— `<up-navbar>` 的根节点
334
+ * 才是父容器的 flex item(会话列表页是 `display: flex; flex-direction: column` 的
335
+ * `.conversations-shell`)。App 端(uvue 原生渲染)下,自定义组件的根节点不会被子
336
+ * 节点的显式高度撑开:占位块在,但它贡献的高度传不到父级;而 fixed 层又不占流,
337
+ * 于是列表第一行直接压在导航行下面 —— 这正是「开了 placeholder 也没用」的实况。
338
+ *
339
+ * 高度值只有一份(`placeholderHeight`),根节点与内部占位块共用,不会叠成两份。
340
+ */
341
+ const rootStyle = computed((): UTSJSONObject => {
342
+ const style = {} as UTSJSONObject
343
+ if (!placeholderActive.value) {
344
+ return style
345
+ }
346
+ style['height'] = placeholderHeight.value
347
+ return style
348
+ })
349
+
257
350
  // 有效大标题行高:title 为空时不渲染大标题行,行高塌陷为 0
258
351
  const largeTitleHeight = computed((): number => {
259
352
  return props.title != '' ? LARGE_TITLE_HEIGHT : 0
@@ -272,12 +365,48 @@
272
365
  return Math.min(Math.max(offset / distance, 0), 1)
273
366
  })
274
367
 
275
- // 磨砂在前半段走完,为居中标题的出现铺好不透明底
368
+ // 非 iOS 模式的磨砂参数:滚过 2px 开始淡入,28px 走满。
369
+ // 「半透明」由 `.up-navbar__glass--frosted` 的底色(主题里的 75%/78% rgba)保证,
370
+ // 这里只负责淡入,不再额外压 opacity —— 否则两层衰减叠起来会淡得看不见。
371
+ //
372
+ // 行程刻意短:iOS 的导航栏在内容压到栏下时是**立刻**出背景(scrollEdge → standard),
373
+ // 过渡只是让离散的滚动事件看起来连续。40px 那种长行程在「列表只滚了一点点」的场景
374
+ // 下永远走不满,看上去就像磨砂没开。
375
+ const FROSTED_START = 2
376
+ const FROSTED_FADE = 28
377
+
378
+ /** 磨砂层要不要渲染。 */
379
+ const showGlass = computed((): boolean => {
380
+ return isIosMode.value || props.frosted
381
+ })
382
+
383
+ /** 是否「非 iOS 模式的滚动磨砂」(决定要不要额外画底部细线)。 */
384
+ const frostedGlass = computed((): boolean => {
385
+ return props.frosted && !isIosMode.value
386
+ })
387
+
388
+ /**
389
+ * 磨砂层要不要画底部分隔线。
390
+ *
391
+ * 与 `border` 同一语义 —— `border` 的描述就是「导航栏底部是否显示下边框」,
392
+ * 磨砂层那条线就是下边框,必须服从它。早先这条线写死在磨砂层样式里,
393
+ * 调用方明明传了 `:border="false"`,滚动时照样会冒出一条,等于这个开关半失效。
394
+ */
395
+ const frostedBorder = computed((): boolean => {
396
+ return frostedGlass.value && props.border
397
+ })
398
+
399
+ // iOS 模式:磨砂在前半段走完,为居中标题的出现铺好不透明底。
400
+ // frosted 模式:按滚动距离从 0 淡入到 1(「半透明」由底色负责,不是靠 opacity)。
276
401
  const glassOpacity = computed((): number => {
277
- if (!isIosMode.value) {
402
+ if (isIosMode.value) {
403
+ return Math.min(Math.max(iosProgress.value / 0.5, 0), 1)
404
+ }
405
+ if (!props.frosted) {
278
406
  return 0
279
407
  }
280
- return Math.min(Math.max(iosProgress.value / 0.5, 0), 1)
408
+ const offset = parseInt(getPx(props.scrollTop))
409
+ return Math.min(Math.max((offset - FROSTED_START) / FROSTED_FADE, 0), 1)
281
410
  })
282
411
 
283
412
  // 居中标题在后段才启动,此时磨砂层已满不透明,不会与大标题互相透出
@@ -288,16 +417,25 @@
288
417
  return Math.min(Math.max((iosProgress.value - 0.75) / 0.25, 0), 1)
289
418
  })
290
419
 
291
- // ios 模式下状态栏占位透明,背景由磨砂层统一承担
420
+ // 磨砂层显示时状态栏占位透明,背景由磨砂层统一承担
292
421
  const statusBarBgColor = computed((): string => {
293
- return isIosMode.value ? 'transparent' : props.bgColor
422
+ return showGlass.value ? 'transparent' : props.bgColor
423
+ })
424
+
425
+ /** 状态栏占位块的样式(高度可被 `statusBarInsetTop` 覆盖)。 */
426
+ const statusBarSpacerStyle = computed((): UTSJSONObject => {
427
+ const style = {} as UTSJSONObject
428
+ style['height'] = addUnit(effectiveStatusBarHeight.value, 'px')
429
+ style['backgroundColor'] = statusBarBgColor.value
430
+ return style
294
431
  })
295
432
 
296
433
  const flowSpacerStyle = computed((): UTSJSONObject => {
297
434
  const style = {} as UTSJSONObject
298
- let top = parseInt(getPx(props.height))
435
+ const rawTop = parseInt(getPx(props.height))
436
+ let top = rawTop > 0 ? rawTop : 44
299
437
  if (props.safeAreaInsetTop) {
300
- top += statusBarHeight.value
438
+ top += effectiveStatusBarHeight.value
301
439
  }
302
440
  style['height'] = addUnit(top, 'px')
303
441
  return style
@@ -314,12 +452,18 @@
314
452
  return UTSJSONObject.assign(style, addStyle(props.titleStyle) as UTSJSONObject)
315
453
  })
316
454
 
317
- // 磨砂层。App 原生端不支持 backdrop-filter,0.82 不透明底色即为可读性下限;
455
+ // 磨砂层。App 原生端不支持 backdrop-filter 时,底色本身就是可读性下限;
318
456
  // backdrop-filter 仍然写入,在 WebView 类端上会自动生效。
457
+ //
458
+ // frosted 模式**不写** backgroundColor:调用方为了透明顶栏会把 bgColor 传成
459
+ // `transparent`,那样磨砂层就没有底色了。底色交给 `.up-navbar__glass--frosted`
460
+ // 的 CSS(卡片色 72%),与主题变量联动。
319
461
  const glassStyle = computed((): UTSJSONObject => {
320
462
  const style = {} as UTSJSONObject
321
463
  style['opacity'] = glassOpacity.value
322
- style['backgroundColor'] = props.bgColor
464
+ if (!frostedGlass.value) {
465
+ style['backgroundColor'] = props.bgColor
466
+ }
323
467
  return style
324
468
  })
325
469
 
@@ -344,8 +488,8 @@
344
488
  const contentStyle = computed((): UTSJSONObject => {
345
489
  const style = {} as UTSJSONObject
346
490
  style['height'] = addUnit(props.height)
347
- // ios 模式下内容区透明,背景交给磨砂层,避免遮住其淡入效果
348
- style['backgroundColor'] = isIosMode.value ? 'transparent' : props.bgColor
491
+ // 磨砂层显示时内容区透明,背景交给磨砂层,避免遮住其淡入效果
492
+ style['backgroundColor'] = showGlass.value ? 'transparent' : props.bgColor
349
493
  return style
350
494
  })
351
495
 
@@ -376,7 +520,9 @@
376
520
  }
377
521
 
378
522
  defineExpose({
379
- statusBarHeight,
523
+ // 名字跟着内部的 `systemStatusBarHeight` 走。早先这里还叫 `statusBarHeight`,
524
+ // 重命名后忘了同步 —— uts 里引用不存在的标识符,整段 setup 直接编译不过。
525
+ systemStatusBarHeight,
380
526
  placeholderHeight,
381
527
  isIosMode,
382
528
  iosProgress,
@@ -389,6 +535,9 @@
389
535
  @import "../../libs/css/components.scss";
390
536
 
391
537
  .up-navbar {
538
+ // 占位模式下根节点自己带高度;作为 flex 列容器的子项时不能被压缩,
539
+ // 否则内容一多,占位段先被挤掉,列表又顶回导航行下面。
540
+ flex-shrink: 0;
392
541
 
393
542
  &--fixed {
394
543
  position: fixed;
@@ -426,11 +575,33 @@
426
575
  backdrop-filter: saturate(180%) blur(var(--up-navbar-glass-blur, 20px));
427
576
  }
428
577
 
578
+ // 滚动磨砂(非 iOS 模式):底色是半透明的卡片色,配合 backdrop-filter 才是
579
+ // 「高仿 iOS」的材质 —— 不透明底会把背后的模糊完全盖掉。
580
+ //
581
+ // 底色走**主题变量**(`theme.uts` 里算好的 rgba),不写 `color-mix`:
582
+ // uvue App 端不支持 `color-mix`,整条声明会被丢掉;而 App 端也不支持
583
+ // `backdrop-filter` —— 两层同时失效,滚动时顶栏就跟全透明一样,磨砂等于没开。
584
+ &__glass--frosted {
585
+ background-color: var(--up-navbar-glass-bg, rgba(255, 255, 255, 0.75));
586
+ }
587
+
588
+ // 底部分隔线单独一个类,**只在 `border` 为真时**才挂上来。
589
+ // 早先它写死在 `--frosted` 里,页面传 `:border="false"` 也照样出线。
590
+ &__glass--border {
591
+ border-bottom: 1px solid var(--up-navbar-glass-border, rgba(0, 0, 0, 0.08));
592
+ }
593
+
594
+ &__statusbar {
595
+ width: 100%;
596
+ }
597
+
429
598
  &__content {
430
599
  @include flex(row);
431
600
  align-items: center;
432
601
  height: 44px;
433
- background-color: #9acafc;
602
+ // 底色一律由 `contentStyle` 内联给出;这里必须是透明 —— 早先这里是
603
+ // `#9acafc`(primaryDisabled 的调试遗留色),内联背景一旦失效顶栏就整条变蓝。
604
+ background-color: transparent;
434
605
  // 磨砂层是绝对定位的,会盖住同级非定位元素,故内容区显式提升层级
435
606
  position: relative;
436
607
  z-index: 1;
@@ -171,9 +171,22 @@ const isIosMode = computed(() => props.mode === 'ios')
171
171
  // 有效大标题行高:title 为空时不渲染大标题行,行高塌陷为 0
172
172
  const largeTitleHeight = computed(() => (props.title ? LARGE_TITLE_HEIGHT : 0))
173
173
 
174
+ // web 端 `statusBarHeight` 恒 0,但 iOS Safari(viewport-fit=cover,地址栏收起)的
175
+ // 页面视口顶部包含状态栏区域——真实高度是 env(safe-area-inset-top)。探针读一次。
176
+ let envSafeAreaTop = 0
177
+ // #ifdef H5
178
+ try {
179
+ const probe = document.createElement('div')
180
+ probe.setAttribute('style', 'position:absolute;visibility:hidden;height:env(safe-area-inset-top);padding:0;border:0;')
181
+ document.body.appendChild(probe)
182
+ envSafeAreaTop = probe.getBoundingClientRect().height
183
+ document.body.removeChild(probe)
184
+ } catch (e) {}
185
+ // #endif
186
+
174
187
  // in-flow 层顶部让位块高度,为固定层腾出空间
175
188
  const flowSpacerHeight = computed(() => {
176
- const statusBarHeight = props.safeAreaInsetTop ? sys().statusBarHeight : 0
189
+ const statusBarHeight = props.safeAreaInsetTop ? (sys().statusBarHeight || envSafeAreaTop) : 0
177
190
  return addUnit(getPx(props.height) + statusBarHeight, 'px')
178
191
  })
179
192
 
@@ -81,6 +81,16 @@ export const props = defineMixin({
81
81
  pageInline: {
82
82
  type: Boolean,
83
83
  default: () => defProps.popup.pageInline
84
+ },
85
+ // bottom 弹层是否支持拖动下滑关闭
86
+ dragClose: {
87
+ type: Boolean,
88
+ default: false
89
+ },
90
+ // 拖动下滑关闭的位移阈值,单位px
91
+ dragCloseThreshold: {
92
+ type: [String, Number],
93
+ default: 80
84
94
  }
85
95
  }
86
96
  })
@@ -20,7 +20,11 @@
20
20
  >
21
21
  <view
22
22
  class="up-popup__content"
23
- :style="[contentStyle]"
23
+ :style="[contentStyle, dragStyle]"
24
+ @touchstart="onDragTouchStart"
25
+ @touchmove="onDragTouchMove"
26
+ @touchend="onDragTouchEnd"
27
+ @touchcancel="onDragTouchEnd"
24
28
  @tap.stop="stopContentTap"
25
29
  >
26
30
  <up-status-bar v-if="safeAreaInsetTop"></up-status-bar>
@@ -69,6 +73,8 @@
69
73
  * @property {String} closeIconPos 自定义关闭图标位置(默认 'top-right' )
70
74
  * @property {String | Number} round 圆角值(默认 0)
71
75
  * @property {Boolean} zoom 当mode=center时 是否开启缩放(默认 true )
76
+ * @property {Boolean} dragClose bottom 弹层是否支持拖动下滑关闭(默认 false;配合弹层顶部的拖拽手柄使用)
77
+ * @property {String | Number} dragCloseThreshold 拖动下滑关闭的位移阈值,单位px(默认 80)
72
78
  * @property {Object} customStyle 组件的样式,对象形式
73
79
  * @event {Function} open 弹出层打开(进场动画结束后)
74
80
  * @event {Function} close 弹出层收起(关闭动作发生时,离场动画开始前;外部直接将show置为false时也会触发)
@@ -161,6 +167,16 @@
161
167
  type: Boolean,
162
168
  default: defProps.getBoolean('popup.pageInline')
163
169
  },
170
+ // bottom 弹层是否支持拖动下滑关闭
171
+ dragClose: {
172
+ type: Boolean,
173
+ default: false
174
+ },
175
+ // 拖动下滑关闭的位移阈值,单位px
176
+ dragCloseThreshold: {
177
+ type: [String, Number],
178
+ default: 80
179
+ },
164
180
  customStyle: {
165
181
  type: [Object, String],
166
182
  default: () => ({})
@@ -289,6 +305,78 @@
289
305
  return ''
290
306
  })
291
307
 
308
+ /*
309
+ * 拖动下滑关闭(dragClose)。
310
+ *
311
+ * 仅 bottom 弹层生效:手指按下后向下拖,content 跟手位移;松手时超过阈值
312
+ * 走既有 close 流程(update:show=false + close 事件),否则带回弹动画归位。
313
+ * 向上拖不拦截,弹层内部滚动不受影响。
314
+ */
315
+ const dragOffset = ref(0)
316
+ const dragActive = ref(false)
317
+ let dragStartY = 0
318
+ let dragTracking = false
319
+ let dragReleaseTimer = -1
320
+
321
+ const dragStyle = computed((): UTSJSONObject => {
322
+ if (!dragActive.value || dragOffset.value <= 0) {
323
+ return {} as UTSJSONObject
324
+ }
325
+ const style = {
326
+ transform: `translateY(${dragOffset.value}px)`,
327
+ } as UTSJSONObject
328
+ // 跟手阶段不做过渡;松手回弹时加过渡(dragActive 在回弹结束后复位)
329
+ style["transition"] = dragTracking ? "none" : "transform 0.25s ease-out"
330
+ return style
331
+ })
332
+
333
+ const dragCloseEnabled = computed((): boolean => {
334
+ return props.dragClose && props.mode === "bottom" && !props.pageInline
335
+ })
336
+
337
+ const onDragTouchStart = (e: TouchEvent): void => {
338
+ if (!dragCloseEnabled.value) return
339
+ if (e.touches.length == 0) return
340
+ dragStartY = e.touches[0].clientY
341
+ dragTracking = true
342
+ }
343
+
344
+ const onDragTouchMove = (e: TouchEvent): void => {
345
+ if (!dragTracking) return
346
+ if (e.touches.length == 0) return
347
+ const delta = e.touches[0].clientY - dragStartY
348
+ if (delta <= 0) {
349
+ // 向上拖:不进入拖动态,交给内部滚动
350
+ if (!dragActive.value) return
351
+ dragOffset.value = 0
352
+ return
353
+ }
354
+ dragActive.value = true
355
+ dragOffset.value = delta
356
+ }
357
+
358
+ const onDragTouchEnd = (_e: UniEvent): void => {
359
+ if (!dragTracking) return
360
+ dragTracking = false
361
+ const offset = dragOffset.value
362
+ const thresholdValue = parseInt(props.dragCloseThreshold.toString())
363
+ const threshold = isNaN(thresholdValue) ? 80 : thresholdValue
364
+ if (offset >= threshold) {
365
+ dragActive.value = false
366
+ dragOffset.value = 0
367
+ close(_e)
368
+ return
369
+ }
370
+ // 回弹:dragStyle 保留到过渡结束后再卸下,否则回弹动画会被瞬时移除样式截断
371
+ if (dragReleaseTimer >= 0) {
372
+ clearTimeout(dragReleaseTimer)
373
+ }
374
+ dragReleaseTimer = setTimeout(() => {
375
+ dragActive.value = false
376
+ dragOffset.value = 0
377
+ }, 260)
378
+ }
379
+
292
380
  const stopContentTap = (e: UniEvent): void => {
293
381
  e.stopPropagation()
294
382
  }
@@ -20,7 +20,11 @@
20
20
  >
21
21
  <view
22
22
  class="up-popup__content"
23
- :style="[contentStyle]"
23
+ :style="[contentStyle, dragStyle]"
24
+ @touchstart="onDragTouchStart"
25
+ @touchmove="onDragTouchMove"
26
+ @touchend="onDragTouchEnd"
27
+ @touchcancel="onDragTouchEnd"
24
28
  @tap.stop="noop"
25
29
  >
26
30
  <up-status-bar v-if="safeAreaInsetTop"></up-status-bar>
@@ -92,6 +96,70 @@
92
96
  const emit = defineEmits(['open', 'close', 'closed', 'click', 'update:show'])
93
97
  const instance = getCurrentInstance()
94
98
  const { noop } = useUltraUI(props)
99
+
100
+ /*
101
+ * 拖动下滑关闭(dragClose)。
102
+ *
103
+ * 仅 bottom 弹层生效:手指按下后向下拖,content 跟手位移;松手时超过阈值
104
+ * 走既有 close 流程(update:show=false + close 事件),否则带回弹动画归位。
105
+ * 向上拖不拦截,弹层内部滚动不受影响。
106
+ */
107
+ const dragOffset = ref(0)
108
+ const dragActive = ref(false)
109
+ let dragStartY = 0
110
+ let dragTracking = false
111
+ let dragReleaseTimer = -1
112
+
113
+ const dragStyle = computed(() => {
114
+ if (!dragActive.value || dragOffset.value <= 0) return {}
115
+ return {
116
+ transform: `translateY(${dragOffset.value}px)`,
117
+ transition: dragTracking ? 'none' : 'transform 0.25s ease-out',
118
+ }
119
+ })
120
+
121
+ const dragCloseEnabled = computed(() => props.dragClose && props.mode === 'bottom' && !props.pageInline)
122
+
123
+ const onDragTouchStart = (e) => {
124
+ if (!dragCloseEnabled.value) return
125
+ if (!e.touches || e.touches.length === 0) return
126
+ dragStartY = e.touches[0].clientY
127
+ dragTracking = true
128
+ }
129
+
130
+ const onDragTouchMove = (e) => {
131
+ if (!dragTracking) return
132
+ if (!e.touches || e.touches.length === 0) return
133
+ const delta = e.touches[0].clientY - dragStartY
134
+ if (delta <= 0) {
135
+ // 向上拖:不进入拖动态,交给内部滚动
136
+ if (!dragActive.value) return
137
+ dragOffset.value = 0
138
+ return
139
+ }
140
+ dragActive.value = true
141
+ dragOffset.value = delta
142
+ }
143
+
144
+ const onDragTouchEnd = (_e) => {
145
+ if (!dragTracking) return
146
+ dragTracking = false
147
+ const offset = dragOffset.value
148
+ const thresholdValue = parseInt(props.dragCloseThreshold)
149
+ const threshold = isNaN(thresholdValue) ? 80 : thresholdValue
150
+ if (offset >= threshold) {
151
+ dragActive.value = false
152
+ dragOffset.value = 0
153
+ close(_e)
154
+ return
155
+ }
156
+ // 回弹:dragStyle 保留到过渡结束后再卸下,否则回弹动画会被瞬时移除样式截断
157
+ if (dragReleaseTimer >= 0) clearTimeout(dragReleaseTimer)
158
+ dragReleaseTimer = setTimeout(() => {
159
+ dragActive.value = false
160
+ dragOffset.value = 0
161
+ }, 260)
162
+ }
95
163
  const overlayDuration = ref(props.duration + 50)
96
164
  // close事件是否已由组件内部的关闭动作发出,避免show变化时重复发出
97
165
  const closeEmitted = ref(false)
@@ -39,7 +39,8 @@
39
39
 
40
40
  <script setup lang="uts">
41
41
  import { computed, getCurrentInstance, nextTick, onMounted, ref } from 'vue';
42
- import defProps from './radio.uts';
42
+ import defProps from './radio.uts'
43
+ import { upThemeVar } from '../../libs/theme/theme.uts';
43
44
  import { addUnit, addStyle, os, deepMerge, formValidate, error } from '../../libs/function/index.uts';
44
45
  import { useUltraUI } from '../../libs/composable/useUltraUI.uts';
45
46
  /**
@@ -213,7 +214,7 @@
213
214
  const elActiveColor = computed((): string => {
214
215
  return props.activeColor as string != ''
215
216
  ? props.activeColor as string
216
- : (parentData.value['activeColor'] as string != '' ? parentData.value['activeColor'] as string : '#2979ff')
217
+ : (parentData.value['activeColor'] as string != '' ? parentData.value['activeColor'] as string : upThemeVar('--up-primary', '#2979ff'))
217
218
  })
218
219
 
219
220
  // 组件选未中激活时的颜色
@@ -485,8 +486,9 @@
485
486
 
486
487
  &--checked {
487
488
  color: $up-radio-checked-color;
488
- background-color: $up-radio-checked-background-color;
489
- border-color: $up-radio-checked-border-color;
489
+ /* 兜底色接主题变量:activeColor 未显式传时(radio-group 默认)随主色走 */
490
+ background-color: var(--up-primary, $up-radio-checked-background-color);
491
+ border-color: var(--up-primary, $up-radio-checked-border-color);
490
492
  }
491
493
 
492
494
  &--disabled {
@@ -13,7 +13,7 @@ export default {
13
13
  value: '',
14
14
  disabled: false,
15
15
  shape: 'circle',
16
- activeColor: '#2979ff',
16
+ activeColor: 'var(--up-primary, #2979ff)',
17
17
  inactiveColor: '#c8c9cc',
18
18
  name: '',
19
19
  size: 18,
@@ -13,7 +13,7 @@ export default {
13
13
  value: '',
14
14
  disabled: false,
15
15
  shape: 'circle',
16
- activeColor: '#2979ff',
16
+ activeColor: 'var(--up-primary, #2979ff)',
17
17
  inactiveColor: '#c8c9cc',
18
18
  name: '',
19
19
  size: 18,