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.
- package/changelog.md +84 -0
- package/components/up-audio/audio-pool.js +2 -0
- package/components/up-audio/audio-pool.uts +2 -0
- package/components/up-audio/audio.js +19 -0
- package/components/up-audio/audio.uts +19 -0
- package/components/up-audio/props.js +43 -0
- package/components/up-audio/up-audio.uvue +418 -0
- package/components/up-audio/up-audio.vue +315 -0
- package/components/up-lottie/libs/LICENSE-lottie-miniprogram +21 -0
- package/components/up-lottie/libs/LICENSE-lottie-web +21 -0
- package/components/up-lottie/libs/gen-uvue-variant.py +47 -0
- package/components/up-lottie/libs/harmony-canvas-adapter.js +394 -0
- package/components/up-lottie/libs/lottie-miniprogram.min.js +14 -0
- package/components/up-lottie/libs/lottie-miniprogram.uvue.js +32 -0
- package/components/up-lottie/libs/lottie_svg.min.js +6 -0
- package/components/up-lottie/libs/wx-env.js +15 -0
- package/components/up-lottie/lottie.js +14 -0
- package/components/up-lottie/lottie.uts +14 -0
- package/components/up-lottie/props.js +32 -0
- package/components/up-lottie/up-lottie.uvue +514 -0
- package/components/up-lottie/up-lottie.vue +652 -0
- package/components/up-parse/node/node.vue +21 -3
- package/components/up-video/video.uts +1 -2
- package/libs/config/props.js +4 -0
- package/package.json +3 -3
- package/types/comps/audio.d.ts +57 -0
- package/types/comps/lottie.d.ts +34 -0
- package/types/comps.d.ts +121 -120
- package/types/index.d.ts +383 -382
package/changelog.md
CHANGED
|
@@ -1,3 +1,87 @@
|
|
|
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
|
+
|
|
60
|
+
## 4.5.75
|
|
61
|
+
feat: 新增 up-lottie 动画组件(对齐 uview-plus 3.8.135)
|
|
62
|
+
|
|
63
|
+
加载 AE 导出的 Lottie JSON 并播放。API 与 uview-plus 3.x 的 u-lottie 完全一致,.vue / .uvue 两套实现同步提供。
|
|
64
|
+
|
|
65
|
+
新增:
|
|
66
|
+
- up-lottie 组件,props:filePath(http(s) 地址;H5 / App-Vue 额外支持本地静态路径)、animationData(直接传 JSON 对象,优先级高于 filePath)、contentModel(ScaleToFill / AspectFill / Center,默认 AspectFill)、loop(默认 false)、autoplay(默认 true)
|
|
67
|
+
- 实例方法:reload / playAnimation / pauseAnimation / resumeAnimation / cancelAnimation
|
|
68
|
+
- 事件 statechange:ready / play / pause / resume / stop / complete / error(error 附 message);微信小程序从 event.detail 读取,统一按 event.detail || event 兼容
|
|
69
|
+
- 新增示例页 pages/componentsD/lottie,含填充模式切换、播放控制与状态日志
|
|
70
|
+
|
|
71
|
+
平台实现分两条轨:
|
|
72
|
+
- .vue(uni-app Vue3):H5 / App-Vue 走 lottie-web 5.13.0 的 SVG 渲染器,H5 端引擎按需 import() 拆成独立异步 chunk、不进共享入口包,加载失败会重置缓存以便重试;微信小程序 / 鸿蒙走 lottie-miniprogram 1.0.12 + up-canvas 的 canvas 渲染器,鸿蒙无 DOM canvas 节点,由 harmony-canvas-adapter 把旧版 CanvasContext 适配成标准 2D 上下文;nvue 与其他小程序统一通过 statechange 的 error 类型暴露,不发散报错
|
|
73
|
+
- .uvue(uni-app x):无 DOM,不走 SVG 渲染器,直接用 up-canvas 暴露的原生 CanvasRenderingContext2D 承载 lottie-miniprogram 的 canvas 渲染器。lottie-miniprogram 在模块加载期就会调用 wx.getSystemInfoSync(),而 uni-app x 没有 wx 全局,因此单独提供一份注入了 wx 桩的副本 libs/lottie-miniprogram.uvue.js(原文件保持不动,供 .vue 侧使用)
|
|
74
|
+
- .uvue 侧初始化前会探测 2D API 覆盖度(save / restore / bezierCurveTo / setTransform / fillText 等 16 项),缺失时通过 error 事件报出具体缺哪个方法,不会静默白屏
|
|
75
|
+
|
|
76
|
+
已知限制:
|
|
77
|
+
- 同一页面仅支持一个并行的 canvas 轨实例(lottie-miniprogram 持有全局 canvas 引用,组件内已加互斥守卫)
|
|
78
|
+
- 微信小程序 / 鸿蒙 / uni-app x 无法读取本地 JSON 文件,请改用 animationData 或 http(s) 地址
|
|
79
|
+
- 图片类资源在 canvas 轨下受平台限制(小程序不支持动态创建 canvas),建议保持图片原始宽高与 JSON 描述一致
|
|
80
|
+
- 远程 JSON 可包含可执行表达式(lottie 的 expressions 特性),请仅加载可信来源
|
|
81
|
+
- .uvue 侧本次仅完成编译级验证(全项目 109 页面编译通过),未做真机运行时验证;.vue 侧与 3.x 实现一致
|
|
82
|
+
|
|
83
|
+
体量:引擎资源约 451 KB(lottie_svg.min.js 242 KB、lottie-miniprogram.min.js 209 KB、lottie-miniprogram.uvue.js 210 KB 含 wx 桩)。H5 端 lottie-web 为异步 chunk 不影响首屏;仅使用 up-lottie 的项目才会引入对应引擎,不影响其他组件。
|
|
84
|
+
|
|
1
85
|
## 4.5.74
|
|
2
86
|
fix: 对齐 uview-plus 3.8.129 ~ 3.8.137 的上游修复
|
|
3
87
|
|
|
@@ -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>
|