PageMusic.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. import { IconMusic } from "@/assets/icons";
  2. import { isWeixinBrowser } from "@/controllers/wxController";
  3. import { useEditor } from "@/modules/editor";
  4. import {
  5. PauseCircleOutlined,
  6. PlayCircleOutlined,
  7. SoundOutlined,
  8. } from "@ant-design/icons-vue";
  9. import { css } from "@linaria/core";
  10. import { Button, Dropdown, Slider } from "ant-design-vue";
  11. import { Howl } from "howler";
  12. import { nanoid } from "nanoid";
  13. import { isPc } from "@queenjs/utils";
  14. import {
  15. defineComponent,
  16. reactive,
  17. ref,
  18. watch,
  19. onUnmounted,
  20. onMounted,
  21. } from "vue";
  22. import { bool, number } from "vue-types";
  23. import { MusicOptions } from "./localMusic";
  24. declare const WeixinJSBridge: any;
  25. export const PageMusic = defineComponent({
  26. setup() {
  27. const { store, helper, controls, actions } = useEditor();
  28. const rootComp = helper.findRootComp();
  29. const volume =
  30. rootComp?.value.volume != undefined ? rootComp?.value.volume : 0.3;
  31. const state = reactive({
  32. playStatus: false,
  33. duration: 0,
  34. currentTime: 0,
  35. muted: true,
  36. volume: volume,
  37. });
  38. let audioKey = nanoid();
  39. let audioBgm = ref();
  40. const initAudioBgm = () => {
  41. audioBgm.value = null;
  42. const curAudio = MusicOptions.find((e) => {
  43. return e.value == rootComp?.value.music;
  44. });
  45. const src = curAudio?.src || "";
  46. audioBgm.value = new Howl({
  47. src: [src],
  48. loop: store.isEditMode ? false : true,
  49. preload: true,
  50. HTML5: true,
  51. volume: volume,
  52. });
  53. controls.mediaCtrl.setMediasInstance(audioKey, audioBgm.value);
  54. audioBgm.value.on("load", () => {
  55. state.duration = audioBgm.value.duration();
  56. if (!store.isEditMode) {
  57. if (isWeixinBrowser()) {
  58. WeixinJSBridge.invoke(
  59. "getNetworkType",
  60. {},
  61. () => {
  62. playAudio(true);
  63. },
  64. false
  65. );
  66. } else {
  67. playAudio(true);
  68. }
  69. }
  70. });
  71. audioBgm.value.on("loaderror", () => {
  72. console.log("音频加载失败");
  73. });
  74. audioBgm.value.on("playerror", () => {
  75. console.log("音频播放失败");
  76. });
  77. audioBgm.value.on("play", () => {
  78. controls.mediaCtrl.pauseOtherMedia(audioKey);
  79. if (!state.playStatus) {
  80. state.playStatus = true;
  81. }
  82. playStep();
  83. });
  84. audioBgm.value.on("pause", () => {
  85. audioRest();
  86. });
  87. audioBgm.value.on("end", () => {
  88. if (store.isEditMode) {
  89. audioRest();
  90. }
  91. });
  92. setTimeout(() => {
  93. checkAutoPlay();
  94. }, 500);
  95. };
  96. const checkAutoPlay = () => {
  97. const duration = audioBgm.value.duration();
  98. if (duration) {
  99. state.duration = duration;
  100. }
  101. if (!audioBgm.value || store.isEditMode) {
  102. return;
  103. }
  104. let playing = audioBgm.value.playing();
  105. if (!playing) {
  106. playAudio(true);
  107. }
  108. };
  109. const playStep = () => {
  110. if (!audioBgm.value) {
  111. return;
  112. }
  113. let playing = audioBgm.value.playing();
  114. if (!playing) {
  115. return;
  116. }
  117. let seek = audioBgm.value.seek();
  118. state.currentTime = seek;
  119. requestAnimationFrame(playStep);
  120. };
  121. const playAudio = async (status: boolean) => {
  122. if (!audioBgm.value) {
  123. return;
  124. }
  125. if (status) {
  126. audioBgm.value.play();
  127. } else {
  128. audioRest();
  129. }
  130. let playing = audioBgm.value.playing();
  131. if (status && playing) {
  132. state.playStatus = true;
  133. return;
  134. }
  135. state.playStatus = false;
  136. };
  137. watch(
  138. () => rootComp?.value.music,
  139. () => {
  140. audioRest();
  141. initAudioBgm();
  142. }
  143. );
  144. onMounted(() => {
  145. initAudioBgm();
  146. });
  147. onUnmounted(() => {
  148. audioRest();
  149. audioBgm.value = null;
  150. controls.mediaCtrl.removeMedia(audioKey);
  151. });
  152. const seekChange = (v: number) => {
  153. state.currentTime = v;
  154. audioBgm.value && audioBgm.value.seek(v);
  155. };
  156. const volumeChange = (v: number) => {
  157. state.volume = v;
  158. if (rootComp) {
  159. actions.updateCompData(rootComp, "value.volume", v);
  160. }
  161. audioBgm.value && audioBgm.value.volume(v);
  162. };
  163. const audioRest = () => {
  164. if (!audioBgm.value) {
  165. return;
  166. }
  167. let playing = audioBgm.value.playing();
  168. if (playing) {
  169. audioBgm.value.pause();
  170. }
  171. state.playStatus = false;
  172. state.currentTime = 0;
  173. audioBgm.value.seek(0);
  174. };
  175. return () => {
  176. const music = rootComp?.value.music;
  177. return (
  178. <div
  179. class={[
  180. store.isEditMode ? MusicEditStyle : MusicStyle,
  181. isPc() ? "absolute" : "fixed",
  182. ]}
  183. >
  184. {store.isEditMode ? (
  185. <AudioPlayer
  186. key={music}
  187. playStatus={state.playStatus}
  188. volume={state.volume}
  189. onStatus={playAudio}
  190. onSeekChange={seekChange}
  191. onVolumeChange={volumeChange}
  192. duration={state.duration}
  193. currentTime={state.currentTime}
  194. />
  195. ) : (
  196. <div
  197. class={["music_button", state.playStatus ? "rotating" : ""]}
  198. onClick={() => {
  199. playAudio(!state.playStatus);
  200. }}
  201. >
  202. <IconMusic />
  203. </div>
  204. )}
  205. </div>
  206. );
  207. };
  208. },
  209. });
  210. const AudioPlayer = defineComponent({
  211. props: {
  212. volume: number(),
  213. playStatus: bool(),
  214. currentTime: number(),
  215. duration: number(),
  216. },
  217. emits: ["status", "seekChange", "volumeChange"],
  218. setup(props, { emit }) {
  219. const audioControl = (playStatus: boolean) => {
  220. emit("status", playStatus);
  221. };
  222. const seekChange = (v: any) => {
  223. emit("seekChange", v);
  224. };
  225. const volumeChange = (v: any) => {
  226. emit("volumeChange", v);
  227. };
  228. const formatTime = (secs?: number) => {
  229. if (!secs) {
  230. return "00:00";
  231. }
  232. secs = Math.round(secs);
  233. const minutes = Math.floor(secs / 60) || 0;
  234. const seconds = secs - minutes * 60 || 0;
  235. return (
  236. String(minutes).padStart(2, "0") +
  237. ":" +
  238. String(seconds).padStart(2, "0")
  239. );
  240. };
  241. const volumeSlider = () => {
  242. return (
  243. <div class={[VolumeSliderStyle]}>
  244. <div class={"h-100px pb-8px"}>
  245. <Slider
  246. tooltipVisible={false}
  247. min={0}
  248. max={1}
  249. step={0.1}
  250. vertical={true}
  251. value={props.volume}
  252. onChange={volumeChange}
  253. ></Slider>
  254. </div>
  255. </div>
  256. );
  257. };
  258. return () => {
  259. return (
  260. <div class={AudioPlayerStyle}>
  261. {!props.playStatus ? (
  262. <Button
  263. type="link"
  264. icon={<PlayCircleOutlined style={{ fontSize: "24px" }} />}
  265. onClick={() => audioControl(true)}
  266. ></Button>
  267. ) : (
  268. <Button
  269. type="link"
  270. icon={<PauseCircleOutlined style={{ fontSize: "24px" }} />}
  271. onClick={() => audioControl(false)}
  272. ></Button>
  273. )}
  274. <div class={"flex-1 px-10px"}>
  275. <Slider
  276. disabled={!props.playStatus}
  277. tooltipVisible={false}
  278. min={0}
  279. max={Math.floor(props.duration || 0)}
  280. value={props.currentTime}
  281. onChange={seekChange}
  282. ></Slider>
  283. </div>
  284. <div>
  285. {formatTime(props.currentTime)}/{formatTime(props.duration)}
  286. </div>
  287. <div>
  288. <Dropdown
  289. disabled={!props.playStatus}
  290. overlay={volumeSlider()}
  291. trigger="click"
  292. placement="top"
  293. >
  294. <Button
  295. type="link"
  296. icon={<SoundOutlined style={{ fontSize: "18px" }} />}
  297. ></Button>
  298. </Dropdown>
  299. </div>
  300. </div>
  301. );
  302. };
  303. },
  304. });
  305. const VolumeSliderStyle = css`
  306. margin-bottom: -4px;
  307. padding: 8px 4px;
  308. background-color: #303030;
  309. border-radius: 4px;
  310. box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.2);
  311. /* slider style */
  312. .ant-slider-disabled {
  313. opacity: 0.7;
  314. }
  315. .ant-slider-step {
  316. background-color: rgba(255, 255, 255, 0.27);
  317. }
  318. .ant-slider-track {
  319. border-radius: 4px;
  320. background-color: rgba(255, 255, 255, 1);
  321. }
  322. .ant-slider:not(.ant-slider-disabled):hover {
  323. .ant-slider-handle {
  324. background-color: @inf-primary-color;
  325. &:not(.ant-tooltip-open) {
  326. border-color: #fff;
  327. }
  328. }
  329. }
  330. .ant-slider {
  331. &.ant-slider-disabled {
  332. .ant-slider-handle {
  333. background-color: #bbb;
  334. opacity: 0.8;
  335. }
  336. }
  337. }
  338. .ant-slider-handle {
  339. width: 14px;
  340. height: 8px;
  341. border-radius: 2px;
  342. border-color: #fff;
  343. background-color: #fff;
  344. }
  345. .ant-slider-handle-click-focused {
  346. border-color: #fff;
  347. background-color: @inf-primary-color;
  348. }
  349. `;
  350. const MusicEditStyle = css`
  351. flex: 1;
  352. `;
  353. const AudioPlayerStyle = css`
  354. width: 100%;
  355. display: flex;
  356. align-items: center;
  357. /* slider style */
  358. .ant-slider-disabled {
  359. opacity: 0.7;
  360. }
  361. .ant-slider-step {
  362. background-color: rgba(255, 255, 255, 0.27);
  363. }
  364. .ant-slider-track {
  365. border-radius: 4px;
  366. background-color: rgba(255, 255, 255, 1);
  367. }
  368. .ant-slider:not(.ant-slider-disabled):hover {
  369. .ant-slider-handle {
  370. background-color: @inf-primary-color;
  371. &:not(.ant-tooltip-open) {
  372. border-color: #fff;
  373. }
  374. }
  375. }
  376. .ant-slider {
  377. &.ant-slider-disabled {
  378. .ant-slider-handle {
  379. background-color: #bbb;
  380. opacity: 0.8;
  381. }
  382. }
  383. }
  384. .ant-slider-handle {
  385. width: 8px;
  386. border-radius: 2px;
  387. border-color: #fff;
  388. background-color: #fff;
  389. }
  390. .ant-slider-handle-click-focused {
  391. border-color: #fff;
  392. background-color: @inf-primary-color;
  393. }
  394. `;
  395. const MusicStyle = css`
  396. top: 10px;
  397. right: 10px;
  398. z-index: 999;
  399. .music_button {
  400. width: 48px;
  401. height: 48px;
  402. display: inline-flex;
  403. justify-content: center;
  404. align-items: center;
  405. background-color: rgba(0, 0, 0, 0.5);
  406. font-size: 28px;
  407. border-radius: 24px;
  408. cursor: pointer;
  409. &.rotating {
  410. animation: myRotate 5s linear infinite;
  411. }
  412. }
  413. @keyframes myRotate {
  414. 0% {
  415. transform: rotate(0);
  416. }
  417. 100% {
  418. transform: rotate(360deg);
  419. }
  420. }
  421. `;