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.
@@ -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>
@@ -37,8 +37,11 @@ defineOptions({
37
37
 
38
38
  /**
39
39
  * lottie-miniprogram 的 setup() 需要一个小程序 canvas 节点:
40
- * 读 width / height、调 getContext('2d')。uni-app x 没有 canvas 节点对象,
41
- * 这里用一个桩把 up-canvas 暴露的原生 2D 上下文包装成它期望的形状。
40
+ * 读 width / height、调 getContext('2d'),并在初始化时接管 requestAnimationFrame /
41
+ * cancelAnimationFrame(内部会执行 `canvas.cancelAnimationFrame.bind(canvas)`,
42
+ * 缺这两个方法会抛 "Cannot read properties of undefined (reading 'bind')")。
43
+ * uni-app x 没有 canvas 节点对象,这里用一个桩把 up-canvas 暴露的原生 2D 上下文
44
+ * 包装成它期望的形状。
42
45
  */
43
46
  class UpLottieCanvasStub {
44
47
  width: number = 0
@@ -54,6 +57,17 @@ class UpLottieCanvasStub {
54
57
  getContext(type: string): CanvasRenderingContext2D {
55
58
  return this.context
56
59
  }
60
+
61
+ // 用 setTimeout 模拟 16ms 一帧;uni-app x 没有 canvas 节点级的 rAF
62
+ requestAnimationFrame(callback: (time: number) => void): number {
63
+ return setTimeout(() => {
64
+ callback(Date.now())
65
+ }, 16)
66
+ }
67
+
68
+ cancelAnimationFrame(handle: number): void {
69
+ clearTimeout(handle)
70
+ }
57
71
  }
58
72
 
59
73
  const props = defineProps({
@@ -41,9 +41,9 @@
41
41
  <iframe v-else-if="n.name==='iframe'" :style="n.attrs.style" :allowfullscreen="n.attrs.allowfullscreen" :frameborder="n.attrs.frameborder" :src="n.attrs.src" />
42
42
  <embed v-else-if="n.name==='embed'" :style="n.attrs.style" :src="n.attrs.src" />
43
43
  <!-- #endif -->
44
- <!-- #ifndef MP-TOUTIAO || ((H5 || APP-PLUS) && VUE3) -->
45
- <!-- 音频 -->
46
- <audio v-else-if="n.name==='audio'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :author="n.attrs.author" :controls="n.attrs.controls" :loop="n.attrs.loop" :name="n.attrs.name" :poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i" @play="play" @error="mediaError" />
44
+ <!-- #ifndef APP-PLUS-NVUE -->
45
+ <!-- 音频:uni-app Vue3 已废弃 <audio> 组件,统一改用 up-audio 播放(基于 uni.createInnerAudioContext) -->
46
+ <up-audio v-else-if="n.name==='audio'" :src="n.src" :title="n.attrs.title || n.attrs.name" :loop="!!n.attrs.loop" @play="onAudioPlay" @error="onAudioError" />
47
47
  <!-- #endif -->
48
48
  <view v-else-if="(n.name==='table'&&n.c)||n.name==='li'" :id="n.attrs.id" :class="'_'+n.name+' '+n.attrs.class" :style="n.attrs.style">
49
49
  <node v-if="n.name==='li'" :childs="n.children" :opts="opts" />
@@ -112,6 +112,10 @@ import { getCurrentInstance, nextTick, onBeforeUnmount, onMounted, reactive } fr
112
112
  import node from './node'
113
113
  // #endif
114
114
 
115
+ // #ifndef APP-PLUS-NVUE
116
+ import UpAudio from '../up-audio/up-audio.vue'
117
+ // #endif
118
+
115
119
  defineOptions({
116
120
  name: 'node',
117
121
  options: {
@@ -233,6 +237,20 @@ function play(e) {
233
237
  // #endif
234
238
  }
235
239
 
240
+ /**
241
+ * @description 音频播放事件(up-audio 触发,转发给 up-parse)
242
+ */
243
+ function onAudioPlay(payload) {
244
+ root?.$emit('play', payload)
245
+ }
246
+
247
+ /**
248
+ * @description 音频加载/播放错误事件(up-audio 触发,转发给 up-parse)
249
+ */
250
+ function onAudioError(payload) {
251
+ root?.$emit('error', payload)
252
+ }
253
+
236
254
  /**
237
255
  * @description 图片点击事件
238
256
  */
@@ -74,5 +74,4 @@ export default {
74
74
  // 进度条与选中态颜色
75
75
  activeColor: '#2979ff'
76
76
  }
77
- }
78
- as UTSJSONObject
77
+ } as UTSJSONObject
@@ -8,6 +8,7 @@ import config from './config.js'
8
8
  import ActionSheet from '../../components/up-action-sheet/actionSheet.js'
9
9
  import Album from '../../components/up-album/album.js'
10
10
  import Alert from '../../components/up-alert/alert.js'
11
+ import Audio from '../../components/up-audio/audio.js'
11
12
  import Avatar from '../../components/up-avatar/avatar.js'
12
13
  import AvatarGroup from '../../components/up-avatar-group/avatarGroup.js'
13
14
  import Backtop from '../../components/up-back-top/backtop.js'
@@ -106,6 +107,7 @@ const props = {
106
107
  ...ActionSheet,
107
108
  ...Album,
108
109
  ...Alert,
110
+ ...Audio,
109
111
  ...Avatar,
110
112
  ...AvatarGroup,
111
113
  ...Backtop,
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "id": "uview-ultra",
3
3
  "name": "uview-ultra",
4
- "displayName": "零云®UviewUltra v4重磅发布,大满贯全面兼容uniapp-x、uniapp鸿蒙组件库。",
5
- "version": "4.5.75",
4
+ "displayName": "零云®UviewUltra v4重磅发布,大满贯全面兼容uniapp-x蒸汽模式、uniapp鸿蒙组件库。",
5
+ "version": "4.5.77",
6
6
  "description": "uview-ultra全面兼容uni-app/uni-app-x/鸿蒙/nvue,全面的组件和便捷的工具会让您信手拈来,如鱼得水",
7
7
  "keywords": [
8
8
  "uview",
@@ -20,7 +20,7 @@
20
20
  "dcloudext": {
21
21
  "sale": {
22
22
  "regular": {
23
- "price": "0.00"
23
+ "price": "9.90"
24
24
  },
25
25
  "sourcecode": {
26
26
  "price": "0.00"
@@ -0,0 +1,57 @@
1
+ import { AllowedComponentProps, VNodeProps } from './_common'
2
+
3
+ declare interface AudioProps {
4
+ /**
5
+ * 音频地址,支持单个地址或地址数组(多源,加载失败自动切换)
6
+ * @default ""
7
+ */
8
+ src?: string | string[]
9
+ /**
10
+ * 标题,显示在音频条上
11
+ * @default ""
12
+ */
13
+ title?: string
14
+ /**
15
+ * 是否循环播放
16
+ * @default false
17
+ */
18
+ loop?: boolean
19
+ /**
20
+ * 是否显示倍速切换
21
+ * @default true
22
+ */
23
+ showRate?: boolean
24
+ /**
25
+ * 可切换的倍速档位
26
+ * @default [0.5, 0.75, 1, 1.25, 1.5, 2]
27
+ */
28
+ rateList?: number[]
29
+ /**
30
+ * 主题色(播放按钮、进度条选中色)
31
+ * @default "#2979ff"
32
+ */
33
+ activeColor?: string
34
+ /**
35
+ * 是否显示标题
36
+ * @default true
37
+ */
38
+ showTitle?: boolean
39
+ /**
40
+ * 开始播放时触发
41
+ */
42
+ onPlay?: (e: { source: string; src: string }) => any
43
+ /**
44
+ * 加载/播放出错时触发
45
+ */
46
+ onError?: (e: { source: string; errMsg: string }) => any
47
+ }
48
+
49
+ declare interface _Audio {
50
+ new (): {
51
+ $props: AllowedComponentProps &
52
+ VNodeProps &
53
+ AudioProps
54
+ }
55
+ }
56
+
57
+ export declare const Audio: _Audio