oxy-uni-ui 1.2.3 → 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 (235) hide show
  1. package/attributes.json +1 -1
  2. package/components/common/abstracts/variable.scss +353 -328
  3. package/components/common/util.ts +185 -32
  4. package/components/composables/index.ts +1 -0
  5. package/components/composables/usePopover.ts +24 -20
  6. package/components/composables/useVirtualScroll.ts +10 -9
  7. package/components/composables/useWindowResize.ts +35 -0
  8. package/components/oxy-action-sheet/index.scss +24 -11
  9. package/components/oxy-action-sheet/oxy-action-sheet.vue +27 -19
  10. package/components/oxy-action-sheet/types.ts +7 -0
  11. package/components/oxy-backtop/index.scss +3 -3
  12. package/components/oxy-backtop/oxy-backtop.vue +9 -6
  13. package/components/oxy-backtop/types.ts +7 -7
  14. package/components/oxy-badge/index.scss +4 -4
  15. package/components/oxy-badge/oxy-badge.vue +3 -3
  16. package/components/oxy-badge/types.ts +2 -2
  17. package/components/oxy-button/index.scss +5 -5
  18. package/components/oxy-button/oxy-button.vue +5 -1
  19. package/components/oxy-calendar/index.scss +11 -11
  20. package/components/oxy-calendar/oxy-calendar.vue +1 -0
  21. package/components/oxy-calendar/types.ts +5 -0
  22. package/components/oxy-calendar-view/month/index.scss +4 -4
  23. package/components/oxy-calendar-view/month/types.ts +36 -0
  24. package/components/oxy-calendar-view/monthPanel/index.scss +7 -7
  25. package/components/oxy-calendar-view/monthPanel/month-panel.vue +14 -8
  26. package/components/oxy-calendar-view/year/index.scss +4 -4
  27. package/components/oxy-calendar-view/yearPanel/index.scss +4 -4
  28. package/components/oxy-calendar-view/yearPanel/year-panel.vue +21 -5
  29. package/components/oxy-card/index.scss +2 -2
  30. package/components/oxy-cell/index.scss +8 -8
  31. package/components/oxy-checkbox/index.scss +7 -7
  32. package/components/oxy-checkbox-group/index.scss +2 -2
  33. package/components/oxy-circle/oxy-circle.vue +10 -7
  34. package/components/oxy-circle/types.ts +5 -5
  35. package/components/oxy-col/oxy-col.vue +2 -2
  36. package/components/oxy-col-picker/index.scss +4 -4
  37. package/components/oxy-col-picker/oxy-col-picker.vue +6 -5
  38. package/components/oxy-col-picker/types.ts +7 -2
  39. package/components/oxy-collapse/index.scss +2 -2
  40. package/components/oxy-collapse-item/oxy-collapse-item.vue +3 -3
  41. package/components/oxy-corner/index.scss +32 -32
  42. package/components/oxy-count-to/oxy-count-to.vue +3 -3
  43. package/components/oxy-curtain/index.scss +15 -15
  44. package/components/oxy-curtain/oxy-curtain.vue +4 -2
  45. package/components/oxy-curtain/types.ts +6 -1
  46. package/components/oxy-date-strip/oxy-date-strip.vue +2 -2
  47. package/components/oxy-date-strip/types.ts +1 -1
  48. package/components/oxy-date-strip-item/index.scss +3 -3
  49. package/components/oxy-datetime-picker/index.scss +11 -11
  50. package/components/oxy-datetime-picker/oxy-datetime-picker.vue +1 -0
  51. package/components/oxy-datetime-picker/types.ts +5 -0
  52. package/components/oxy-drop-menu/index.scss +3 -3
  53. package/components/oxy-drop-menu/oxy-drop-menu.vue +3 -3
  54. package/components/oxy-drop-menu-item/index.scss +1 -1
  55. package/components/oxy-drop-menu-item/oxy-drop-menu-item.vue +4 -3
  56. package/components/oxy-drop-menu-item/types.ts +5 -0
  57. package/components/oxy-echarts/types.ts +6 -0
  58. package/components/oxy-fab/index.scss +8 -8
  59. package/components/oxy-fab/oxy-fab.vue +22 -3
  60. package/components/oxy-file-list/index.scss +24 -23
  61. package/components/oxy-file-list/oxy-file-list.vue +2 -2
  62. package/components/oxy-floating-panel/oxy-floating-panel.vue +13 -9
  63. package/components/oxy-floating-panel/{type.ts → types.ts} +8 -8
  64. package/components/oxy-footer/index.scss +19 -0
  65. package/components/oxy-footer/oxy-footer.vue +78 -0
  66. package/components/oxy-footer/types.ts +17 -0
  67. package/components/oxy-form-item/types.ts +22 -1
  68. package/components/oxy-gap/oxy-gap.vue +2 -2
  69. package/components/oxy-gap/types.ts +2 -2
  70. package/components/oxy-grid/oxy-grid.vue +1 -1
  71. package/components/oxy-grid/types.ts +1 -1
  72. package/components/oxy-grid-item/index.scss +1 -1
  73. package/components/oxy-grid-item/oxy-grid-item.vue +7 -5
  74. package/components/oxy-grid-item/types.ts +1 -1
  75. package/components/oxy-guidance/index.scss +75 -0
  76. package/components/oxy-guidance/oxy-guidance.vue +201 -0
  77. package/components/oxy-guidance/types.ts +33 -0
  78. package/components/oxy-icon/oxy-icon.vue +2 -2
  79. package/components/oxy-icon/types.ts +1 -1
  80. package/components/oxy-img/oxy-img.vue +4 -4
  81. package/components/oxy-img/types.ts +3 -3
  82. package/components/oxy-img-cropper/index.scss +12 -12
  83. package/components/oxy-img-cropper/oxy-img-cropper.vue +97 -52
  84. package/components/oxy-img-cropper/types.ts +2 -2
  85. package/components/oxy-img-lazy/oxy-img-lazy.vue +3 -3
  86. package/components/oxy-img-lazy/types.ts +3 -3
  87. package/components/oxy-index-anchor/index.scss +2 -2
  88. package/components/oxy-index-anchor/oxy-index-anchor.vue +2 -2
  89. package/components/oxy-index-anchor/{type.ts → types.ts} +3 -0
  90. package/components/oxy-index-bar/index.scss +3 -3
  91. package/components/oxy-index-bar/oxy-index-bar.vue +3 -3
  92. package/components/oxy-index-bar/{type.ts → types.ts} +2 -2
  93. package/components/oxy-input/index.scss +1 -1
  94. package/components/oxy-input-number/index.scss +5 -5
  95. package/components/oxy-input-number/oxy-input-number.vue +2 -2
  96. package/components/oxy-input-number/types.ts +3 -2
  97. package/components/oxy-keyboard/index.scss +5 -5
  98. package/components/oxy-keyboard/key/index.scss +3 -3
  99. package/components/oxy-keyboard/key/index.vue +2 -2
  100. package/components/oxy-keyboard/key/types.ts +15 -0
  101. package/components/oxy-keyboard/oxy-keyboard.vue +1 -0
  102. package/components/oxy-keyboard/types.ts +5 -0
  103. package/components/oxy-link/index.scss +2 -2
  104. package/components/oxy-list/oxy-list.vue +4 -3
  105. package/components/oxy-loading/oxy-loading.vue +8 -4
  106. package/components/oxy-loading/types.ts +1 -1
  107. package/components/oxy-loadmore/index.scss +3 -3
  108. package/components/oxy-long-press-menu/index.scss +93 -0
  109. package/components/oxy-long-press-menu/oxy-long-press-menu.vue +338 -0
  110. package/components/oxy-long-press-menu/types.ts +34 -0
  111. package/components/oxy-message-box/index.scss +12 -11
  112. package/components/oxy-message-box/oxy-message-box.vue +11 -3
  113. package/components/oxy-message-box/types.ts +14 -0
  114. package/components/oxy-navbar/index.scss +2 -2
  115. package/components/oxy-navbar/oxy-navbar.vue +58 -13
  116. package/components/oxy-navbar/types.ts +8 -1
  117. package/components/oxy-navbar-capsule/types.ts +3 -0
  118. package/components/oxy-notice-bar/index.scss +3 -3
  119. package/components/oxy-notice-bar/oxy-notice-bar.vue +9 -5
  120. package/components/oxy-notice-bar/types.ts +3 -3
  121. package/components/oxy-notify/index.ts +1 -0
  122. package/components/oxy-notify/oxy-notify.vue +3 -2
  123. package/components/oxy-notify/types.ts +7 -0
  124. package/components/oxy-pagination/index.scss +1 -1
  125. package/components/oxy-password-input/oxy-password-input.vue +2 -2
  126. package/components/oxy-password-input/types.ts +1 -1
  127. package/components/oxy-picker/index.scss +45 -2
  128. package/components/oxy-picker/oxy-picker.vue +100 -14
  129. package/components/oxy-picker/types.ts +29 -1
  130. package/components/oxy-picker-view/index.scss +3 -3
  131. package/components/oxy-picker-view/oxy-picker-view.vue +4 -4
  132. package/components/oxy-popover/index.scss +9 -9
  133. package/components/oxy-popup/index.scss +2 -2
  134. package/components/oxy-popup/oxy-popup.vue +35 -2
  135. package/components/oxy-popup/types.ts +8 -1
  136. package/components/oxy-progress/index.scss +3 -3
  137. package/components/oxy-qrcode/draw.ts +398 -0
  138. package/components/oxy-qrcode/index.scss +2 -0
  139. package/components/oxy-qrcode/oxy-qrcode.vue +124 -0
  140. package/components/oxy-qrcode/qrcode.ts +936 -0
  141. package/components/oxy-qrcode/types.ts +42 -0
  142. package/components/oxy-radio/index.scss +10 -10
  143. package/components/oxy-radio-group/index.scss +2 -2
  144. package/components/oxy-rate/types.ts +4 -4
  145. package/components/oxy-resize/index.scss +2 -2
  146. package/components/oxy-resize/oxy-resize.vue +4 -4
  147. package/components/oxy-resize/types.ts +3 -0
  148. package/components/oxy-rich-text/index.scss +30 -29
  149. package/components/oxy-rich-text/mp-html/mp-html.vue +33 -24
  150. package/components/oxy-rich-text/mp-html/node/node.vue +30 -19
  151. package/components/oxy-rich-text/oxy-rich-text.vue +31 -31
  152. package/components/oxy-rich-text/types.ts +6 -1
  153. package/components/oxy-row/oxy-row.vue +3 -3
  154. package/components/oxy-row/types.ts +1 -1
  155. package/components/oxy-search/index.scss +3 -3
  156. package/components/oxy-segmented/index.scss +16 -16
  157. package/components/oxy-segmented/oxy-segmented.vue +23 -3
  158. package/components/oxy-select/index.scss +144 -68
  159. package/components/oxy-select/oxy-select.vue +85 -50
  160. package/components/oxy-select/types.ts +13 -1
  161. package/components/oxy-select-picker/index.scss +7 -7
  162. package/components/oxy-select-picker/oxy-select-picker.vue +1 -0
  163. package/components/oxy-select-picker/types.ts +2 -0
  164. package/components/oxy-sidebar-item/index.scss +1 -1
  165. package/components/oxy-signature/oxy-signature.vue +18 -10
  166. package/components/oxy-signature/types.ts +106 -13
  167. package/components/oxy-skeleton/oxy-skeleton.vue +6 -6
  168. package/components/oxy-skeleton/types.ts +1 -1
  169. package/components/oxy-slider/index.scss +3 -3
  170. package/components/oxy-sort-button/index.scss +8 -8
  171. package/components/oxy-status-tip/index.scss +4 -4
  172. package/components/oxy-status-tip/oxy-status-tip.vue +5 -5
  173. package/components/oxy-status-tip/types.ts +3 -3
  174. package/components/oxy-step/index.scss +14 -14
  175. package/components/oxy-sticky/oxy-sticky.vue +6 -6
  176. package/components/oxy-stream-render/types.ts +4 -1
  177. package/components/oxy-swipe-action/oxy-swipe-action.vue +27 -2
  178. package/components/oxy-swiper/oxy-swiper.vue +6 -6
  179. package/components/oxy-swiper/types.ts +5 -5
  180. package/components/oxy-switch/index.scss +8 -8
  181. package/components/oxy-switch/oxy-switch.vue +2 -2
  182. package/components/oxy-switch/types.ts +1 -1
  183. package/components/oxy-tab/index.scss +11 -1
  184. package/components/oxy-tabbar/index.scss +1 -1
  185. package/components/oxy-tabbar/oxy-tabbar.vue +39 -10
  186. package/components/oxy-table/index.scss +5 -5
  187. package/components/oxy-table/oxy-table.vue +8 -6
  188. package/components/oxy-table/types.ts +2 -2
  189. package/components/oxy-table-col/oxy-table-col.vue +3 -3
  190. package/components/oxy-table-col/types.ts +2 -2
  191. package/components/oxy-tabs/index.scss +43 -15
  192. package/components/oxy-tabs/oxy-tabs.vue +53 -19
  193. package/components/oxy-tabs/types.ts +15 -3
  194. package/components/oxy-tag/index.scss +15 -15
  195. package/components/oxy-text/index.scss +5 -1
  196. package/components/oxy-text/oxy-text.vue +76 -7
  197. package/components/oxy-text/types.ts +12 -0
  198. package/components/oxy-textarea/index.scss +6 -6
  199. package/components/oxy-toast/oxy-toast.vue +24 -8
  200. package/components/oxy-tooltip/index.scss +9 -9
  201. package/components/oxy-tree/index.scss +51 -15
  202. package/components/oxy-tree/oxy-tree.vue +13 -9
  203. package/components/oxy-tree/types.ts +12 -9
  204. package/components/oxy-upload/index.scss +21 -21
  205. package/components/oxy-upload/types.ts +2 -2
  206. package/components/oxy-verification-code/index.scss +6 -0
  207. package/components/oxy-verification-code/oxy-verification-code.vue +187 -0
  208. package/components/oxy-verification-code/types.ts +82 -0
  209. package/components/oxy-video-preview/index.scss +4 -4
  210. package/components/oxy-virtual-scroll/index.scss +4 -4
  211. package/components/oxy-virtual-scroll/oxy-virtual-scroll.vue +11 -7
  212. package/components/oxy-virtual-scroll/types.ts +14 -14
  213. package/components/oxy-voice-player/index.scss +908 -0
  214. package/components/oxy-voice-player/oxy-voice-player.vue +821 -0
  215. package/components/oxy-voice-player/types.ts +567 -0
  216. package/components/oxy-waterfall/oxy-waterfall.vue +6 -6
  217. package/components/oxy-waterfall/types.ts +6 -6
  218. package/components/oxy-watermark/oxy-watermark.vue +35 -13
  219. package/components/oxy-watermark/types.ts +14 -14
  220. package/global.d.ts +2 -0
  221. package/locale/lang/ar-SA.ts +3 -0
  222. package/locale/lang/en-US.ts +3 -0
  223. package/locale/lang/zh-CN.ts +3 -0
  224. package/package.json +97 -1
  225. package/tags.json +1 -1
  226. package/web-types.json +1 -1
  227. package/components/oxy-number-keyboard/index.scss +0 -78
  228. package/components/oxy-number-keyboard/key/index.scss +0 -81
  229. package/components/oxy-number-keyboard/key/index.vue +0 -78
  230. package/components/oxy-number-keyboard/key/types.ts +0 -11
  231. package/components/oxy-number-keyboard/oxy-number-keyboard.vue +0 -151
  232. package/components/oxy-number-keyboard/types.ts +0 -83
  233. package/components/oxy-tree/components/tree-node-content.vue +0 -72
  234. package/components/oxy-tree/index.ts +0 -51
  235. package/oxy-uni-ui.zip +0 -0
@@ -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>