uview-ultra 4.5.74 → 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.
@@ -0,0 +1,315 @@
1
+ <template>
2
+ <view class="up-audio">
3
+ <!-- 播放/暂停按钮 -->
4
+ <view
5
+ class="up-audio__btn"
6
+ :style="{ backgroundColor: activeColor }"
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>
43
+ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
44
+ import { props as audioProps } from './props.js'
45
+ import { commonProps } from '../../libs/composable/useUltraUI.js'
46
+ import { audioInstancePool } from './audio-pool.js'
47
+ /**
48
+ * up-audio 音频播放器
49
+ * @description uni-app 在 Vue3 中已废弃 <audio> 组件,本组件基于 uni.createInnerAudioContext 实现播放,可单独使用,up-parse 的音频节点也由本组件渲染。
50
+ * @tutorial https://uview-plus.jiangruyi.com/components/audio.html
51
+ * @property {String | Array} src 音频地址,支持数组多源,加载失败自动切换
52
+ * @property {String} title 标题,显示在音频条上
53
+ * @property {Boolean} loop 是否循环播放 (默认 false )
54
+ * @property {Boolean} showRate 是否显示倍速切换 (默认 true )
55
+ * @property {Array} rateList 可切换的倍速档位 (默认 [0.5, 0.75, 1, 1.25, 1.5, 2] )
56
+ * @property {String} activeColor 主题色(播放按钮、进度条选中色) (默认 '#2979ff' )
57
+ * @property {Boolean} showTitle 是否显示标题 (默认 true )
58
+ * @event {Function} play 开始播放时触发,参数 { source, src }
59
+ * @event {Function} error 加载/播放出错时触发,参数 { source, errMsg }
60
+ * @example <up-audio src="https://cdn.example.com/a.mp3" title="示例音频"></up-audio>
61
+ */
62
+ defineOptions({
63
+ name: 'up-audio',
64
+ // #ifdef MP-WEIXIN
65
+ options: {
66
+ virtualHost: true
67
+ }
68
+ // #endif
69
+ })
70
+
71
+ const props = defineProps({
72
+ ...commonProps,
73
+ ...audioProps.props
74
+ })
75
+ const emit = defineEmits(['play', 'error'])
76
+
77
+ const playing = ref(false)
78
+ const currentTime = ref(0)
79
+ const duration = ref(0)
80
+ const progress = ref(0)
81
+ const srcIndex = ref(0)
82
+ const rate = ref(1)
83
+ const seeking = ref(false)
84
+ const errorMsg = ref('')
85
+ let ctx = null
86
+
87
+ const srcList = computed(() => {
88
+ if (Array.isArray(props.src)) return props.src.filter(Boolean)
89
+ return props.src ? [props.src] : []
90
+ })
91
+ const displayTitle = computed(() => props.title || '')
92
+ const hasTitle = computed(() => props.showTitle && displayTitle.value !== '')
93
+ const currentSrc = computed(() => srcList.value[srcIndex.value] || '')
94
+ const rateText = computed(() => {
95
+ const r = rate.value
96
+ return (Number.isInteger(r) ? r.toFixed(0) : String(r)) + 'x'
97
+ })
98
+
99
+ // src 变化时重建播放器
100
+ watch(() => props.src, () => {
101
+ reset()
102
+ }, { deep: true })
103
+
104
+ onMounted(() => {
105
+ init()
106
+ })
107
+
108
+ onBeforeUnmount(() => {
109
+ destroy()
110
+ })
111
+
112
+ // 源变化时重建
113
+ function reset() {
114
+ destroy()
115
+ srcIndex.value = 0
116
+ currentTime.value = 0
117
+ duration.value = 0
118
+ progress.value = 0
119
+ playing.value = false
120
+ init()
121
+ }
122
+
123
+ function init() {
124
+ if (!currentSrc.value) return
125
+ const audioCtx = uni.createInnerAudioContext()
126
+ ctx = audioCtx
127
+ audioCtx.src = currentSrc.value
128
+ if (props.loop) audioCtx.loop = true
129
+ audioCtx.onPlay(() => {
130
+ playing.value = true
131
+ // 暂停实例池中的其它音频,保证多实例互斥
132
+ for (let i = audioInstancePool.length; i--;) {
133
+ if (audioInstancePool[i] !== audioCtx) {
134
+ try { audioInstancePool[i].pause() } catch (e) {}
135
+ }
136
+ }
137
+ emit('play', {
138
+ source: 'audio',
139
+ src: currentSrc.value
140
+ })
141
+ })
142
+ audioCtx.onPause(() => { playing.value = false })
143
+ audioCtx.onStop(() => { playing.value = false })
144
+ audioCtx.onEnded(() => {
145
+ playing.value = false
146
+ currentTime.value = 0
147
+ progress.value = 0
148
+ })
149
+ audioCtx.onTimeUpdate(() => {
150
+ if (seeking.value) return
151
+ currentTime.value = audioCtx.currentTime || 0
152
+ if (audioCtx.duration) duration.value = audioCtx.duration
153
+ progress.value = duration.value ? (currentTime.value / duration.value) * 100 : 0
154
+ })
155
+ audioCtx.onCanplay(() => {
156
+ if (audioCtx.duration) duration.value = audioCtx.duration
157
+ })
158
+ audioCtx.onError(err => handleError(err))
159
+ audioInstancePool.push(audioCtx)
160
+ }
161
+
162
+ function destroy() {
163
+ if (ctx) {
164
+ const idx = audioInstancePool.indexOf(ctx)
165
+ if (idx > -1) audioInstancePool.splice(idx, 1)
166
+ try { ctx.destroy() } catch (e) {}
167
+ ctx = null
168
+ }
169
+ }
170
+
171
+ function toggle() {
172
+ if (!ctx) return
173
+ if (playing.value) {
174
+ ctx.pause()
175
+ } else {
176
+ ctx.play()
177
+ }
178
+ }
179
+
180
+ // 拖动进度条中(只更新本地显示)
181
+ function onChanging(e) {
182
+ seeking.value = true
183
+ progress.value = e.detail.value
184
+ currentTime.value = duration.value ? (duration.value * e.detail.value) / 100 : 0
185
+ }
186
+
187
+ // 拖动结束,真正 seek
188
+ function onChange(e) {
189
+ seeking.value = false
190
+ progress.value = e.detail.value
191
+ if (ctx && duration.value) {
192
+ try { ctx.seek((duration.value * e.detail.value) / 100) } catch (err) {}
193
+ }
194
+ }
195
+
196
+ function changeRate() {
197
+ const list = (props.rateList && props.rateList.length) ? props.rateList : [0.5, 0.75, 1, 1.25, 1.5, 2]
198
+ let i = list.indexOf(rate.value)
199
+ if (i === -1) i = list.indexOf(1)
200
+ rate.value = list[(i + 1) % list.length]
201
+ if (ctx) {
202
+ try { ctx.playbackRate = rate.value } catch (e) {}
203
+ }
204
+ }
205
+
206
+ // 加载失败:尝试下一个源;全部失败则抛 error
207
+ function handleError(err) {
208
+ if (srcIndex.value < srcList.value.length - 1) {
209
+ srcIndex.value += 1
210
+ currentTime.value = 0
211
+ progress.value = 0
212
+ if (ctx) {
213
+ ctx.src = currentSrc.value
214
+ try { ctx.play() } catch (e) {}
215
+ }
216
+ return
217
+ }
218
+ playing.value = false
219
+ errorMsg.value = (err && err.errMsg) || '音频加载失败'
220
+ emit('error', {
221
+ source: 'audio',
222
+ errMsg: errorMsg.value
223
+ })
224
+ }
225
+
226
+ function formatTime(sec) {
227
+ sec = Math.floor(sec || 0)
228
+ const m = Math.floor(sec / 60)
229
+ const s = sec % 60
230
+ return m + ':' + (s < 10 ? '0' + s : s)
231
+ }
232
+
233
+ defineExpose({
234
+ toggle,
235
+ reset,
236
+ changeRate
237
+ })
238
+ </script>
239
+
240
+ <style
241
+ lang="scss"
242
+ scoped
243
+ >
244
+ .up-audio {
245
+ display: flex;
246
+ align-items: center;
247
+ padding: 12rpx 16rpx;
248
+ margin: 8rpx 0;
249
+ border-radius: 8rpx;
250
+ background-color: #f5f7fa;
251
+
252
+ &__btn {
253
+ flex-shrink: 0;
254
+ width: 56rpx;
255
+ height: 56rpx;
256
+ border-radius: 50%;
257
+ display: flex;
258
+ align-items: center;
259
+ justify-content: center;
260
+ margin-right: 16rpx;
261
+ }
262
+
263
+ &__btn-icon {
264
+ color: #ffffff;
265
+ font-size: 22rpx;
266
+ line-height: 1;
267
+ }
268
+
269
+ &__main {
270
+ flex: 1;
271
+ min-width: 0;
272
+ }
273
+
274
+ &__row {
275
+ display: flex;
276
+ align-items: center;
277
+ justify-content: space-between;
278
+ margin-bottom: 2rpx;
279
+ }
280
+
281
+ &__title {
282
+ flex: 1;
283
+ font-size: 26rpx;
284
+ color: #303133;
285
+ overflow: hidden;
286
+ text-overflow: ellipsis;
287
+ white-space: nowrap;
288
+ }
289
+
290
+ &__time {
291
+ flex-shrink: 0;
292
+ font-size: 22rpx;
293
+ color: #909399;
294
+ margin-left: 12rpx;
295
+ }
296
+
297
+ &__slider {
298
+ margin: 0;
299
+ padding: 0;
300
+ }
301
+
302
+ &__rate {
303
+ flex-shrink: 0;
304
+ margin-left: 16rpx;
305
+ padding: 4rpx 12rpx;
306
+ border-radius: 20rpx;
307
+ border: 1px solid #dcdfe6;
308
+ }
309
+
310
+ &__rate-text {
311
+ font-size: 22rpx;
312
+ color: #606266;
313
+ }
314
+ }
315
+ </style>
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 wechat-miniprogram
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Bodymovin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,47 @@
1
+ # -*- coding: utf-8 -*-
2
+ """生成 uvue 专用的 lottie-miniprogram 副本。
3
+
4
+ lottie-miniprogram 在**模块加载期**就会执行 `var b = wx.getSystemInfoSync()`,
5
+ 而 uni-app x 没有 wx 全局,直接 import 会在启动时抛 ReferenceError。
6
+ 这里在文件最前面补一个模块作用域的 wx 桩,后续压缩代码里的 `wx` 引用
7
+ 会解析到这个局部变量(比挂 globalThis 更可靠,不依赖运行时全局对象)。
8
+
9
+ 只补 getSystemInfoSync / request 两项:
10
+ - getSystemInfoSync:lottie 只用来取 pixelRatio(dpr 我们显式传 1)
11
+ - request:仅在按 path 加载远程 JSON 时使用,本组件只用 animationData
12
+
13
+ 原始文件保持不动,供 .vue 侧(微信小程序/鸿蒙,有真实 wx)使用。
14
+ """
15
+ import io
16
+
17
+ SRC = 'lottie-miniprogram.min.js'
18
+ DST = 'lottie-miniprogram.uvue.js'
19
+
20
+ SHIM = """/* ------------------------------------------------------------------
21
+ * uni-app x 适配层(本文件由 lottie-miniprogram.min.js 自动生成)
22
+ * lottie-miniprogram 在模块加载期即调用 wx.getSystemInfoSync(),
23
+ * 而 uni-app x 没有 wx 全局,故在此补一个模块作用域的 wx 桩。
24
+ * 下方代码与 lottie-miniprogram.min.js 完全一致,未做任何修改。
25
+ * ------------------------------------------------------------------ */
26
+ var wx = {
27
+ getSystemInfoSync: function () {
28
+ try {
29
+ var info = (typeof uni !== 'undefined' && uni.getSystemInfoSync) ? (uni.getSystemInfoSync() || {}) : {}
30
+ return { pixelRatio: info.pixelRatio || 1, platform: info.platform || '' }
31
+ } catch (e) {
32
+ return { pixelRatio: 1, platform: '' }
33
+ }
34
+ },
35
+ request: function () {}
36
+ };
37
+
38
+ """
39
+
40
+ with io.open(SRC, 'rb') as f:
41
+ body = f.read()
42
+
43
+ with io.open(DST, 'wb') as f:
44
+ f.write(SHIM.encode('utf-8'))
45
+ f.write(body)
46
+
47
+ print('generated %s, size=%d' % (DST, len(SHIM.encode('utf-8')) + len(body)))