uview-ultra 4.5.75 → 4.5.77

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,62 @@
1
+ ## 4.5.77
2
+ feat: 新增 up-audio 音频播放器组件,up-parse 音频节点改用 up-audio 渲染(对齐 uview-plus 3.8.138)
3
+
4
+ uni-app 在 Vue3 中已废弃 <audio> 组件。本版新增基于 uni.createInnerAudioContext 的公共组件 up-audio,并替换 up-parse 内部已废弃的 <audio> 渲染,富文本里的音频在 H5 / App-Vue / 各小程序端可正常播放。
5
+
6
+ 新增 up-audio 组件:
7
+ - props:src(支持单个地址或地址数组,多源加载失败自动切换下一个源)、title、loop(默认 false)、showRate(默认 true)、rateList(默认 [0.5, 0.75, 1, 1.25, 1.5, 2])、activeColor(默认 '#2979ff')、showTitle(默认 true)
8
+ - 进度条拖动 seek、倍速切换、播放/暂停;事件 play(参数 { source, src })、error(参数 { source, errMsg },全部源失败才抛出)
9
+ - 多实例互斥:播放其中一个自动暂停其它 up-audio 实例
10
+ - 实例方法:toggle / reset / changeRate
11
+ - .vue / .uvue 两套实现同步提供;uvue 侧基于 uni-app x 的 uni.createInnerAudioContext(App-Android 4.33+ / Web 4.0+)
12
+ - 新增示例页 pages/componentsD/audio
13
+
14
+ up-parse 修复:
15
+ - 音频节点原用已废弃的原生 <audio> 标签,uni-app Vue3 下(H5 / App-Vue)该分支被编译条件直接排除,富文本里的音频节点静默不渲染;现统一改由 up-audio 渲染,play / error 事件经 node 转发给 up-parse
16
+ - up-parse 的 .uvue 侧走 rich-text,本就不渲染音频节点,本次不变
17
+
18
+ 验证:Android 全项目编译通过(110 页面,0 error);.uvue 侧已在 MuMu 模拟器(Android)完成真机运行验证:多源回退自动切换、播放/暂停切换、时长与进度更新正常,无 JS 异常。
19
+
20
+ ## 4.5.76
21
+ fix: 修复 up-video 换行 as 断言导致 App 启动白屏(P0),并完成 up-lottie 真机验证
22
+
23
+ 这是一个 P0 回归,4.5.45 引入 up-video 起就存在,请尽快升级。
24
+
25
+ 问题:components/up-video/video.uts 的默认值对象结尾把类型断言写成了换行形式:
26
+
27
+ }
28
+ as UTSJSONObject
29
+
30
+ UTS 编译器对跨行的 as 处理有误,把它拆成三条独立语句输出到 app-service.js:
31
+
32
+ });
33
+ as;
34
+ UTSJSONObject;
35
+
36
+ as 是 TS 保留字、不是变量,App 启动求值 app-service 时直接抛
37
+ ReferenceError: as is not defined,整个应用白屏——不是 up-video 单页失效,
38
+ 而是所有页面都起不来。
39
+
40
+ 长期未被发现的原因:此前校验一直用 launch app-android --compile true,
41
+ 那是只编译不运行,产物里多出来的 as; 语句在编译期完全合法。
42
+
43
+ 修复:把断言合并回同一行 } as UTSJSONObject。已确认修复后
44
+ unpackage/dist/dev/app-android/app-service.js 中不再有孤立的 as; 语句,
45
+ 真机运行日志也不再出现该 ReferenceError。全库已扫描,仅此一处换行写法。
46
+
47
+ up-lottie 的 .uvue 侧完成真机运行时验证(关闭 4.5.75 的遗留项):
48
+ - canvas 桩补 requestAnimationFrame / cancelAnimationFrame。lottie-miniprogram 的
49
+ setup() 会执行 canvas.cancelAnimationFrame.bind(canvas),缺这两个方法时抛
50
+ Cannot read properties of undefined (reading 'bind'),画面空白
51
+ - 示例页 stageStyle 由「传函数引用给 :style」改为 computed 返回样式对象。
52
+ uni-app x 的 :style 不解析函数引用,会触发框架内 normalizeSharedDataStyle 的
53
+ Cannot convert undefined or null to object
54
+ - 验证结果:真机(HUAWEI ABR-AL80 / Android)上示例页正常渲染,
55
+ loading.json 循环播放且连续两帧画面位置不同,2D API 探测全过,
56
+ 无 statechange error、无 JS 异常。up-lottie 在 uni-app x 端可用
57
+
58
+ 另:README / displayName 文案调整为「uniapp-x 蒸汽模式」「120+ 精选组件」。
59
+
1
60
  ## 4.5.75
2
61
  feat: 新增 up-lottie 动画组件(对齐 uview-plus 3.8.135)
3
62
 
@@ -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>