uview-ultra 4.5.75 → 4.5.78

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,76 @@
1
+ ## 4.5.78
2
+ perf: 同步 uview-plus 3.8.140/3.8.141 三处性能优化,并恢复源码授权价 99.00
3
+
4
+ 本次对齐 uview-plus 3.8.140 与 3.8.141 的三处性能优化(.vue 侧 Vue3 全端完整移植),同时按既定计划恢复源码授权价格。函数返回值与组件行为完全不变。
5
+
6
+ 性能优化(.vue 侧):
7
+ - os()/getDeviceInfo() 结果模块级缓存:设备信息与平台标识运行期不变,消除高频路径重复发起的同步系统信息查询——up-sticky 挂载检测由每次 4 次同步查询降为仅首次 1 次,up-radio 样式 computed 在父组件更新时不再逐实例触发同步查询,up-rate、up-input、up-textarea、up-toast、up-no-network 等调用点同步受益
8
+ - up-navbar:default 模式占位块高度改为 placeholderHeight computed,仅在 height 变化时重算,不再随组件每次重渲染重复读取 statusBarHeight
9
+ - up-tabs:list 深度监听增加内容指纹守卫——父组件重渲染传入内容相同的新数组引用(模板内联数组、重新 map 等常见写法)时,跳过全量 deepClone 与 1+N 次节点测量,仅付一次序列化比对成本;内容真变走原逻辑,deep 监听语义不变
10
+ - .uvue/.uts 侧保持不变:up-navbar 的 uvue 变体已在 setup 一次性读取状态栏高度;up-tabs 的 uvue 深度监听暂不改动,保持 App 端(uni-app x)稳定性
11
+
12
+ 价格恢复:
13
+ - 源码授权价恢复为 99.00 元(4.5.76 源码价 0.00 事故后、4.5.77 按「先不改价格」临时与线上对齐,24h 改价锁已过期),普通授权 9.90 元维持不变
14
+
15
+ ## 4.5.77
16
+ feat: 新增 up-audio 音频播放器组件,up-parse 音频节点改用 up-audio 渲染(对齐 uview-plus 3.8.138)
17
+
18
+ uni-app 在 Vue3 中已废弃 <audio> 组件。本版新增基于 uni.createInnerAudioContext 的公共组件 up-audio,并替换 up-parse 内部已废弃的 <audio> 渲染,富文本里的音频在 H5 / App-Vue / 各小程序端可正常播放。
19
+
20
+ 新增 up-audio 组件:
21
+ - props:src(支持单个地址或地址数组,多源加载失败自动切换下一个源)、title、loop(默认 false)、showRate(默认 true)、rateList(默认 [0.5, 0.75, 1, 1.25, 1.5, 2])、activeColor(默认 '#2979ff')、showTitle(默认 true)
22
+ - 进度条拖动 seek、倍速切换、播放/暂停;事件 play(参数 { source, src })、error(参数 { source, errMsg },全部源失败才抛出)
23
+ - 多实例互斥:播放其中一个自动暂停其它 up-audio 实例
24
+ - 实例方法:toggle / reset / changeRate
25
+ - .vue / .uvue 两套实现同步提供;uvue 侧基于 uni-app x 的 uni.createInnerAudioContext(App-Android 4.33+ / Web 4.0+)
26
+ - 新增示例页 pages/componentsD/audio
27
+
28
+ up-parse 修复:
29
+ - 音频节点原用已废弃的原生 <audio> 标签,uni-app Vue3 下(H5 / App-Vue)该分支被编译条件直接排除,富文本里的音频节点静默不渲染;现统一改由 up-audio 渲染,play / error 事件经 node 转发给 up-parse
30
+ - up-parse 的 .uvue 侧走 rich-text,本就不渲染音频节点,本次不变
31
+
32
+ 验证:Android 全项目编译通过(110 页面,0 error);.uvue 侧已在 MuMu 模拟器(Android)完成真机运行验证:多源回退自动切换、播放/暂停切换、时长与进度更新正常,无 JS 异常。
33
+
34
+ ## 4.5.76
35
+ fix: 修复 up-video 换行 as 断言导致 App 启动白屏(P0),并完成 up-lottie 真机验证
36
+
37
+ 这是一个 P0 回归,4.5.45 引入 up-video 起就存在,请尽快升级。
38
+
39
+ 问题:components/up-video/video.uts 的默认值对象结尾把类型断言写成了换行形式:
40
+
41
+ }
42
+ as UTSJSONObject
43
+
44
+ UTS 编译器对跨行的 as 处理有误,把它拆成三条独立语句输出到 app-service.js:
45
+
46
+ });
47
+ as;
48
+ UTSJSONObject;
49
+
50
+ as 是 TS 保留字、不是变量,App 启动求值 app-service 时直接抛
51
+ ReferenceError: as is not defined,整个应用白屏——不是 up-video 单页失效,
52
+ 而是所有页面都起不来。
53
+
54
+ 长期未被发现的原因:此前校验一直用 launch app-android --compile true,
55
+ 那是只编译不运行,产物里多出来的 as; 语句在编译期完全合法。
56
+
57
+ 修复:把断言合并回同一行 } as UTSJSONObject。已确认修复后
58
+ unpackage/dist/dev/app-android/app-service.js 中不再有孤立的 as; 语句,
59
+ 真机运行日志也不再出现该 ReferenceError。全库已扫描,仅此一处换行写法。
60
+
61
+ up-lottie 的 .uvue 侧完成真机运行时验证(关闭 4.5.75 的遗留项):
62
+ - canvas 桩补 requestAnimationFrame / cancelAnimationFrame。lottie-miniprogram 的
63
+ setup() 会执行 canvas.cancelAnimationFrame.bind(canvas),缺这两个方法时抛
64
+ Cannot read properties of undefined (reading 'bind'),画面空白
65
+ - 示例页 stageStyle 由「传函数引用给 :style」改为 computed 返回样式对象。
66
+ uni-app x 的 :style 不解析函数引用,会触发框架内 normalizeSharedDataStyle 的
67
+ Cannot convert undefined or null to object
68
+ - 验证结果:真机(HUAWEI ABR-AL80 / Android)上示例页正常渲染,
69
+ loading.json 循环播放且连续两帧画面位置不同,2D API 探测全过,
70
+ 无 statechange error、无 JS 异常。up-lottie 在 uni-app x 端可用
71
+
72
+ 另:README / displayName 文案调整为「uniapp-x 蒸汽模式」「120+ 精选组件」。
73
+
1
74
  ## 4.5.75
2
75
  feat: 新增 up-lottie 动画组件(对齐 uview-plus 3.8.135)
3
76
 
@@ -0,0 +1,2 @@
1
+ // 全局实例池:播放某个音频时自动暂停其它 up-audio 实例,所有实例共享(模块级单例)
2
+ export const audioInstancePool = []
@@ -0,0 +1,2 @@
1
+ // 全局实例池:播放某个音频时自动暂停其它 up-audio 实例,所有实例共享(模块级单例)
2
+ export const audioInstancePool: Array<InnerAudioContext> = []
@@ -0,0 +1,19 @@
1
+ export default {
2
+ // audio 音频播放器
3
+ audio: {
4
+ // 音频地址,支持单个地址或地址数组(多源,加载失败自动切换)
5
+ src: '',
6
+ // 标题,显示在音频条上
7
+ title: '',
8
+ // 是否循环播放
9
+ loop: false,
10
+ // 是否显示倍速切换
11
+ showRate: true,
12
+ // 可切换的倍速档位
13
+ rateList: [0.5, 0.75, 1, 1.25, 1.5, 2],
14
+ // 主题色(播放按钮、进度条选中色)
15
+ activeColor: '#2979ff',
16
+ // 是否显示标题
17
+ showTitle: true
18
+ }
19
+ }
@@ -0,0 +1,19 @@
1
+ export default {
2
+ // audio 音频播放器
3
+ audio: {
4
+ // 音频地址,支持单个地址或地址数组(多源,加载失败自动切换)
5
+ src: '',
6
+ // 标题,显示在音频条上
7
+ title: '',
8
+ // 是否循环播放
9
+ loop: false,
10
+ // 是否显示倍速切换
11
+ showRate: true,
12
+ // 可切换的倍速档位
13
+ rateList: [0.5, 0.75, 1, 1.25, 1.5, 2],
14
+ // 主题色(播放按钮、进度条选中色)
15
+ activeColor: '#2979ff',
16
+ // 是否显示标题
17
+ showTitle: true
18
+ }
19
+ } as UTSJSONObject
@@ -0,0 +1,43 @@
1
+ import { defineMixin } from '../../libs/vue.js'
2
+ import defProps from '../../libs/config/props.js'
3
+ export const props = defineMixin({
4
+ props: {
5
+ // 音频地址,支持单个地址或地址数组(多源,加载失败自动切换)
6
+ src: {
7
+ type: [String, Array],
8
+ default: () => defProps.audio.src
9
+ },
10
+ // 标题,显示在音频条上
11
+ title: {
12
+ type: String,
13
+ default: () => defProps.audio.title
14
+ },
15
+ // 是否循环播放
16
+ loop: {
17
+ type: Boolean,
18
+ default: () => defProps.audio.loop
19
+ },
20
+ // 是否显示倍速切换
21
+ showRate: {
22
+ type: Boolean,
23
+ default: () => defProps.audio.showRate
24
+ },
25
+ // 可切换的倍速档位
26
+ rateList: {
27
+ type: Array,
28
+ default: () => defProps.audio.rateList
29
+ },
30
+ // 主题色(播放按钮、进度条选中色)
31
+ activeColor: {
32
+ type: String,
33
+ default: () => defProps.audio.activeColor
34
+ },
35
+ // 是否显示标题
36
+ showTitle: {
37
+ type: Boolean,
38
+ default: () => defProps.audio.showTitle
39
+ }
40
+ }
41
+ })
42
+
43
+ export default props
@@ -0,0 +1,418 @@
1
+ <template>
2
+ <view class="up-audio">
3
+ <!-- 播放/暂停按钮 -->
4
+ <view
5
+ class="up-audio__btn"
6
+ :style="btnStyle"
7
+ @tap.stop="toggle"
8
+ >
9
+ <text class="up-audio__btn-icon">{{ playing ? '❚❚' : '▶' }}</text>
10
+ </view>
11
+ <!-- 主体:标题 + 时间 + 进度条 -->
12
+ <view class="up-audio__main">
13
+ <view class="up-audio__row">
14
+ <text
15
+ v-if="hasTitle"
16
+ class="up-audio__title"
17
+ >{{ displayTitle }}</text>
18
+ <text class="up-audio__time">{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</text>
19
+ </view>
20
+ <slider
21
+ class="up-audio__slider"
22
+ :value="progress"
23
+ :max="100"
24
+ :block-size="10"
25
+ :activeColor="activeColor"
26
+ backgroundColor="#e4e7ed"
27
+ @changing="onChanging"
28
+ @change="onChange"
29
+ />
30
+ </view>
31
+ <!-- 倍速 -->
32
+ <view
33
+ v-if="showRate"
34
+ class="up-audio__rate"
35
+ @tap.stop="changeRate"
36
+ >
37
+ <text class="up-audio__rate-text">{{ rateText }}</text>
38
+ </view>
39
+ </view>
40
+ </template>
41
+
42
+ <script setup lang="uts">
43
+ defineOptions({
44
+ styleIsolation: 'app-and-page',
45
+ name: 'up-audio'
46
+ // #ifdef MP-WEIXIN
47
+ ,
48
+ options: {
49
+ virtualHost: true
50
+ }
51
+ // #endif
52
+ })
53
+
54
+ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
55
+ import defProps from './audio.uts'
56
+ import { audioInstancePool } from './audio-pool.uts'
57
+ /**
58
+ * up-audio 音频播放器
59
+ * @description uni-app x 端基于 uni.createInnerAudioContext 实现播放,可单独使用
60
+ * @property {String | Array} src 音频地址,支持数组多源,加载失败自动切换
61
+ * @property {String} title 标题,显示在音频条上
62
+ * @property {Boolean} loop 是否循环播放 (默认 false )
63
+ * @property {Boolean} showRate 是否显示倍速切换 (默认 true )
64
+ * @property {Array} rateList 可切换的倍速档位 (默认 [0.5, 0.75, 1, 1.25, 1.5, 2] )
65
+ * @property {String} activeColor 主题色(播放按钮、进度条选中色) (默认 '#2979ff' )
66
+ * @property {Boolean} showTitle 是否显示标题 (默认 true )
67
+ * @event {Function} play 开始播放时触发,参数 { source, src }
68
+ * @event {Function} error 加载/播放出错时触发,参数 { source, errMsg }
69
+ * @example <up-audio src="https://cdn.example.com/a.mp3" title="示例音频"></up-audio>
70
+ */
71
+
72
+ // props
73
+ const props = defineProps({
74
+ // 音频地址,支持单个地址或地址数组(多源,加载失败自动切换)
75
+ src: {
76
+ type: [String, Array as PropType<Array<string>>],
77
+ default: ''
78
+ },
79
+ // 标题,显示在音频条上
80
+ title: {
81
+ type: String,
82
+ default: defProps.getString('audio.title')
83
+ },
84
+ // 是否循环播放
85
+ loop: {
86
+ type: Boolean,
87
+ default: defProps.getBoolean('audio.loop')
88
+ },
89
+ // 是否显示倍速切换
90
+ showRate: {
91
+ type: Boolean,
92
+ default: defProps.getBoolean('audio.showRate')
93
+ },
94
+ // 可切换的倍速档位
95
+ rateList: {
96
+ type: Array as PropType<Array<number>>,
97
+ default(): Array<number> {
98
+ return [0.5, 0.75, 1, 1.25, 1.5, 2] as Array<number>
99
+ }
100
+ },
101
+ // 主题色(播放按钮、进度条选中色)
102
+ activeColor: {
103
+ type: String,
104
+ default: defProps.getString('audio.activeColor')
105
+ },
106
+ // 是否显示标题
107
+ showTitle: {
108
+ type: Boolean,
109
+ default: defProps.getBoolean('audio.showTitle')
110
+ },
111
+ customStyle: {
112
+ type: [Object, String],
113
+ default(): UTSJSONObject {
114
+ return {} as UTSJSONObject
115
+ }
116
+ }
117
+ })
118
+
119
+ // 定义事件
120
+ const emit = defineEmits(['play', 'error'])
121
+
122
+ // 响应式数据
123
+ const playing = ref(false)
124
+ const currentTime = ref(0)
125
+ const duration = ref(0)
126
+ const progress = ref(0)
127
+ const srcIndex = ref(0)
128
+ const rate = ref(1.0)
129
+ const seeking = ref(false)
130
+ const errorMsg = ref('')
131
+ let ctx: InnerAudioContext | null = null
132
+
133
+ // 归一化源列表:string 或 Array<string> 统一为非空数组
134
+ const srcList = computed((): Array<string> => {
135
+ const list: Array<string> = []
136
+ const srcValue = props.src
137
+ if (srcValue == null) {
138
+ return list
139
+ }
140
+ if (typeof srcValue == 'string') {
141
+ const single = srcValue as string
142
+ if (single.length > 0) {
143
+ list.push(single)
144
+ }
145
+ } else {
146
+ const arr = srcValue as Array<string>
147
+ for (let i = 0; i < arr.length; i++) {
148
+ const item = arr[i]
149
+ if (item != null && item.length > 0) {
150
+ list.push(item)
151
+ }
152
+ }
153
+ }
154
+ return list
155
+ })
156
+
157
+ const displayTitle = computed((): string => props.title)
158
+ const hasTitle = computed((): boolean => props.showTitle && displayTitle.value.length > 0)
159
+ const currentSrc = computed((): string => {
160
+ const list = srcList.value
161
+ if (srcIndex.value >= 0 && srcIndex.value < list.length) {
162
+ return list[srcIndex.value]
163
+ }
164
+ return ''
165
+ })
166
+ const rateText = computed((): string => {
167
+ const r = rate.value
168
+ const isInt = r == Math.floor(r)
169
+ const base = isInt ? Math.floor(r).toString() : r.toString()
170
+ return base + 'x'
171
+ })
172
+ // 播放按钮底色
173
+ const btnStyle = computed((): string => {
174
+ return 'background-color: ' + props.activeColor
175
+ })
176
+
177
+ // src 变化时重建播放器
178
+ watch((): any => props.src, (_n: any | null) => {
179
+ reset()
180
+ }, { deep: true })
181
+
182
+ onMounted(() => {
183
+ init()
184
+ })
185
+
186
+ // 组件卸载前释放播放器资源
187
+ onBeforeUnmount(() => {
188
+ destroy()
189
+ })
190
+
191
+ // 源变化时重建
192
+ function reset() {
193
+ destroy()
194
+ srcIndex.value = 0
195
+ currentTime.value = 0
196
+ duration.value = 0
197
+ progress.value = 0
198
+ playing.value = false
199
+ init()
200
+ }
201
+
202
+ function init() {
203
+ if (currentSrc.value.length == 0) return
204
+ const audioCtx = uni.createInnerAudioContext()
205
+ ctx = audioCtx
206
+ audioCtx.src = currentSrc.value
207
+ if (props.loop) {
208
+ audioCtx.loop = true
209
+ }
210
+ audioCtx.onPlay((result: any | null) => {
211
+ playing.value = true
212
+ // 暂停实例池中的其它音频,保证多实例互斥
213
+ for (let i = audioInstancePool.length - 1; i >= 0; i--) {
214
+ const other = audioInstancePool[i]
215
+ if (other !== audioCtx) {
216
+ other.pause()
217
+ }
218
+ }
219
+ emit('play', { source: 'audio', src: currentSrc.value } as UTSJSONObject)
220
+ })
221
+ audioCtx.onPause((result: any | null) => {
222
+ playing.value = false
223
+ })
224
+ audioCtx.onStop((result: any | null) => {
225
+ playing.value = false
226
+ })
227
+ audioCtx.onEnded((result: any | null) => {
228
+ playing.value = false
229
+ currentTime.value = 0
230
+ progress.value = 0
231
+ })
232
+ audioCtx.onTimeUpdate((result: any | null) => {
233
+ if (seeking.value) return
234
+ currentTime.value = audioCtx.currentTime
235
+ const dur = audioCtx.duration
236
+ if (dur > 0) {
237
+ duration.value = dur
238
+ }
239
+ progress.value = duration.value > 0 ? currentTime.value / duration.value * 100 : 0
240
+ })
241
+ audioCtx.onCanplay((result: any | null) => {
242
+ const dur = audioCtx.duration
243
+ if (dur > 0) {
244
+ duration.value = dur
245
+ }
246
+ })
247
+ audioCtx.onError((err: ICreateInnerAudioContextFail) => {
248
+ handleError(err)
249
+ })
250
+ audioInstancePool.push(audioCtx)
251
+ }
252
+
253
+ function destroy() {
254
+ const audioCtx = ctx
255
+ if (audioCtx != null) {
256
+ const idx = audioInstancePool.indexOf(audioCtx)
257
+ if (idx != -1) {
258
+ audioInstancePool.splice(idx, 1)
259
+ }
260
+ audioCtx.destroy()
261
+ ctx = null
262
+ }
263
+ }
264
+
265
+ const toggle = () => {
266
+ const audioCtx = ctx
267
+ if (audioCtx == null) return
268
+ if (playing.value) {
269
+ audioCtx.pause()
270
+ } else {
271
+ audioCtx.play()
272
+ }
273
+ }
274
+
275
+ // 拖动进度条中(只更新本地显示)
276
+ const onChanging = (e: UniSliderChangeEvent) => {
277
+ seeking.value = true
278
+ progress.value = e.detail.value
279
+ currentTime.value = duration.value > 0 ? duration.value * e.detail.value / 100 : 0
280
+ }
281
+
282
+ // 拖动结束,真正 seek
283
+ const onChange = (e: UniSliderChangeEvent) => {
284
+ seeking.value = false
285
+ progress.value = e.detail.value
286
+ const audioCtx = ctx
287
+ if (audioCtx != null && duration.value > 0) {
288
+ audioCtx.seek(duration.value * e.detail.value / 100)
289
+ }
290
+ }
291
+
292
+ const changeRate = () => {
293
+ const list = props.rateList
294
+ let i = list.indexOf(rate.value)
295
+ if (i == -1) {
296
+ i = list.indexOf(1)
297
+ }
298
+ rate.value = list[(i + 1) % list.length]
299
+ const audioCtx = ctx
300
+ if (audioCtx != null) {
301
+ audioCtx.playbackRate = rate.value
302
+ }
303
+ }
304
+
305
+ // 加载失败:尝试下一个源;全部失败则抛 error
306
+ function handleError(err: ICreateInnerAudioContextFail) {
307
+ if (srcIndex.value < srcList.value.length - 1) {
308
+ srcIndex.value = srcIndex.value + 1
309
+ currentTime.value = 0
310
+ progress.value = 0
311
+ const audioCtx = ctx
312
+ if (audioCtx != null) {
313
+ audioCtx.src = currentSrc.value
314
+ audioCtx.play()
315
+ }
316
+ return
317
+ }
318
+ playing.value = false
319
+ const msg = err.errMsg
320
+ errorMsg.value = msg == null || msg.length == 0 ? '音频加载失败' : msg
321
+ emit('error', { source: 'audio', errMsg: errorMsg.value } as UTSJSONObject)
322
+ }
323
+
324
+ function formatTime(sec: number): string {
325
+ const total = Math.floor(sec)
326
+ const m = Math.floor(total / 60)
327
+ const s = total % 60
328
+ const sStr = s < 10 ? '0' + s.toString() : s.toString()
329
+ return m.toString() + ':' + sStr
330
+ }
331
+
332
+ // 导出供模板使用的方法
333
+ defineExpose({
334
+ toggle,
335
+ reset,
336
+ changeRate
337
+ })
338
+ </script>
339
+
340
+ <style
341
+ lang="scss"
342
+ scoped
343
+ >
344
+ .up-audio {
345
+ display: flex;
346
+ flex-direction: row;
347
+ align-items: center;
348
+ padding: 12rpx 16rpx;
349
+ margin: 8rpx 0;
350
+ border-radius: 8rpx;
351
+ background-color: #f5f7fa;
352
+
353
+ &__btn {
354
+ flex-shrink: 0;
355
+ width: 56rpx;
356
+ height: 56rpx;
357
+ border-radius: 28rpx;
358
+ display: flex;
359
+ flex-direction: row;
360
+ align-items: center;
361
+ justify-content: center;
362
+ margin-right: 16rpx;
363
+ }
364
+
365
+ &__btn-icon {
366
+ color: #ffffff;
367
+ font-size: 22rpx;
368
+ line-height: 24rpx;
369
+ }
370
+
371
+ &__main {
372
+ flex: 1;
373
+ }
374
+
375
+ &__row {
376
+ display: flex;
377
+ flex-direction: row;
378
+ align-items: center;
379
+ justify-content: space-between;
380
+ margin-bottom: 2rpx;
381
+ }
382
+
383
+ &__title {
384
+ flex: 1;
385
+ font-size: 26rpx;
386
+ color: #303133;
387
+ lines: 1;
388
+ text-overflow: ellipsis;
389
+ }
390
+
391
+ &__time {
392
+ flex-shrink: 0;
393
+ font-size: 22rpx;
394
+ color: #909399;
395
+ margin-left: 12rpx;
396
+ }
397
+
398
+ &__slider {
399
+ margin: 0;
400
+ padding: 0;
401
+ }
402
+
403
+ &__rate {
404
+ flex-shrink: 0;
405
+ margin-left: 16rpx;
406
+ padding: 4rpx 12rpx;
407
+ border-radius: 20rpx;
408
+ border-width: 1px;
409
+ border-style: solid;
410
+ border-color: #dcdfe6;
411
+ }
412
+
413
+ &__rate-text {
414
+ font-size: 22rpx;
415
+ color: #606266;
416
+ }
417
+ }
418
+ </style>