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.
@@ -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({
@@ -19,7 +19,7 @@
19
19
  class="up-navbar__placeholder"
20
20
  v-if="!isIosMode && fixed && placeholder"
21
21
  :style="{
22
- height: addUnit(getPx(height) + sys().statusBarHeight,'px'),
22
+ height: placeholderHeight,
23
23
  }"
24
24
  ></view>
25
25
  <view :class="[(isIosMode || fixed) && 'up-navbar--fixed']">
@@ -190,6 +190,12 @@ const flowSpacerHeight = computed(() => {
190
190
  return addUnit(getPx(props.height) + statusBarHeight, 'px')
191
191
  })
192
192
 
193
+ // default 模式占位块高度。收进 computed 后仅在 height 变化时重算,
194
+ // 状态栏高度不必随每次重渲染重复读取
195
+ const placeholderHeight = computed(() => {
196
+ return addUnit(getPx(props.height) + sys().statusBarHeight, 'px')
197
+ })
198
+
193
199
  // 压缩进度。progress=1 即大标题恰好完全没入导航栏
194
200
  const iosProgress = computed(() => {
195
201
  if (!isIosMode.value) return 1
@@ -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
  */
@@ -230,7 +230,19 @@ watch(() => props.current, (newValue) => {
230
230
  }
231
231
  }, { immediate: true })
232
232
 
233
- watch(() => props.list, () => {
233
+ // 内容指纹守卫:父组件重渲染常用内容相同的新数组引用(模板内联数组、重新 map 等)
234
+ // 触发本 deep watch,内容未变时跳过 deepClone 与 resize 的整轮节点测量。
235
+ // 序列化失败的异构输入(如循环引用)回退为按原逻辑处理
236
+ let listFingerprint = null
237
+ watch(() => props.list, (newValue) => {
238
+ let fingerprint = null
239
+ try {
240
+ fingerprint = JSON.stringify(newValue)
241
+ } catch (e) {}
242
+ if (fingerprint !== null && fingerprint === listFingerprint) {
243
+ return
244
+ }
245
+ listFingerprint = fingerprint
234
246
  syncTabList()
235
247
  nextTick(() => {
236
248
  resize()
@@ -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,
@@ -96,8 +96,14 @@ export function sleep(value = 30) {
96
96
  * @returns {string} 返回所在平台(小写)
97
97
  * @link 运行期判断平台 https://uniapp.dcloud.io/frame?id=判断平台
98
98
  */
99
+ // 平台标识运行期不变,缓存结果避免高频路径重复发起同步桥调用
100
+ let cachedOs = ''
99
101
  export function os() {
100
- return uni.getSystemInfoSync().platform.toLowerCase()
102
+ if (cachedOs) {
103
+ return cachedOs
104
+ }
105
+ cachedOs = uni.getSystemInfoSync().platform.toLowerCase()
106
+ return cachedOs
101
107
  }
102
108
  /**
103
109
  * @description 获取系统信息同步接口
@@ -122,7 +128,13 @@ export function getWindowInfo() {
122
128
  * @description 获取设备信息接口
123
129
  * @link 获取设备信息 https://uniapp.dcloud.io/api/system/info?id=getdeviceinfo
124
130
  */
131
+ // 设备信息运行期不变,缓存结果避免重复发起同步桥调用;
132
+ // 调用方(platform/system 等)均为只读读取,共享同一对象是安全的
133
+ let cachedDeviceInfo = null
125
134
  export function getDeviceInfo() {
135
+ if (cachedDeviceInfo) {
136
+ return cachedDeviceInfo
137
+ }
126
138
  let ret = {}
127
139
  // #ifdef APP || H5 || MP-WEIXIN
128
140
  ret = uni.getDeviceInfo()
@@ -130,6 +142,7 @@ export function getDeviceInfo() {
130
142
  // #ifndef APP || H5 || MP-WEIXIN
131
143
  ret = sys()
132
144
  // #endif
145
+ cachedDeviceInfo = ret
133
146
  return ret
134
147
  }
135
148
 
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.78",
6
6
  "description": "uview-ultra全面兼容uni-app/uni-app-x/鸿蒙/nvue,全面的组件和便捷的工具会让您信手拈来,如鱼得水",
7
7
  "keywords": [
8
8
  "uview",
@@ -20,10 +20,10 @@
20
20
  "dcloudext": {
21
21
  "sale": {
22
22
  "regular": {
23
- "price": "0.00"
23
+ "price": "9.90"
24
24
  },
25
25
  "sourcecode": {
26
- "price": "0.00"
26
+ "price": "99.00"
27
27
  }
28
28
  },
29
29
  "contact": {
@@ -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