oxy-uni-ui 1.2.0 → 2.0.0

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.
Files changed (306) hide show
  1. package/attributes.json +1 -1
  2. package/components/common/abstracts/variable.scss +396 -321
  3. package/components/common/path.ts +9 -0
  4. package/components/common/util.ts +200 -5
  5. package/components/composables/index.ts +1 -0
  6. package/components/composables/useGlobalLoading.ts +42 -0
  7. package/components/composables/useGlobalMessage.ts +48 -0
  8. package/components/composables/useGlobalToast.ts +84 -0
  9. package/components/composables/usePopover.ts +24 -20
  10. package/components/composables/useVirtualScroll.ts +13 -11
  11. package/components/composables/useWindowResize.ts +35 -0
  12. package/components/oxy-action-sheet/index.scss +24 -11
  13. package/components/oxy-action-sheet/oxy-action-sheet.vue +27 -19
  14. package/components/oxy-action-sheet/types.ts +7 -0
  15. package/components/oxy-backtop/index.scss +3 -3
  16. package/components/oxy-backtop/oxy-backtop.vue +9 -6
  17. package/components/oxy-backtop/types.ts +7 -7
  18. package/components/oxy-badge/index.scss +4 -4
  19. package/components/oxy-badge/oxy-badge.vue +3 -3
  20. package/components/oxy-badge/types.ts +2 -2
  21. package/components/oxy-button/index.scss +5 -5
  22. package/components/oxy-button/oxy-button.vue +5 -1
  23. package/components/oxy-calendar/index.scss +11 -11
  24. package/components/oxy-calendar/oxy-calendar.vue +1 -0
  25. package/components/oxy-calendar/types.ts +5 -0
  26. package/components/oxy-calendar-view/month/index.scss +4 -4
  27. package/components/oxy-calendar-view/month/types.ts +36 -0
  28. package/components/oxy-calendar-view/monthPanel/index.scss +7 -7
  29. package/components/oxy-calendar-view/monthPanel/month-panel.vue +14 -8
  30. package/components/oxy-calendar-view/year/index.scss +4 -4
  31. package/components/oxy-calendar-view/yearPanel/index.scss +4 -4
  32. package/components/oxy-calendar-view/yearPanel/year-panel.vue +21 -5
  33. package/components/oxy-card/index.scss +2 -2
  34. package/components/oxy-cell/index.scss +8 -8
  35. package/components/oxy-cell/oxy-cell.vue +15 -2
  36. package/components/oxy-cell/types.ts +4 -0
  37. package/components/oxy-checkbox/index.scss +8 -8
  38. package/components/oxy-checkbox/oxy-checkbox.vue +2 -2
  39. package/components/oxy-checkbox-group/index.scss +2 -2
  40. package/components/oxy-circle/oxy-circle.vue +10 -7
  41. package/components/oxy-circle/types.ts +5 -5
  42. package/components/oxy-col/oxy-col.vue +2 -2
  43. package/components/oxy-col-picker/index.scss +4 -4
  44. package/components/oxy-col-picker/oxy-col-picker.vue +9 -5
  45. package/components/oxy-col-picker/types.ts +12 -3
  46. package/components/oxy-collapse/index.scss +2 -2
  47. package/components/oxy-collapse-item/oxy-collapse-item.vue +3 -3
  48. package/components/oxy-corner/index.scss +32 -32
  49. package/components/oxy-corner/oxy-corner.vue +15 -3
  50. package/components/oxy-corner/types.ts +15 -1
  51. package/components/oxy-count-to/oxy-count-to.vue +3 -3
  52. package/components/oxy-curtain/index.scss +15 -15
  53. package/components/oxy-curtain/oxy-curtain.vue +4 -2
  54. package/components/oxy-curtain/types.ts +6 -1
  55. package/components/oxy-date-strip/index.scss +10 -0
  56. package/components/oxy-date-strip/oxy-date-strip.vue +198 -0
  57. package/components/oxy-date-strip/types.ts +98 -0
  58. package/components/oxy-date-strip/utils.ts +67 -0
  59. package/components/oxy-date-strip-item/index.scss +94 -0
  60. package/components/oxy-date-strip-item/oxy-date-strip-item.vue +102 -0
  61. package/components/oxy-date-strip-item/types.ts +53 -0
  62. package/components/oxy-datetime-picker/index.scss +11 -11
  63. package/components/oxy-datetime-picker/oxy-datetime-picker.vue +4 -1
  64. package/components/oxy-datetime-picker/types.ts +10 -1
  65. package/components/oxy-drop-menu/index.scss +3 -3
  66. package/components/oxy-drop-menu/oxy-drop-menu.vue +3 -3
  67. package/components/oxy-drop-menu-item/index.scss +1 -1
  68. package/components/oxy-drop-menu-item/oxy-drop-menu-item.vue +4 -3
  69. package/components/oxy-drop-menu-item/types.ts +5 -0
  70. package/components/oxy-echarts/index.scss +17 -0
  71. package/components/oxy-echarts/index.ts +1 -0
  72. package/components/oxy-echarts/oxy-echarts.vue +32 -0
  73. package/components/oxy-echarts/types.ts +18 -0
  74. package/components/oxy-fab/index.scss +8 -8
  75. package/components/oxy-fab/oxy-fab.vue +22 -3
  76. package/components/oxy-file-list/index.scss +42 -15
  77. package/components/oxy-file-list/oxy-file-list.vue +208 -34
  78. package/components/oxy-file-list/types.ts +58 -2
  79. package/components/oxy-floating-panel/oxy-floating-panel.vue +13 -9
  80. package/components/oxy-floating-panel/{type.ts → types.ts} +8 -8
  81. package/components/oxy-footer/index.scss +19 -0
  82. package/components/oxy-footer/oxy-footer.vue +78 -0
  83. package/components/oxy-footer/types.ts +17 -0
  84. package/components/oxy-form-item/types.ts +22 -1
  85. package/components/oxy-gap/oxy-gap.vue +2 -2
  86. package/components/oxy-gap/types.ts +2 -2
  87. package/components/oxy-global-loading/oxy-global-loading.vue +53 -0
  88. package/components/oxy-global-message/oxy-global-message.vue +64 -0
  89. package/components/oxy-global-toast/oxy-global-toast.vue +53 -0
  90. package/components/oxy-grid/oxy-grid.vue +1 -1
  91. package/components/oxy-grid/types.ts +1 -1
  92. package/components/oxy-grid-item/index.scss +1 -1
  93. package/components/oxy-grid-item/oxy-grid-item.vue +7 -5
  94. package/components/oxy-grid-item/types.ts +1 -1
  95. package/components/oxy-guidance/index.scss +75 -0
  96. package/components/oxy-guidance/oxy-guidance.vue +201 -0
  97. package/components/oxy-guidance/types.ts +33 -0
  98. package/components/oxy-icon/oxy-icon.vue +2 -2
  99. package/components/oxy-icon/types.ts +1 -1
  100. package/components/oxy-img/oxy-img.vue +4 -4
  101. package/components/oxy-img/types.ts +3 -3
  102. package/components/oxy-img-cropper/index.scss +12 -12
  103. package/components/oxy-img-cropper/oxy-img-cropper.vue +97 -52
  104. package/components/oxy-img-cropper/types.ts +2 -2
  105. package/components/oxy-img-lazy/index.scss +17 -0
  106. package/components/oxy-img-lazy/oxy-img-lazy.vue +332 -0
  107. package/components/oxy-img-lazy/types.ts +69 -0
  108. package/components/oxy-index-anchor/index.scss +2 -2
  109. package/components/oxy-index-anchor/oxy-index-anchor.vue +2 -2
  110. package/components/oxy-index-anchor/{type.ts → types.ts} +3 -0
  111. package/components/oxy-index-bar/index.scss +3 -3
  112. package/components/oxy-index-bar/oxy-index-bar.vue +3 -3
  113. package/components/oxy-index-bar/{type.ts → types.ts} +2 -2
  114. package/components/oxy-input/index.scss +1 -1
  115. package/components/oxy-input-number/index.scss +5 -5
  116. package/components/oxy-input-number/oxy-input-number.vue +2 -2
  117. package/components/oxy-input-number/types.ts +3 -2
  118. package/components/oxy-keyboard/index.scss +5 -5
  119. package/components/oxy-keyboard/key/index.scss +3 -3
  120. package/components/oxy-keyboard/key/index.vue +2 -2
  121. package/components/oxy-keyboard/key/types.ts +15 -0
  122. package/components/oxy-keyboard/oxy-keyboard.vue +1 -0
  123. package/components/oxy-keyboard/types.ts +5 -0
  124. package/components/oxy-link/index.scss +57 -0
  125. package/components/oxy-link/oxy-link.vue +130 -0
  126. package/components/oxy-link/types.ts +81 -0
  127. package/components/oxy-list/index.scss +7 -1
  128. package/components/oxy-list/oxy-list.vue +4 -3
  129. package/components/oxy-list/types.ts +1 -1
  130. package/components/oxy-loading/oxy-loading.vue +8 -4
  131. package/components/oxy-loading/types.ts +1 -1
  132. package/components/oxy-loadmore/index.scss +3 -3
  133. package/components/oxy-long-press-menu/index.scss +93 -0
  134. package/components/oxy-long-press-menu/oxy-long-press-menu.vue +338 -0
  135. package/components/oxy-long-press-menu/types.ts +34 -0
  136. package/components/oxy-message-box/index.scss +12 -11
  137. package/components/oxy-message-box/oxy-message-box.vue +11 -3
  138. package/components/oxy-message-box/types.ts +14 -0
  139. package/components/oxy-navbar/index.scss +2 -2
  140. package/components/oxy-navbar/oxy-navbar.vue +58 -13
  141. package/components/oxy-navbar/types.ts +8 -1
  142. package/components/oxy-navbar-capsule/types.ts +3 -0
  143. package/components/oxy-notice-bar/index.scss +3 -3
  144. package/components/oxy-notice-bar/oxy-notice-bar.vue +9 -5
  145. package/components/oxy-notice-bar/types.ts +3 -3
  146. package/components/oxy-notify/index.ts +1 -0
  147. package/components/oxy-notify/oxy-notify.vue +3 -2
  148. package/components/oxy-notify/types.ts +7 -0
  149. package/components/oxy-pagination/index.scss +1 -1
  150. package/components/oxy-password-input/oxy-password-input.vue +2 -2
  151. package/components/oxy-password-input/types.ts +1 -1
  152. package/components/oxy-picker/index.scss +45 -2
  153. package/components/oxy-picker/oxy-picker.vue +103 -14
  154. package/components/oxy-picker/types.ts +33 -1
  155. package/components/oxy-picker-view/index.scss +3 -3
  156. package/components/oxy-picker-view/oxy-picker-view.vue +4 -4
  157. package/components/oxy-popover/index.scss +9 -9
  158. package/components/oxy-popup/index.scss +2 -2
  159. package/components/oxy-popup/oxy-popup.vue +35 -2
  160. package/components/oxy-popup/types.ts +8 -1
  161. package/components/oxy-progress/index.scss +3 -3
  162. package/components/oxy-qrcode/draw.ts +398 -0
  163. package/components/oxy-qrcode/index.scss +2 -0
  164. package/components/oxy-qrcode/oxy-qrcode.vue +124 -0
  165. package/components/oxy-qrcode/qrcode.ts +936 -0
  166. package/components/oxy-qrcode/types.ts +42 -0
  167. package/components/oxy-radio/index.scss +13 -13
  168. package/components/oxy-radio/oxy-radio.vue +1 -1
  169. package/components/oxy-radio-group/index.scss +2 -2
  170. package/components/oxy-rate/types.ts +4 -4
  171. package/components/oxy-resize/index.scss +2 -2
  172. package/components/oxy-resize/oxy-resize.vue +4 -4
  173. package/components/oxy-resize/types.ts +3 -0
  174. package/components/oxy-rich-text/icon/emjio.svg +1 -0
  175. package/components/oxy-rich-text/icon/quote.svg +1 -0
  176. package/components/oxy-rich-text/icon/text.svg +1 -0
  177. package/components/oxy-rich-text/icon/title.svg +1 -0
  178. package/components/oxy-rich-text/index.scss +160 -0
  179. package/components/oxy-rich-text/mp-html/card/card.vue +122 -0
  180. package/components/oxy-rich-text/mp-html/card/index.js +7 -0
  181. package/components/oxy-rich-text/mp-html/editable/config.js +15 -0
  182. package/components/oxy-rich-text/mp-html/editable/index.js +553 -0
  183. package/components/oxy-rich-text/mp-html/emoji/index.js +203 -0
  184. package/components/oxy-rich-text/mp-html/highlight/config.js +5 -0
  185. package/components/oxy-rich-text/mp-html/highlight/index.js +96 -0
  186. package/components/oxy-rich-text/mp-html/highlight/prism.css +1 -0
  187. package/components/oxy-rich-text/mp-html/highlight/prism.min.js +7 -0
  188. package/components/oxy-rich-text/mp-html/img-cache/index.js +138 -0
  189. package/components/oxy-rich-text/mp-html/latex/index.js +80 -0
  190. package/components/oxy-rich-text/mp-html/latex/katex.css +1 -0
  191. package/components/oxy-rich-text/mp-html/latex/katex.min.js +1 -0
  192. package/components/oxy-rich-text/mp-html/markdown/index.js +50 -0
  193. package/components/oxy-rich-text/mp-html/markdown/marked.min.js +71 -0
  194. package/components/oxy-rich-text/mp-html/mp-html.d.ts +184 -0
  195. package/components/oxy-rich-text/mp-html/mp-html.vue +684 -0
  196. package/components/oxy-rich-text/mp-html/node/node.vue +1172 -0
  197. package/components/oxy-rich-text/mp-html/parser.js +1428 -0
  198. package/components/oxy-rich-text/mp-html/search/index.js +132 -0
  199. package/components/oxy-rich-text/mp-html/style/index.js +129 -0
  200. package/components/oxy-rich-text/mp-html/style/parser.js +175 -0
  201. package/components/oxy-rich-text/mp-html/template/index.js +67 -0
  202. package/components/oxy-rich-text/mp-html/txv-video/index.js +46 -0
  203. package/components/oxy-rich-text/oxy-rich-text.vue +642 -0
  204. package/components/oxy-rich-text/types.ts +76 -0
  205. package/components/oxy-row/oxy-row.vue +3 -3
  206. package/components/oxy-row/types.ts +1 -1
  207. package/components/oxy-search/index.scss +3 -3
  208. package/components/oxy-segmented/index.scss +16 -16
  209. package/components/oxy-segmented/oxy-segmented.vue +23 -3
  210. package/components/oxy-select/index.scss +331 -0
  211. package/components/oxy-select/oxy-select.vue +456 -0
  212. package/components/oxy-select/types.ts +83 -0
  213. package/components/oxy-select-picker/index.scss +7 -7
  214. package/components/oxy-select-picker/oxy-select-picker.vue +4 -0
  215. package/components/oxy-select-picker/types.ts +7 -1
  216. package/components/oxy-sidebar-item/index.scss +1 -1
  217. package/components/oxy-signature/oxy-signature.vue +18 -10
  218. package/components/oxy-signature/types.ts +106 -13
  219. package/components/oxy-skeleton/oxy-skeleton.vue +6 -6
  220. package/components/oxy-skeleton/types.ts +1 -1
  221. package/components/oxy-slider/index.scss +3 -3
  222. package/components/oxy-sort-button/index.scss +8 -8
  223. package/components/oxy-status-tip/index.scss +4 -4
  224. package/components/oxy-status-tip/oxy-status-tip.vue +5 -5
  225. package/components/oxy-status-tip/types.ts +3 -3
  226. package/components/oxy-step/index.scss +14 -14
  227. package/components/oxy-sticky/oxy-sticky.vue +6 -6
  228. package/components/oxy-stream-render/index.scss +6 -0
  229. package/components/oxy-stream-render/oxy-stream-render.vue +204 -0
  230. package/components/oxy-stream-render/types.ts +8 -0
  231. package/components/oxy-swipe-action/oxy-swipe-action.vue +27 -2
  232. package/components/oxy-swiper/oxy-swiper.vue +6 -6
  233. package/components/oxy-swiper/types.ts +5 -5
  234. package/components/oxy-switch/index.scss +8 -8
  235. package/components/oxy-switch/oxy-switch.vue +2 -2
  236. package/components/oxy-switch/types.ts +1 -1
  237. package/components/oxy-tab/index.scss +11 -1
  238. package/components/oxy-tabbar/index.scss +1 -1
  239. package/components/oxy-tabbar/oxy-tabbar.vue +39 -10
  240. package/components/oxy-table/index.scss +5 -5
  241. package/components/oxy-table/oxy-table.vue +8 -6
  242. package/components/oxy-table/types.ts +2 -2
  243. package/components/oxy-table-col/oxy-table-col.vue +3 -3
  244. package/components/oxy-table-col/types.ts +2 -2
  245. package/components/oxy-tabs/index.scss +43 -15
  246. package/components/oxy-tabs/oxy-tabs.vue +53 -19
  247. package/components/oxy-tabs/types.ts +15 -3
  248. package/components/oxy-tag/index.scss +15 -15
  249. package/components/oxy-text/index.scss +5 -1
  250. package/components/oxy-text/oxy-text.vue +76 -7
  251. package/components/oxy-text/types.ts +12 -0
  252. package/components/oxy-textarea/index.scss +6 -6
  253. package/components/oxy-toast/oxy-toast.vue +24 -8
  254. package/components/oxy-tooltip/index.scss +9 -9
  255. package/components/oxy-tree/index.scss +61 -9
  256. package/components/oxy-tree/oxy-tree.vue +102 -17
  257. package/components/oxy-tree/types.ts +23 -10
  258. package/components/oxy-upload/index.scss +21 -21
  259. package/components/oxy-upload/types.ts +2 -2
  260. package/components/oxy-verification-code/index.scss +6 -0
  261. package/components/oxy-verification-code/oxy-verification-code.vue +187 -0
  262. package/components/oxy-verification-code/types.ts +82 -0
  263. package/components/oxy-video-preview/index.scss +4 -4
  264. package/components/oxy-virtual-scroll/index.scss +4 -4
  265. package/components/oxy-virtual-scroll/oxy-virtual-scroll.vue +11 -7
  266. package/components/oxy-virtual-scroll/types.ts +14 -14
  267. package/components/oxy-voice-player/index.scss +908 -0
  268. package/components/oxy-voice-player/oxy-voice-player.vue +821 -0
  269. package/components/oxy-voice-player/types.ts +567 -0
  270. package/components/oxy-waterfall/index.scss +18 -0
  271. package/components/oxy-waterfall/oxy-waterfall.vue +218 -0
  272. package/components/oxy-waterfall/types.ts +90 -0
  273. package/components/oxy-waterfall-item/index.scss +8 -0
  274. package/components/oxy-waterfall-item/oxy-waterfall-item.vue +89 -0
  275. package/components/oxy-waterfall-item/types.ts +16 -0
  276. package/components/oxy-watermark/oxy-watermark.vue +35 -13
  277. package/components/oxy-watermark/types.ts +14 -14
  278. package/global.d.ts +9 -0
  279. package/index.ts +3 -0
  280. package/locale/lang/ar-SA.ts +3 -0
  281. package/locale/lang/en-US.ts +29 -0
  282. package/locale/lang/zh-CN.ts +29 -0
  283. package/package.json +97 -1
  284. package/tags.json +1 -1
  285. package/uni-echarts/changelog.md +2 -0
  286. package/uni-echarts/components/index.js +1 -0
  287. package/uni-echarts/components/uni-echarts/events.js +95 -0
  288. package/uni-echarts/components/uni-echarts/types.d.ts +183 -0
  289. package/uni-echarts/components/uni-echarts/types.js +1 -0
  290. package/uni-echarts/components/uni-echarts/uni-echarts.vue +530 -0
  291. package/uni-echarts/components/uni-echarts/uni-echarts.vue.d.ts +19 -0
  292. package/uni-echarts/global.d.ts +7 -0
  293. package/uni-echarts/index.d.ts +440 -0
  294. package/uni-echarts/index.js +2 -0
  295. package/uni-echarts/package.json +105 -0
  296. package/uni-echarts/shared-core.d.ts +269 -0
  297. package/uni-echarts/shared-core.js +900 -0
  298. package/web-types.json +1 -1
  299. package/components/oxy-number-keyboard/index.scss +0 -78
  300. package/components/oxy-number-keyboard/key/index.scss +0 -81
  301. package/components/oxy-number-keyboard/key/index.vue +0 -78
  302. package/components/oxy-number-keyboard/key/types.ts +0 -11
  303. package/components/oxy-number-keyboard/oxy-number-keyboard.vue +0 -151
  304. package/components/oxy-number-keyboard/types.ts +0 -83
  305. package/components/oxy-tree/components/tree-node-content.vue +0 -72
  306. package/components/oxy-tree/index.ts +0 -51
@@ -0,0 +1,821 @@
1
+ <template>
2
+ <view :class="rootClass" :style="customStyle">
3
+ <template v-if="viewMode === 'normal'">
4
+ <view class="oxy-voice-player__normal-mode-container">
5
+ <view
6
+ v-if="showCover"
7
+ class="oxy-voice-player__cover-section"
8
+ :class="{ 'is-hidden': isCoverHidden }"
9
+ @touchstart.stop="handleTouchStart"
10
+ @touchmove.stop="handleTouchMove"
11
+ @touchend.stop="handleTouchEnd"
12
+ >
13
+ <view class="oxy-voice-player__cover-container">
14
+ <view
15
+ class="oxy-voice-player__cover-disc"
16
+ :class="{ 'is-rotating': isPlaying && enableRotation, 'is-paused': !isPlaying && enableRotation }"
17
+ >
18
+ <image
19
+ class="oxy-voice-player__cover-image"
20
+ :src="currentSong.cover || computedDefaultCover"
21
+ mode="aspectFill"
22
+ @error="handleCoverError"
23
+ />
24
+ <view class="oxy-voice-player__cover-center"></view>
25
+ </view>
26
+ </view>
27
+ </view>
28
+
29
+ <view v-if="showInfo" class="oxy-voice-player__info" :class="{ 'is-hidden': isCoverHidden }">
30
+ <text class="oxy-voice-player__song-title">{{ currentSong.title || '未知歌曲' }}</text>
31
+ <text v-if="currentSong.artist" class="oxy-voice-player__song-artist">{{ currentSong.artist }}</text>
32
+ <text v-if="currentSong.album" class="oxy-voice-player__song-album">{{ currentSong.album }}</text>
33
+ </view>
34
+
35
+ <view
36
+ v-if="showLyrics && currentLyrics.length"
37
+ class="oxy-voice-player__lyrics"
38
+ :class="{ 'is-expanded': isCoverHidden }"
39
+ @touchstart.stop="handleTouchStart"
40
+ @touchmove.stop="handleTouchMove"
41
+ @touchend.stop="handleTouchEnd"
42
+ >
43
+ <scroll-view class="oxy-voice-player__lyrics-container" scroll-y :scroll-top="lyricsScrollTop" scroll-with-animation>
44
+ <view
45
+ v-for="(lyric, index) in currentLyrics"
46
+ :key="index"
47
+ class="oxy-voice-player__lyric-line"
48
+ :class="{ 'is-active': index === currentLyricIndex }"
49
+ >
50
+ <text class="oxy-voice-player__lyric-text">{{ lyric.text }}</text>
51
+ <text v-if="lyric.translation" class="oxy-voice-player__lyric-translation">{{ lyric.translation }}</text>
52
+ </view>
53
+ </scroll-view>
54
+ </view>
55
+
56
+ <view class="oxy-voice-player__progress-section">
57
+ <view class="oxy-voice-player__time-info">
58
+ <text class="oxy-voice-player__current-time">{{ formatTime(currentTime) }}</text>
59
+ <text class="oxy-voice-player__total-time">{{ formatTime(duration) }}</text>
60
+ </view>
61
+ <view class="oxy-voice-player__slider-wrapper">
62
+ <view class="oxy-voice-player__progress-buffered" :style="{ width: bufferedPercent + '%' }"></view>
63
+ <oxy-slider
64
+ :model-value="progressPercent"
65
+ :min="0"
66
+ :max="100"
67
+ :step="0.1"
68
+ hide-label
69
+ hide-min-max
70
+ inactive-color="#e5e5e533"
71
+ active-color="#e5e5e5"
72
+ @dragstart="handleDragstart"
73
+ @dragmove="handleDragmove"
74
+ @dragend="handleDragend"
75
+ />
76
+ </view>
77
+ </view>
78
+
79
+ <view class="oxy-voice-player__controls-section">
80
+ <view class="oxy-voice-player__main-controls">
81
+ <button v-if="showPrevNext" class="oxy-voice-player__control-btn" @click="handlePrev">
82
+ <oxy-icon name="chevron-left" custom-class="oxy-voice-player__control-icon" />
83
+ </button>
84
+ <button class="oxy-voice-player__control-btn oxy-voice-player--play-btn" @click="handleTogglePlay">
85
+ <oxy-icon :name="isPlaying ? 'pause' : 'play'" custom-class="oxy-voice-player__btn-icon" />
86
+ </button>
87
+ <button v-if="showPrevNext" class="oxy-voice-player__control-btn" @click="handleNext">
88
+ <oxy-icon name="chevron-right" custom-class="oxy-voice-player__control-icon" />
89
+ </button>
90
+ </view>
91
+
92
+ <view v-if="extraControls.length > 0" class="oxy-voice-player__extra-controls">
93
+ <button v-if="extraControls.includes('playMode')" class="oxy-voice-player__extra-btn" @click="handleTogglePlayMode">
94
+ <oxy-icon :name="playModeIcon" custom-class="oxy-voice-player__extra-btn-icon" />
95
+ </button>
96
+ <button
97
+ v-if="extraControls.includes('favorite')"
98
+ class="oxy-voice-player__extra-btn oxy-voice-player--favorite-btn"
99
+ @click="handleToggleFavorite"
100
+ >
101
+ <oxy-icon name="heart" custom-class="oxy-voice-player__extra-btn-icon" :class="{ 'is-favorited': isFavorited }" />
102
+ </button>
103
+ <button v-if="extraControls.includes('mini')" class="oxy-voice-player__extra-btn" @click="handleToggleViewMode">
104
+ <oxy-icon name="arrow-down" custom-class="oxy-voice-player__extra-btn-icon" />
105
+ </button>
106
+ <button v-if="extraControls.includes('volume')" class="oxy-voice-player__extra-btn" @click="showVolumePanel = !showVolumePanel">
107
+ <oxy-icon name="sound" custom-class="oxy-voice-player__extra-btn-icon" />
108
+ </button>
109
+ </view>
110
+ </view>
111
+
112
+ <view v-if="showVolumePanel" class="oxy-voice-player__volume-panel">
113
+ <view class="oxy-voice-player__volume-slider-wrapper">
114
+ <oxy-slider v-model="volumePercent" :min="0" :max="100" :step="1" hide-label hide-min-max />
115
+ </view>
116
+ <text class="oxy-voice-player__volume-text">{{ Math.round(volumePercent) }}%</text>
117
+ </view>
118
+ </view>
119
+ </template>
120
+
121
+ <template v-if="viewMode === 'mini'">
122
+ <view class="oxy-voice-player__mini-player">
123
+ <view class="oxy-voice-player__mini-cover">
124
+ <view
125
+ class="oxy-voice-player__mini-cover-disc"
126
+ :class="{ 'is-rotating': isPlaying && enableRotation, 'is-paused': !isPlaying && enableRotation }"
127
+ >
128
+ <image
129
+ class="oxy-voice-player__mini-cover-image"
130
+ :src="currentSong.cover || computedDefaultCover"
131
+ mode="aspectFill"
132
+ @error="handleCoverError"
133
+ />
134
+ <view class="oxy-voice-player__mini-cover-center"></view>
135
+ </view>
136
+ </view>
137
+
138
+ <view class="oxy-voice-player__mini-info">
139
+ <text class="oxy-voice-player__mini-title">{{ currentSong.title || '未知歌曲' }}</text>
140
+ <text v-if="currentSong.artist" class="oxy-voice-player__mini-artist">{{ currentSong.artist }}</text>
141
+ </view>
142
+
143
+ <view class="oxy-voice-player__mini-controls">
144
+ <button v-if="showPrevNext" class="oxy-voice-player__mini-control-btn" @click="handlePrev">
145
+ <oxy-icon name="chevron-left" custom-class="oxy-voice-player__mini-btn-icon" />
146
+ </button>
147
+ <button class="oxy-voice-player__mini-control-btn oxy-voice-player——play-btn" @click="handleTogglePlay">
148
+ <oxy-icon :name="isPlaying ? 'pause' : 'play'" custom-class="oxy-voice-player__mini-btn-icon" />
149
+ </button>
150
+ <button v-if="showPrevNext" class="oxy-voice-player__mini-control-btn" @click="handleNext">
151
+ <oxy-icon name="chevron-right" custom-class="oxy-voice-player__mini-btn-icon" />
152
+ </button>
153
+ <button class="oxy-voice-player__mini-control-btn" @click="handleToggleViewMode">
154
+ <oxy-icon name="arrow-up" custom-class="oxy-voice-player__mini-btn-icon" />
155
+ </button>
156
+ </view>
157
+
158
+ <view class="oxy-voice-player__mini-progress">
159
+ <view class="oxy-voice-player__mini-progress-bar" :style="{ width: progressPercent + '%' }"></view>
160
+ </view>
161
+ </view>
162
+ </template>
163
+
164
+ <template v-if="viewMode === 'simple'">
165
+ <view class="oxy-voice-player__simple-player">
166
+ <view class="oxy-voice-player__simple-time-info">
167
+ <text class="oxy-voice-player__simple-current-time">{{ formatTime(currentTime) }}</text>
168
+ <text class="oxy-voice-player__simple-total-time">{{ formatTime(duration) }}</text>
169
+ </view>
170
+ <view class="oxy-voice-player__simple-progress-section">
171
+ <view class="oxy-voice-player__simple-slider-wrapper">
172
+ <oxy-slider
173
+ v-model="progressPercent"
174
+ :min="0"
175
+ :max="100"
176
+ :step="0.1"
177
+ hide-label
178
+ hide-min-max
179
+ @dragstart="handleDragstart"
180
+ @dragmove="handleDragmove"
181
+ @dragend="handleDragend"
182
+ />
183
+ </view>
184
+ </view>
185
+ </view>
186
+ </template>
187
+
188
+ <template v-if="viewMode === 'hideView'"></template>
189
+ </view>
190
+ </template>
191
+
192
+ <script lang="ts">
193
+ export default {
194
+ name: 'oxy-voice-player',
195
+ options: {
196
+ addGlobalClass: true,
197
+ virtualHost: true,
198
+ styleIsolation: 'shared'
199
+ }
200
+ }
201
+ </script>
202
+
203
+ <script lang="ts" setup>
204
+ import OxyIcon from '../oxy-icon/oxy-icon.vue'
205
+ import { computed, onMounted, onUnmounted, ref, watch, nextTick } from 'vue'
206
+ import dayjs from '../../dayjs'
207
+ import {
208
+ voicePlayerProps,
209
+ type VoicePlayerEmits,
210
+ type VoicePlayerExpose,
211
+ type VoicePlayerSongInfo,
212
+ type VoicePlayerLyricLine,
213
+ type InnerAudioContext,
214
+ type TouchEvent,
215
+ type TouchPoint
216
+ } from './types'
217
+ const props = defineProps(voicePlayerProps)
218
+ const emit = defineEmits<VoicePlayerEmits>()
219
+
220
+ const isPlaying = ref(false)
221
+ const currentTime = ref(0)
222
+ const duration = ref(0)
223
+ const buffered = ref(0)
224
+ const currentVolume = ref(1)
225
+ const isSliderTouching = ref(false)
226
+
227
+ const internalCurrentIndex = ref(0)
228
+ const internalPlaylist = ref<VoicePlayerSongInfo[]>([])
229
+ const internalPlayMode = ref<'list' | 'single' | 'random'>('list')
230
+
231
+ const showVolumePanel = ref(false)
232
+ const currentLyricIndex = ref(-1)
233
+ const lyricsScrollTop = ref(0)
234
+ const isCoverHidden = ref(false)
235
+
236
+ const isFavorited = ref(false)
237
+
238
+ const audioContext = ref<InnerAudioContext | null>(null)
239
+ const progressTimer = ref<ReturnType<typeof setInterval> | null>(null)
240
+ const isChangingProgress = ref(false)
241
+ const isInitialized = ref(false)
242
+
243
+ const touchStartX = ref(0)
244
+ const touchStartY = ref(0)
245
+ const touchMoveX = ref(0)
246
+ const touchMoveY = ref(0)
247
+ const isTouching = ref(false)
248
+
249
+ function calculatePercent(current: number, total: number): number {
250
+ if (!total || total <= 0) return 0
251
+ return Math.min(100, Math.max(0, (current / total) * 100))
252
+ }
253
+
254
+ const progressPercent = computed(() => calculatePercent(currentTime.value, duration.value))
255
+
256
+ const bufferedPercent = computed(() => calculatePercent(buffered.value, duration.value))
257
+
258
+ const volumePercent = computed({
259
+ get: () => currentVolume.value * 100,
260
+ set: (value: number) => {
261
+ const vol = Math.min(1, Math.max(0, value / 100))
262
+ currentVolume.value = vol
263
+ if (audioContext.value) {
264
+ audioContext.value.volume = vol
265
+ }
266
+ emit('volume-change', vol)
267
+ }
268
+ })
269
+
270
+ const currentSong = computed((): VoicePlayerSongInfo => {
271
+ if (internalPlaylist.value.length > 0 && internalCurrentIndex.value >= 0 && internalCurrentIndex.value < internalPlaylist.value.length) {
272
+ return internalPlaylist.value[internalCurrentIndex.value]
273
+ }
274
+ return {
275
+ src: props.src,
276
+ title: props.title,
277
+ artist: props.artist,
278
+ album: props.album,
279
+ cover: props.cover,
280
+ lyrics: props.lyrics
281
+ }
282
+ })
283
+
284
+ const currentLyrics = computed((): VoicePlayerLyricLine[] => {
285
+ return currentSong.value.lyrics || props.lyrics || []
286
+ })
287
+
288
+ const rootClass = computed(() => {
289
+ const classes = ['oxy-voice-player', 'is-theme-' + props.theme, 'is-mode-' + props.viewMode, isPlaying.value ? 'is-playing' : '', props.customClass]
290
+ return classes.filter(Boolean).join(' ')
291
+ })
292
+
293
+ const playModeIcon = computed(() => {
294
+ switch (internalPlayMode.value) {
295
+ case 'single':
296
+ return 'refresh'
297
+ case 'random':
298
+ return 'swap'
299
+ default:
300
+ return 'refresh1'
301
+ }
302
+ })
303
+
304
+ const computedDefaultCover = computed(() => {
305
+ return props.defaultCover
306
+ })
307
+
308
+ watch(
309
+ () => props.playlist,
310
+ (newVal: VoicePlayerSongInfo[]) => {
311
+ internalPlaylist.value = Array.isArray(newVal) ? [...newVal] : []
312
+ validateCurrentIndex()
313
+ },
314
+ { immediate: true, deep: true }
315
+ )
316
+
317
+ watch(
318
+ () => props.playMode,
319
+ (newVal: 'list' | 'single' | 'random') => {
320
+ if (['list', 'single', 'random'].includes(newVal)) {
321
+ internalPlayMode.value = newVal
322
+ }
323
+ },
324
+ { immediate: true }
325
+ )
326
+
327
+ watch(
328
+ () => props.volume,
329
+ (newVal: string | number) => {
330
+ const normalizedVolume = typeof newVal === 'number' ? newVal : Number(newVal)
331
+ if (Number.isFinite(normalizedVolume) && normalizedVolume >= 0 && normalizedVolume <= 1) {
332
+ currentVolume.value = normalizedVolume
333
+ if (audioContext.value) {
334
+ audioContext.value.volume = normalizedVolume
335
+ }
336
+ }
337
+ },
338
+ { immediate: true }
339
+ )
340
+
341
+ watch(
342
+ () => props.src,
343
+ () => {
344
+ nextTick(() => {
345
+ initializeAudio()
346
+ if (isPlaying.value) {
347
+ play()
348
+ }
349
+ })
350
+ }
351
+ )
352
+
353
+ watch(currentTime, (newTime: number) => {
354
+ if (props.showLyrics) {
355
+ updateCurrentLyric(newTime)
356
+ }
357
+ })
358
+
359
+ watch(currentLyricIndex, () => {
360
+ if (props.showLyrics) {
361
+ scrollToCurrentLyric()
362
+ }
363
+ })
364
+
365
+ function validateCurrentIndex() {
366
+ if (internalPlaylist.value.length === 0) {
367
+ internalCurrentIndex.value = 0
368
+ return
369
+ }
370
+ if (internalCurrentIndex.value < 0) {
371
+ internalCurrentIndex.value = 0
372
+ } else if (internalCurrentIndex.value >= internalPlaylist.value.length) {
373
+ internalCurrentIndex.value = internalPlaylist.value.length - 1
374
+ }
375
+ }
376
+
377
+ function getBufferedTime(context: InnerAudioContext): number {
378
+ const bufferedValue = (context as InnerAudioContext & { buffered?: unknown }).buffered
379
+ if (typeof bufferedValue === 'number') {
380
+ return bufferedValue
381
+ }
382
+ if (bufferedValue && typeof bufferedValue === 'object') {
383
+ const timeRangeLike = bufferedValue as { length?: number; end?: (index: number) => number }
384
+ if (typeof timeRangeLike.length === 'number' && timeRangeLike.length > 0 && typeof timeRangeLike.end === 'function') {
385
+ const lastBufferedTime = timeRangeLike.end(timeRangeLike.length - 1)
386
+ return Number.isFinite(lastBufferedTime) ? lastBufferedTime : 0
387
+ }
388
+ }
389
+ return 0
390
+ }
391
+
392
+ function initializeAudio() {
393
+ try {
394
+ cleanup()
395
+ currentTime.value = 0
396
+
397
+ try {
398
+ audioContext.value = uni.createInnerAudioContext() as unknown as InnerAudioContext
399
+ } catch (e) {
400
+ console.warn('音频上下文创建失败,尝试备用方案:', e)
401
+ }
402
+
403
+ if (!audioContext.value) {
404
+ throw new Error('无法创建音频上下文')
405
+ }
406
+
407
+ audioContext.value.src = currentSong.value.src || props.src
408
+ audioContext.value.autoPlay = false
409
+ audioContext.value.loop = false
410
+ audioContext.value.volume = currentVolume.value
411
+
412
+ audioContext.value.onCanplay(() => {
413
+ const context = audioContext.value
414
+ if (!context) return
415
+ duration.value = context.duration || 0
416
+ emit('canplay', {
417
+ duration: duration.value
418
+ })
419
+ })
420
+
421
+ audioContext.value.onPlay(() => {
422
+ isPlaying.value = true
423
+ startProgressTimer()
424
+ emit('play')
425
+ })
426
+
427
+ audioContext.value.onPause(() => {
428
+ isPlaying.value = false
429
+ stopProgressTimer()
430
+ emit('pause')
431
+ })
432
+
433
+ audioContext.value.onStop(() => {
434
+ isPlaying.value = false
435
+ stopProgressTimer()
436
+ emit('stop')
437
+ })
438
+
439
+ audioContext.value.onEnded(() => {
440
+ isPlaying.value = false
441
+ stopProgressTimer()
442
+ emit('ended')
443
+ handleAutoNext()
444
+ })
445
+
446
+ audioContext.value.onTimeUpdate(() => {
447
+ onTimeUpdate()
448
+ })
449
+
450
+ audioContext.value.onError((error: { errMsg: string; errCode: number }) => {
451
+ handleError('音频播放错误', error)
452
+ emit('error', { message: '音频播放错误', error })
453
+ })
454
+
455
+ isInitialized.value = true
456
+ } catch (error) {
457
+ handleError('初始化音频失败', error)
458
+ }
459
+ }
460
+ const onTimeUpdate = () => {
461
+ const context = audioContext.value
462
+ if (!context || isChangingProgress.value) return
463
+
464
+ currentTime.value = context.currentTime || 0
465
+ duration.value = context.duration || 0
466
+
467
+ // #ifdef H5 || APP
468
+ try {
469
+ buffered.value = getBufferedTime(context)
470
+ } catch (e) {
471
+ buffered.value = 0
472
+ }
473
+ // #endif
474
+
475
+ emit('timeupdate', {
476
+ currentTime: currentTime.value,
477
+ duration: duration.value
478
+ })
479
+ }
480
+ function play() {
481
+ if (!audioContext.value) {
482
+ initializeAudio()
483
+ }
484
+ if (audioContext.value) {
485
+ try {
486
+ audioContext.value.play()
487
+ } catch (error) {
488
+ handleError('播放失败', error)
489
+ }
490
+ }
491
+ }
492
+
493
+ function pause() {
494
+ if (audioContext.value) {
495
+ try {
496
+ audioContext.value.pause()
497
+ } catch (error) {
498
+ handleError('暂停失败', error)
499
+ }
500
+ }
501
+ }
502
+
503
+ function stop() {
504
+ if (audioContext.value) {
505
+ try {
506
+ audioContext.value.stop()
507
+ currentTime.value = 0
508
+ } catch (error) {
509
+ handleError('停止失败', error)
510
+ }
511
+ }
512
+ }
513
+
514
+ function seek(time: number) {
515
+ if (audioContext.value && time >= 0 && time <= duration.value) {
516
+ try {
517
+ audioContext.value.seek(time)
518
+
519
+ // #ifdef mp-weixin
520
+ if (!isPlaying.value) {
521
+ onTimeUpdate()
522
+ }
523
+ // #endif
524
+ // currentTime.value = time
525
+ } catch (error) {
526
+ handleError('跳转失败', error)
527
+ }
528
+ }
529
+ }
530
+
531
+ function setVolume(volume: number) {
532
+ const vol = Math.min(1, Math.max(0, volume))
533
+ currentVolume.value = vol
534
+ if (audioContext.value) {
535
+ audioContext.value.volume = vol
536
+ }
537
+ emit('volume-change', vol)
538
+ }
539
+
540
+ function handleTogglePlay() {
541
+ if (isPlaying.value) {
542
+ pause()
543
+ } else {
544
+ play()
545
+ }
546
+ }
547
+
548
+ function handlePrev() {
549
+ switchSong(
550
+ internalPlaylist.value.length === 0 ? -1 : internalCurrentIndex.value > 0 ? internalCurrentIndex.value - 1 : internalPlaylist.value.length - 1,
551
+ 'prev'
552
+ )
553
+ }
554
+
555
+ function handleNext() {
556
+ switchSong(internalPlaylist.value.length === 0 ? -1 : (internalCurrentIndex.value + 1) % internalPlaylist.value.length, 'next')
557
+ }
558
+
559
+ function switchSong(index: number, eventType?: string) {
560
+ if (index < 0 || index >= internalPlaylist.value.length) {
561
+ if (eventType) {
562
+ emit(eventType as any, null)
563
+ }
564
+ return
565
+ }
566
+
567
+ const wasPlaying = isPlaying.value
568
+ internalCurrentIndex.value = index
569
+ currentTime.value = 0
570
+ duration.value = 0
571
+
572
+ const songChangeEvent = { index, song: internalPlaylist.value[index] }
573
+
574
+ if (eventType) {
575
+ emit(eventType as any, songChangeEvent)
576
+ }
577
+ emit('song-change', songChangeEvent)
578
+
579
+ nextTick(() => {
580
+ initializeAudio()
581
+ if (wasPlaying) {
582
+ play()
583
+ }
584
+ })
585
+ }
586
+
587
+ function handleAutoNext() {
588
+ switch (internalPlayMode.value) {
589
+ case 'single':
590
+ seek(0)
591
+ play()
592
+ break
593
+ case 'random':
594
+ if (internalPlaylist.value.length > 1) {
595
+ let randomIndex = Math.floor(Math.random() * internalPlaylist.value.length)
596
+ while (randomIndex === internalCurrentIndex.value) {
597
+ randomIndex = Math.floor(Math.random() * internalPlaylist.value.length)
598
+ }
599
+ switchSong(randomIndex)
600
+ } else {
601
+ seek(0)
602
+ play()
603
+ }
604
+ break
605
+ default:
606
+ handleNext()
607
+ }
608
+ }
609
+
610
+ function handleTogglePlayMode() {
611
+ const modes: Array<'list' | 'single' | 'random'> = ['list', 'single', 'random']
612
+ const currentIndex = modes.indexOf(internalPlayMode.value)
613
+ const nextMode = modes[(currentIndex + 1) % modes.length]
614
+ internalPlayMode.value = nextMode
615
+ emit('play-mode-change', nextMode)
616
+ }
617
+
618
+ function handleToggleFavorite() {
619
+ isFavorited.value = !isFavorited.value
620
+ emit('favorite-change', isFavorited.value, currentSong.value)
621
+ }
622
+
623
+ function handleToggleViewMode() {
624
+ const newMode = props.viewMode === 'normal' ? 'mini' : 'normal'
625
+ emit('mode-change', newMode)
626
+ }
627
+
628
+ function handleDragstart() {
629
+ isSliderTouching.value = true
630
+ isChangingProgress.value = true
631
+ }
632
+
633
+ function handleDragmove(event: { value: number | number[] }) {
634
+ const value = event.value as number
635
+ currentTime.value = (value / 100) * duration.value
636
+ }
637
+
638
+ function handleDragend(event: { value: number | number[] }) {
639
+ isSliderTouching.value = false
640
+ isChangingProgress.value = false
641
+ const value = event.value as number
642
+ const time = Number(((value / 100) * duration.value).toFixed(1))
643
+ seek(time)
644
+ }
645
+
646
+ function getTouchPoint(e: TouchEvent): TouchPoint | null {
647
+ const touch = (
648
+ e.touches && e.touches.length > 0 ? e.touches[0] : e.changedTouches && e.changedTouches.length > 0 ? e.changedTouches[0] : null
649
+ ) as TouchPoint | null
650
+ return touch
651
+ }
652
+
653
+ function getTouchCoordinate(touch: TouchPoint | null, axis: 'x' | 'y'): number {
654
+ if (!touch) return 0
655
+ if (axis === 'x') {
656
+ return touch.clientX || touch.pageX || touch.x || 0
657
+ }
658
+ return touch.clientY || touch.pageY || touch.y || 0
659
+ }
660
+
661
+ function handleTouchStart(e: TouchEvent) {
662
+ if (props.viewMode === 'mini' || props.viewMode === 'hideView') return
663
+ isTouching.value = true
664
+
665
+ const touch = getTouchPoint(e)
666
+ if (touch) {
667
+ touchStartX.value = getTouchCoordinate(touch, 'x')
668
+ touchStartY.value = getTouchCoordinate(touch, 'y')
669
+ touchMoveX.value = touchStartX.value
670
+ touchMoveY.value = touchStartY.value
671
+ }
672
+ }
673
+
674
+ function handleTouchMove(e: TouchEvent) {
675
+ if (!isTouching.value || props.viewMode === 'mini' || props.viewMode === 'hideView') return
676
+
677
+ const touch = getTouchPoint(e)
678
+ if (touch) {
679
+ touchMoveX.value = getTouchCoordinate(touch, 'x')
680
+ touchMoveY.value = getTouchCoordinate(touch, 'y')
681
+ }
682
+ }
683
+
684
+ function handleTouchEnd(e: TouchEvent) {
685
+ if (!isTouching.value || props.viewMode === 'mini' || props.viewMode === 'hideView') {
686
+ isTouching.value = false
687
+ return
688
+ }
689
+
690
+ const touch = getTouchPoint(e)
691
+ if (!touch) {
692
+ isTouching.value = false
693
+ return
694
+ }
695
+
696
+ const endX = getTouchCoordinate(touch, 'x')
697
+ const endY = getTouchCoordinate(touch, 'y')
698
+
699
+ const deltaX = endX - touchStartX.value
700
+ const deltaY = Math.abs(endY - touchStartY.value)
701
+ const minSwipeDistance = 80
702
+
703
+ if (Math.abs(deltaX) > deltaY * 1.5 && Math.abs(deltaX) > minSwipeDistance) {
704
+ if (deltaX < 0 && !isCoverHidden.value) {
705
+ isCoverHidden.value = true
706
+ } else if (deltaX > 0 && isCoverHidden.value) {
707
+ isCoverHidden.value = false
708
+ }
709
+ }
710
+
711
+ isTouching.value = false
712
+ touchStartX.value = 0
713
+ touchStartY.value = 0
714
+ touchMoveX.value = 0
715
+ touchMoveY.value = 0
716
+ }
717
+
718
+ function handleCoverError() {
719
+ isCoverHidden.value = false
720
+ }
721
+
722
+ function updateCurrentLyric(time: number) {
723
+ if (!currentLyrics.value.length) return
724
+
725
+ for (let i = currentLyrics.value.length - 1; i >= 0; i--) {
726
+ if (time >= currentLyrics.value[i].time) {
727
+ if (currentLyricIndex.value !== i) {
728
+ currentLyricIndex.value = i
729
+ }
730
+ break
731
+ }
732
+ }
733
+ }
734
+
735
+ function scrollToCurrentLyric() {
736
+ if (currentLyricIndex.value >= 0) {
737
+ lyricsScrollTop.value = currentLyricIndex.value * 60
738
+ }
739
+ }
740
+
741
+ function formatTime(time: number): string {
742
+ if (isNaN(time) || time < 0) return '00:00'
743
+ return dayjs(time * 1000).format('mm:ss')
744
+ }
745
+
746
+ function startProgressTimer() {
747
+ stopProgressTimer()
748
+ progressTimer.value = setInterval(() => {
749
+ const context = audioContext.value
750
+ if (context && isPlaying.value && !isChangingProgress.value) {
751
+ currentTime.value = context.currentTime || 0
752
+ duration.value = context.duration || 0
753
+ // #ifdef H5 || APP
754
+ try {
755
+ buffered.value = getBufferedTime(context)
756
+ } catch (e) {
757
+ buffered.value = 0
758
+ }
759
+ // #endif
760
+ }
761
+ }, 200)
762
+ }
763
+
764
+ function stopProgressTimer() {
765
+ if (progressTimer.value) {
766
+ clearInterval(progressTimer.value)
767
+ progressTimer.value = null
768
+ }
769
+ }
770
+
771
+ function handleError(message: string, error: any) {
772
+ console.error(`[oxy-voice-player] ${message}:`, error)
773
+ emit('error', { message, error })
774
+ }
775
+
776
+ function cleanup() {
777
+ try {
778
+ stopProgressTimer()
779
+ if (audioContext.value) {
780
+ audioContext.value.destroy()
781
+ audioContext.value = null
782
+ }
783
+ isInitialized.value = false
784
+ } catch (error) {
785
+ console.error('清理资源失败:', error)
786
+ }
787
+ }
788
+
789
+ onMounted(() => {
790
+ if (props.src || (props.playlist && props.playlist.length > 0)) {
791
+ initializeAudio()
792
+ if (isPlaying.value) {
793
+ play()
794
+ }
795
+ }
796
+ })
797
+
798
+ onUnmounted(() => {
799
+ cleanup()
800
+ })
801
+
802
+ defineExpose<VoicePlayerExpose>({
803
+ play,
804
+ pause,
805
+ stop,
806
+ seek,
807
+ setVolume,
808
+ prev: handlePrev,
809
+ next: handleNext,
810
+ switchToSong: switchSong,
811
+ getPlayState: () => ({
812
+ isPlaying: isPlaying.value,
813
+ currentTime: currentTime.value,
814
+ duration: duration.value,
815
+ currentSong: currentSong.value
816
+ })
817
+ })
818
+ </script>
819
+ <style lang="scss" scoped>
820
+ @import './index.scss';
821
+ </style>