uview-ultra 4.5.48 → 4.5.53

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,48 @@
1
+ ## 4.5.53
2
+ feat: up-navbar 支持滚动磨砂与状态栏占位覆盖,磨砂底色改用主题变量;up-empty 补齐 bottom 具名插槽
3
+
4
+ up-empty(.uvue / .vue):新增 bottom 具名插槽(按钮等操作区),对齐 uview-plus 3.x 接口——此前业务侧 template #bottom 静默不渲染(default 插槽不受影响)。
5
+
6
+ - up-navbar(uni-app x / .uvue)新增 frosted:滚过阈值后淡入一层半透明磨砂(高仿 iOS 材质),供 H5 / Android 等无系统玻璃带的端使用;ios 模式自带磨砂层,二者共用同一套淡入曲线(2px 起、28px 走满)
7
+ - up-navbar 新增 statusBarInsetTop:状态栏占位高度覆盖(-1 用系统值、0 不占位),iOS standalone(PWA)下需传 0,避免导航行被推出系统的 Liquid Glass 滚动边缘带
8
+ - up-navbar 占位高度同时落到根节点并加 flex-shrink: 0,修复 App 端(uvue 原生渲染)「开了 placeholder 但内容仍顶到导航行下」的问题(fixed 层不占流、子节点高度传不到父级 flex 容器)
9
+ - up-navbar 磨砂底色改用主题变量 --up-navbar-glass-bg / --up-navbar-glass-border(light 0.75 / dark 0.78),修复 uvue App 端 color-mix 不生效导致磨砂「看似没开」;状态栏占位由 up-status-bar 改为普通 view,高度可被 statusBarInsetTop 覆盖;bgColor 兜底 #ffffff、修复内容区调试遗留蓝底
10
+ - up-navbar 修复 defineExpose 暴露已重命名变量(statusBarHeight → systemStatusBarHeight)导致整段 setup 编译不过
11
+ - theme.uts 新增 --up-navbar-glass-bg / --up-navbar-glass-border 两个主题变量(明/暗各一套),与 up-root 运行时主题系统联动
12
+ - up-cell(.uvue)默认内距 10px 15px → 14px(large 13px → 16px),left-icon-wrap margin-right 4px → 10px
13
+
14
+ ## 4.5.52
15
+ fix: 修复 up-icon 的 folder 图标在 uni-app x(.uvue)下不显示
16
+
17
+ - up-icon/icons.uts:folder 的 key 误写为 `uiupiconcon-folder`(.js 侧为 `uicon-folder`),
18
+ up-icon.uvue 按 `upicon-folder` 查表落空返回空字符串,图标不渲染;已修正 key,
19
+ 并全量比对 icons.js 与 icons.uts 的 key 集合确认无其它缺失(212 vs 211 → 212 对齐)
20
+
21
+ ## 4.5.51
22
+ fix: 蒸汽模式运行时主题的原生 UI 同步仅在 App 端执行
23
+
24
+ - libs/theme/theme.uts:applyNativeThemeUI 的 setNavigationBarColor / setBackgroundColor / setTabBarStyle 调用结果统一 catch 吞掉(web 端 VDOM 无原生导航语义,未挂载页面时这些 API 会 reject),消除「page not found / not TabBar page」的 unhandledrejection 刷屏
25
+
26
+ ## 4.5.50
27
+ feat: 新增蒸汽模式运行时主题系统与 up-root 页面级根组件,支持运行时暗黑切换
28
+
29
+ uni-app x 蒸汽模式下 App.uvue 不允许 template,无法在应用根注入主题;且样式隔离 2.0 下组件默认 isolated。本次补齐 uview-ultra 在蒸汽模式的暗黑能力:
30
+
31
+ - 新增 libs/theme/theme.uts:运行时主题系统(与 .js 侧 theme.js 同一套默认色值与 u-theme-mode 存储 key),状态基于 vue ref,模板/computed 中调用 upThemeVar() 可建立响应式依赖,切换主题自动刷新。导出 initThemeSystem / setTheme / setThemePreference / getThemePreference / getThemeVars / upThemeVar / upThemeIsDark / upThemeVarsStyle / getThemePageStyle / getThemeCardStyle / getThemeTabBarStyle,index.uts 统一再导出,install 时自动 initThemeSystem()
32
+ - 新增 up-root 页面级根组件(.uvue + .vue 双实现):作为每页根容器,把主题 CSS 变量表(--up-* / --u-*)绑定到根节点 style,页面与 app-and-page 隔离的子组件通过 var(--up-*) 消费;可选注入页面底色/文字色,内嵌全局 toast/notify 宿主(toastHost 属性可关闭)
33
+ - 新增 up-root-toast-host.uvue + libs/root/rootState.uts:up-toast / up-notify 全局宿主注册,导出 rootToast() / rootNotify(),未挂宿主时降级 uni.showToast
34
+ - 用法:每页 <up-root> 包裹根节点;业务样式继续写 var(--up-primary, fallback),脚本侧 upThemeVar('--up-primary', '#3c9cff')
35
+
36
+ ## 4.5.49
37
+ feat: up-goods-sku 支持默认选中与底部安全区配置
38
+
39
+ up-goods-sku 的 .vue 与 .uvue 实现同步补齐 defaultSku 与 safeAreaInsetBottom 能力,行为与 uview-plus 3.x 保持一致。
40
+
41
+ - up-goods-sku:新增 defaultSku 属性,支持设置默认选中的 SKU,并支持响应式更新与重置恢复
42
+ - up-goods-sku:新增 safeAreaInsetBottom 属性(默认 true),可关闭底部安全距离,并透传给 up-popup
43
+ - up-goods-sku:.uvue 侧通过 cloneSkuObject 复制传入的默认选中对象,避免直接引用外部对象
44
+ - 回归验证:新增 scripts/verify-goods-sku-default-selection.mjs,静态断言模板、props、初始化、watch 与 reset 行为
45
+
1
46
  ## 4.5.48
2
47
  fix: 修复 up-tabs-pro、up-novel-reader Vue 版微信小程序编译报错
3
48
 
@@ -200,15 +200,15 @@
200
200
  <style lang="scss" scoped>
201
201
  @import "../../libs/css/components.scss";
202
202
 
203
- $up-cell-padding: 10px 15px !important;
203
+ $up-cell-padding: 14px !important;
204
204
  $up-cell-font-size: 15px !important;
205
205
  $up-cell-line-height: 24px !important;
206
206
  $up-cell-color: $up-main-color !important;
207
207
  $up-cell-icon-size: 16px !important;
208
208
  $up-cell-title-font-size: 15px !important;
209
209
  $up-cell-title-line-height: 22px !important;
210
- $up-cell-title-color: $up-main-color !important;
211
- $up-cell-required-font-size: 14px !important;
210
+ $up-cell-title-color: $up-main-color !important;
211
+ $up-cell-required-font-size: 14px !important;
212
212
  $up-cell-required-color: #f56c6c !important;
213
213
  $up-cell-label-font-size: 12px !important;
214
214
  $up-cell-label-color: $up-tips-color !important;
@@ -217,12 +217,12 @@
217
217
  $up-cell-value-color: $up-content-color !important;
218
218
  $up-cell-clickable-color: $up-bg-color !important;
219
219
  $up-cell-disabled-color: #c8c9cc !important;
220
- $up-cell-padding-top-large: 13px !important;
221
- $up-cell-padding-bottom-large: 13px !important;
220
+ $up-cell-padding-top-large: 16px !important;
221
+ $up-cell-padding-bottom-large: 16px !important;
222
222
  $up-cell-value-font-size-large: 15px !important;
223
223
  $up-cell-label-font-size-large: 14px !important;
224
224
  $up-cell-title-font-size-large: 16px !important;
225
- $up-cell-left-icon-wrap-margin-right: 4px !important;
225
+ $up-cell-left-icon-wrap-margin-right: 10px !important;
226
226
  $up-cell-right-icon-wrap-margin-left: 4px !important;
227
227
  $up-cell-title-flex:1 !important;
228
228
  $up-cell-label-margin-top:5px !important;
@@ -27,6 +27,10 @@
27
27
  <view class="up-empty__wrap" v-if="$slots['default'] != null || $slots['$default'] != null">
28
28
  <slot />
29
29
  </view>
30
+ <!-- 兼容 uview-plus 3.x 的 bottom 具名插槽(按钮等操作区) -->
31
+ <view class="up-empty__wrap" v-if="$slots['bottom'] != null || $slots['$bottom'] != null">
32
+ <slot name="bottom" />
33
+ </view>
30
34
  </view>
31
35
  </template>
32
36
 
@@ -27,6 +27,9 @@
27
27
  <view class="up-empty__wrap" v-if="$slots.default || $slots.$default">
28
28
  <slot />
29
29
  </view>
30
+ <view class="up-empty__wrap" v-if="$slots.bottom || $slots.$bottom">
31
+ <slot name="bottom" />
32
+ </view>
30
33
  </view>
31
34
  </template>
32
35
 
@@ -8,6 +8,7 @@
8
8
  mode="bottom"
9
9
  :closeable="pageInline ? false : closeable"
10
10
  :pageInline="pageInline"
11
+ :safeAreaInsetBottom="safeAreaInsetBottom"
11
12
  :border-radius="20"
12
13
  @close="close"
13
14
  @closed="closedHandler"
@@ -78,7 +79,7 @@
78
79
 
79
80
  <script setup lang="uts">
80
81
  import type { PropType } from 'vue'
81
- import { computed, ref } from 'vue'
82
+ import { computed, ref, watch } from 'vue'
82
83
  function emptySkuList(): Array<UTSJSONObject> {
83
84
  return [] as Array<UTSJSONObject>
84
85
  }
@@ -88,6 +89,17 @@
88
89
  function emptyObject(): UTSJSONObject {
89
90
  return {} as UTSJSONObject
90
91
  }
92
+ function cloneSkuObject(value: UTSJSONObject | null): UTSJSONObject {
93
+ if (value == null) {
94
+ return emptyObject()
95
+ }
96
+ const result = emptyObject()
97
+ const keys = UTSJSONObject.keys(value)
98
+ for (let i = 0; i < keys.length; i++) {
99
+ result[keys[i]] = value[keys[i]]
100
+ }
101
+ return result
102
+ }
91
103
  function normalizeNumber(value: any | null, defaultValue: number = 0): number {
92
104
  if (typeof value === 'number') {
93
105
  return value
@@ -148,6 +160,13 @@ const props = defineProps({
148
160
  return [] as Array<UTSJSONObject>
149
161
  }
150
162
  },
163
+ // 默认选中的SKU
164
+ defaultSku: {
165
+ type: Object as PropType<UTSJSONObject>,
166
+ default(): UTSJSONObject {
167
+ return {} as UTSJSONObject
168
+ }
169
+ },
151
170
  // 最大购买数量
152
171
  maxBuy: {
153
172
  type: Number,
@@ -167,12 +186,17 @@ const props = defineProps({
167
186
  pageInline: {
168
187
  type: Boolean,
169
188
  default: false
189
+ },
190
+ // 是否留出底部安全距离
191
+ safeAreaInsetBottom: {
192
+ type: Boolean,
193
+ default: true
170
194
  }
171
195
  })
172
196
 
173
197
  const emit = defineEmits(['open', 'confirm', 'close', 'closed'])
174
198
  const show = ref(props.pageInline)
175
- const selectedSku = ref(emptyObject())
199
+ const selectedSku = ref(cloneSkuObject(props.defaultSku))
176
200
  const buyNum = ref(1)
177
201
 
178
202
  const skuTreeList = computed((): Array<UTSJSONObject> => {
@@ -380,10 +404,15 @@ const onNumChange = (e: UTSJSONObject): void => {
380
404
  }
381
405
 
382
406
  const reset = (): void => {
383
- selectedSku.value = emptyObject()
407
+ selectedSku.value = cloneSkuObject(props.defaultSku)
384
408
  buyNum.value = 1
385
409
  }
386
410
 
411
+ // 外部更新 defaultSku 时,同步替换内部选中状态
412
+ watch(() : UTSJSONObject | null => props.defaultSku, (newVal: UTSJSONObject | null) => {
413
+ selectedSku.value = cloneSkuObject(newVal)
414
+ })
415
+
387
416
  const open = (): void => {
388
417
  show.value = true
389
418
  emit('open')
@@ -8,6 +8,7 @@
8
8
  mode="bottom"
9
9
  :closeable="pageInline ? false : closeable"
10
10
  :pageInline="pageInline"
11
+ :safeAreaInsetBottom="safeAreaInsetBottom"
11
12
  :border-radius="20"
12
13
  @close="close"
13
14
  @closed="closedHandler"
@@ -77,7 +78,7 @@
77
78
  </template>
78
79
 
79
80
  <script setup>
80
- import { computed, ref } from 'vue'
81
+ import { computed, ref, watch } from 'vue'
81
82
  import { commonProps } from '../../libs/composable/useUltraUI.js'
82
83
  import { t } from '../../libs/i18n/index.js'
83
84
 
@@ -107,6 +108,11 @@ const props = defineProps({
107
108
  type: Array,
108
109
  default: () => []
109
110
  },
111
+ // 默认选中的SKU
112
+ defaultSku: {
113
+ type: Object,
114
+ default: () => ({})
115
+ },
110
116
  // 最大购买数量
111
117
  maxBuy: {
112
118
  type: Number,
@@ -126,12 +132,17 @@ const props = defineProps({
126
132
  pageInline: {
127
133
  type: Boolean,
128
134
  default: false
135
+ },
136
+ // 是否留出底部安全距离
137
+ safeAreaInsetBottom: {
138
+ type: Boolean,
139
+ default: true
129
140
  }
130
141
  })
131
142
  const emit = defineEmits(['open', 'confirm', 'close', 'closed'])
132
143
 
133
144
  const show = ref(false)
134
- const selectedSku = ref({})
145
+ const selectedSku = ref({ ...props.defaultSku })
135
146
  const buyNum = ref(1)
136
147
 
137
148
  if (props.pageInline) {
@@ -270,10 +281,15 @@ function onNumChange(e) {
270
281
  }
271
282
 
272
283
  function reset() {
273
- selectedSku.value = {}
284
+ selectedSku.value = { ...props.defaultSku }
274
285
  buyNum.value = 1
275
286
  }
276
287
 
288
+ // 外部更新 defaultSku 时,同步替换内部选中状态
289
+ watch(() => props.defaultSku, (sku) => {
290
+ selectedSku.value = { ...(sku || {}) }
291
+ })
292
+
277
293
  function open() {
278
294
  show.value = true
279
295
  emit('open')
@@ -10,7 +10,7 @@
10
10
  'upicon-level': '\ue693',
11
11
  'upicon-column-line': '\ue68e',
12
12
  'upicon-checkbox-mark': '\ue807',
13
- 'uiupiconcon-folder': '\ue7f5',
13
+ 'upicon-folder': '\ue7f5',
14
14
  'upicon-movie': '\ue7f6',
15
15
  'upicon-star-fill': '\ue669',
16
16
  'upicon-star': '\ue65f',
@@ -30,6 +30,8 @@ export default {
30
30
  titleStyle: {},
31
31
  mode: 'default',
32
32
  scrollTop: 0,
33
+ frosted: false,
34
+ statusBarInsetTop: -1,
33
35
  avoidCapsule: true
34
36
  }
35
37
  } as UTSJSONObject
@@ -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' : '']"
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
@@ -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,29 @@
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
+ /**
257
+ * 实际使用的状态栏占位高度。
258
+ *
259
+ * `statusBarInsetTop >= 0` 时以它为准,否则用系统值。iOS standalone(PWA,meta=black)
260
+ * 下 webview 本就从系统状态栏下方开始,占位必须传 0 —— 再填一段会把导航行推出
261
+ * iOS 26/27 在页面顶部画的 Liquid Glass 滚动边缘带(约 75pt)。
262
+ */
263
+ const effectiveStatusBarHeight = computed((): number => {
264
+ // 非数字 / undefined 会得到 NaN,`NaN >= 0` 为 false,自然回落到系统值。
265
+ const override = Number(props.statusBarInsetTop)
266
+ if (override >= 0) return override
267
+ return systemStatusBarHeight.value
268
+ })
269
+
270
+ const placeholderHeight = computed((): string => {
271
+ // `height` 取不到时 `parseInt` 会得到 NaN,占位高度就是无效值 → 塌成 0 →
272
+ // fixed 层不占流,内容直接顶到导航栏上。兜 uview 的默认 44。
273
+ const raw = parseInt(getPx(props.height))
274
+ const height = raw > 0 ? raw : 44
275
+ return addUnit(height + effectiveStatusBarHeight.value, 'px')
276
+ })
233
277
 
234
278
  // iOS 大标题行高,同时是压缩进度的分母
235
279
  const LARGE_TITLE_HEIGHT = 52
@@ -254,6 +298,31 @@
254
298
  return props.mode == 'ios'
255
299
  })
256
300
 
301
+ /** 是否处于「fixed + 等高占位」档。 */
302
+ const placeholderActive = computed((): boolean => {
303
+ return !isIosMode.value && props.fixed && props.placeholder
304
+ })
305
+
306
+ /**
307
+ * 根节点样式:占位高度**同时**落在根节点上。
308
+ *
309
+ * 只把高度给内部的 `.up-navbar__placeholder` 是不够的 —— `<up-navbar>` 的根节点
310
+ * 才是父容器的 flex item(会话列表页是 `display: flex; flex-direction: column` 的
311
+ * `.conversations-shell`)。App 端(uvue 原生渲染)下,自定义组件的根节点不会被子
312
+ * 节点的显式高度撑开:占位块在,但它贡献的高度传不到父级;而 fixed 层又不占流,
313
+ * 于是列表第一行直接压在导航行下面 —— 这正是「开了 placeholder 也没用」的实况。
314
+ *
315
+ * 高度值只有一份(`placeholderHeight`),根节点与内部占位块共用,不会叠成两份。
316
+ */
317
+ const rootStyle = computed((): UTSJSONObject => {
318
+ const style = {} as UTSJSONObject
319
+ if (!placeholderActive.value) {
320
+ return style
321
+ }
322
+ style['height'] = placeholderHeight.value
323
+ return style
324
+ })
325
+
257
326
  // 有效大标题行高:title 为空时不渲染大标题行,行高塌陷为 0
258
327
  const largeTitleHeight = computed((): number => {
259
328
  return props.title != '' ? LARGE_TITLE_HEIGHT : 0
@@ -272,12 +341,37 @@
272
341
  return Math.min(Math.max(offset / distance, 0), 1)
273
342
  })
274
343
 
275
- // 磨砂在前半段走完,为居中标题的出现铺好不透明底
344
+ // 非 iOS 模式的磨砂参数:滚过 2px 开始淡入,28px 走满。
345
+ // 「半透明」由 `.up-navbar__glass--frosted` 的底色(主题里的 75%/78% rgba)保证,
346
+ // 这里只负责淡入,不再额外压 opacity —— 否则两层衰减叠起来会淡得看不见。
347
+ //
348
+ // 行程刻意短:iOS 的导航栏在内容压到栏下时是**立刻**出背景(scrollEdge → standard),
349
+ // 过渡只是让离散的滚动事件看起来连续。40px 那种长行程在「列表只滚了一点点」的场景
350
+ // 下永远走不满,看上去就像磨砂没开。
351
+ const FROSTED_START = 2
352
+ const FROSTED_FADE = 28
353
+
354
+ /** 磨砂层要不要渲染。 */
355
+ const showGlass = computed((): boolean => {
356
+ return isIosMode.value || props.frosted
357
+ })
358
+
359
+ /** 是否「非 iOS 模式的滚动磨砂」(决定要不要额外画底部细线)。 */
360
+ const frostedGlass = computed((): boolean => {
361
+ return props.frosted && !isIosMode.value
362
+ })
363
+
364
+ // iOS 模式:磨砂在前半段走完,为居中标题的出现铺好不透明底。
365
+ // frosted 模式:按滚动距离从 0 淡入到 1(「半透明」由底色负责,不是靠 opacity)。
276
366
  const glassOpacity = computed((): number => {
277
- if (!isIosMode.value) {
367
+ if (isIosMode.value) {
368
+ return Math.min(Math.max(iosProgress.value / 0.5, 0), 1)
369
+ }
370
+ if (!props.frosted) {
278
371
  return 0
279
372
  }
280
- return Math.min(Math.max(iosProgress.value / 0.5, 0), 1)
373
+ const offset = parseInt(getPx(props.scrollTop))
374
+ return Math.min(Math.max((offset - FROSTED_START) / FROSTED_FADE, 0), 1)
281
375
  })
282
376
 
283
377
  // 居中标题在后段才启动,此时磨砂层已满不透明,不会与大标题互相透出
@@ -288,16 +382,25 @@
288
382
  return Math.min(Math.max((iosProgress.value - 0.75) / 0.25, 0), 1)
289
383
  })
290
384
 
291
- // ios 模式下状态栏占位透明,背景由磨砂层统一承担
385
+ // 磨砂层显示时状态栏占位透明,背景由磨砂层统一承担
292
386
  const statusBarBgColor = computed((): string => {
293
- return isIosMode.value ? 'transparent' : props.bgColor
387
+ return showGlass.value ? 'transparent' : props.bgColor
388
+ })
389
+
390
+ /** 状态栏占位块的样式(高度可被 `statusBarInsetTop` 覆盖)。 */
391
+ const statusBarSpacerStyle = computed((): UTSJSONObject => {
392
+ const style = {} as UTSJSONObject
393
+ style['height'] = addUnit(effectiveStatusBarHeight.value, 'px')
394
+ style['backgroundColor'] = statusBarBgColor.value
395
+ return style
294
396
  })
295
397
 
296
398
  const flowSpacerStyle = computed((): UTSJSONObject => {
297
399
  const style = {} as UTSJSONObject
298
- let top = parseInt(getPx(props.height))
400
+ const rawTop = parseInt(getPx(props.height))
401
+ let top = rawTop > 0 ? rawTop : 44
299
402
  if (props.safeAreaInsetTop) {
300
- top += statusBarHeight.value
403
+ top += effectiveStatusBarHeight.value
301
404
  }
302
405
  style['height'] = addUnit(top, 'px')
303
406
  return style
@@ -314,12 +417,18 @@
314
417
  return UTSJSONObject.assign(style, addStyle(props.titleStyle) as UTSJSONObject)
315
418
  })
316
419
 
317
- // 磨砂层。App 原生端不支持 backdrop-filter,0.82 不透明底色即为可读性下限;
420
+ // 磨砂层。App 原生端不支持 backdrop-filter 时,底色本身就是可读性下限;
318
421
  // backdrop-filter 仍然写入,在 WebView 类端上会自动生效。
422
+ //
423
+ // frosted 模式**不写** backgroundColor:调用方为了透明顶栏会把 bgColor 传成
424
+ // `transparent`,那样磨砂层就没有底色了。底色交给 `.up-navbar__glass--frosted`
425
+ // 的 CSS(卡片色 72%),与主题变量联动。
319
426
  const glassStyle = computed((): UTSJSONObject => {
320
427
  const style = {} as UTSJSONObject
321
428
  style['opacity'] = glassOpacity.value
322
- style['backgroundColor'] = props.bgColor
429
+ if (!frostedGlass.value) {
430
+ style['backgroundColor'] = props.bgColor
431
+ }
323
432
  return style
324
433
  })
325
434
 
@@ -344,8 +453,8 @@
344
453
  const contentStyle = computed((): UTSJSONObject => {
345
454
  const style = {} as UTSJSONObject
346
455
  style['height'] = addUnit(props.height)
347
- // ios 模式下内容区透明,背景交给磨砂层,避免遮住其淡入效果
348
- style['backgroundColor'] = isIosMode.value ? 'transparent' : props.bgColor
456
+ // 磨砂层显示时内容区透明,背景交给磨砂层,避免遮住其淡入效果
457
+ style['backgroundColor'] = showGlass.value ? 'transparent' : props.bgColor
349
458
  return style
350
459
  })
351
460
 
@@ -376,7 +485,9 @@
376
485
  }
377
486
 
378
487
  defineExpose({
379
- statusBarHeight,
488
+ // 名字跟着内部的 `systemStatusBarHeight` 走。早先这里还叫 `statusBarHeight`,
489
+ // 重命名后忘了同步 —— uts 里引用不存在的标识符,整段 setup 直接编译不过。
490
+ systemStatusBarHeight,
380
491
  placeholderHeight,
381
492
  isIosMode,
382
493
  iosProgress,
@@ -389,6 +500,9 @@
389
500
  @import "../../libs/css/components.scss";
390
501
 
391
502
  .up-navbar {
503
+ // 占位模式下根节点自己带高度;作为 flex 列容器的子项时不能被压缩,
504
+ // 否则内容一多,占位段先被挤掉,列表又顶回导航行下面。
505
+ flex-shrink: 0;
392
506
 
393
507
  &--fixed {
394
508
  position: fixed;
@@ -426,11 +540,29 @@
426
540
  backdrop-filter: saturate(180%) blur(var(--up-navbar-glass-blur, 20px));
427
541
  }
428
542
 
543
+ // 滚动磨砂(非 iOS 模式):底色是半透明的卡片色,配合 backdrop-filter 才是
544
+ // 「高仿 iOS」的材质 —— 不透明底会把背后的模糊完全盖掉。底部细线在内容压到
545
+ // 导航栏下时给出分隔,随磨砂层一起淡入。
546
+ //
547
+ // 底色走**主题变量**(`theme.uts` 里算好的 rgba),不写 `color-mix`:
548
+ // uvue App 端不支持 `color-mix`,整条声明会被丢掉;而 App 端也不支持
549
+ // `backdrop-filter` —— 两层同时失效,滚动时顶栏就跟全透明一样,磨砂等于没开。
550
+ &__glass--frosted {
551
+ background-color: var(--up-navbar-glass-bg, rgba(255, 255, 255, 0.75));
552
+ border-bottom: 1px solid var(--up-navbar-glass-border, rgba(0, 0, 0, 0.08));
553
+ }
554
+
555
+ &__statusbar {
556
+ width: 100%;
557
+ }
558
+
429
559
  &__content {
430
560
  @include flex(row);
431
561
  align-items: center;
432
562
  height: 44px;
433
- background-color: #9acafc;
563
+ // 底色一律由 `contentStyle` 内联给出;这里必须是透明 —— 早先这里是
564
+ // `#9acafc`(primaryDisabled 的调试遗留色),内联背景一旦失效顶栏就整条变蓝。
565
+ background-color: transparent;
434
566
  // 磨砂层是绝对定位的,会盖住同级非定位元素,故内容区显式提升层级
435
567
  position: relative;
436
568
  z-index: 1;
@@ -0,0 +1,28 @@
1
+ /*
2
+ * @Author : jry
3
+ * @Description : uview-ultra up-root component's props mixin file(uni-app Vue3 侧)
4
+ * @version : 4.0
5
+ */
6
+ import { defineMixin } from '../../libs/vue.js'
7
+ import defProps from './root.js'
8
+ let root = defProps['root']
9
+
10
+ export const propsRoot = defineMixin({
11
+ props: {
12
+ // 是否为根节点附加页面底色(--up-page-bg-color)
13
+ background: {
14
+ type: Boolean,
15
+ default: root['background']
16
+ },
17
+ // 是否内嵌全局 toast/notify 宿主
18
+ toastHost: {
19
+ type: Boolean,
20
+ default: root['toastHost']
21
+ },
22
+ // 自定义根节点样式(与主题变量合并,优先级更高)
23
+ customStyle: {
24
+ type: Object,
25
+ default: () => root['customStyle']
26
+ }
27
+ }
28
+ })
@@ -0,0 +1,28 @@
1
+ /*
2
+ * @Author : jry
3
+ * @Description : uview-ultra up-root component's props mixin file
4
+ * @version : 4.0
5
+ */
6
+ import { defineMixin } from '../../libs/vue.uts'
7
+ import defProps from './root.uts'
8
+ let root = defProps['root'] as UTSJSONObject
9
+
10
+ export const propsRoot = defineMixin({
11
+ props: {
12
+ // 是否为根节点附加页面底色(--up-page-bg-color)
13
+ background: {
14
+ type: Boolean,
15
+ default: root['background']
16
+ },
17
+ // 是否内嵌全局 toast/notify 宿主
18
+ toastHost: {
19
+ type: Boolean,
20
+ default: root['toastHost']
21
+ },
22
+ // 自定义根节点样式(与主题变量合并,优先级更高)
23
+ customStyle: {
24
+ type: Object,
25
+ default: () => root['customStyle']
26
+ }
27
+ }
28
+ })
@@ -0,0 +1,14 @@
1
+ /*
2
+ * @Author : jry
3
+ * @Description : up-root 组件默认值(uni-app Vue3 侧,与 root.uts 保持一致)
4
+ */
5
+ export default {
6
+ // root组件
7
+ root: {
8
+ // 是否为根节点附加页面底色(--up-page-bg-color)
9
+ background: true,
10
+ // 是否内嵌全局 toast/notify 宿主
11
+ toastHost: true,
12
+ customStyle: {}
13
+ }
14
+ } as UTSJSONObject