index.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. import { defineComponent, onMounted, onUnmounted, reactive, watch } from 'vue';
  2. // import WaveSurfer from 'wavesurfer.js';
  3. import styles from './index.module.less';
  4. import MSticky from '@/components/m-sticky';
  5. import MHeader from '@/components/m-header';
  6. import {
  7. Button,
  8. Cell,
  9. Image,
  10. List,
  11. Popup,
  12. Slider,
  13. showDialog,
  14. showToast
  15. } from 'vant';
  16. import iconDownload from './images/icon-download.png';
  17. import iconShare from './images/icon-share.png';
  18. import iconDelete from './images/icon-delete.png';
  19. import iconMember from './images/icon-member.png';
  20. import iconZan from './images/icon-zan.png';
  21. import iconZanActive from './images/icon-zan-active.png';
  22. import iconPlay from './images/icon-play.png';
  23. import iconPause from './images/icon-pause.png';
  24. import { postMessage, promisefiyPostMessage } from '@/helpers/native-message';
  25. import { browser, getGradeCh, getSecondRPM } from '@/helpers/utils';
  26. import { useRoute, useRouter } from 'vue-router';
  27. import {
  28. api_userMusicDetail,
  29. api_userMusicRemove,
  30. api_userMusicStarPage
  31. } from './api';
  32. import MEmpty from '@/components/m-empty';
  33. import dayjs from 'dayjs';
  34. import { nextTick } from 'process';
  35. import MVideo from '@/components/m-video';
  36. import ShareModel from './share-model';
  37. import { usePageVisibility } from '@vant/use';
  38. export default defineComponent({
  39. name: 'creation-detail',
  40. setup() {
  41. const route = useRoute();
  42. const router = useRouter();
  43. const audioId = 'a' + +Date.now() + Math.floor(Math.random() * 100);
  44. const state = reactive({
  45. id: route.query.id,
  46. deleteStatus: false,
  47. shareStatus: false,
  48. playType: '' as 'Audio' | 'Video' | '', // 播放类型
  49. musicDetail: {} as any,
  50. timer: null as any,
  51. audioWidth: 0,
  52. paused: true,
  53. currentTime: 0,
  54. duration: 0.1,
  55. loop: false,
  56. dragStatus: false, // 是否开始拖动
  57. isClick: false,
  58. list: [] as any,
  59. listState: {
  60. dataShow: true, // 判断是否有数据
  61. loading: false,
  62. finished: false
  63. },
  64. params: {
  65. page: 1,
  66. rows: 20
  67. }
  68. });
  69. const audioDom = new Audio();
  70. audioDom.controls = true;
  71. audioDom.style.width = '100%';
  72. audioDom.className = styles.audio;
  73. /** 改变播放时间 */
  74. const handleChangeTime = (val: number) => {
  75. state.currentTime = val;
  76. clearTimeout(state.timer);
  77. state.timer = setTimeout(() => {
  78. // audioRef.value.currentTime = val;
  79. audioDom.currentTime = val;
  80. state.timer = null;
  81. }, 60);
  82. };
  83. // 切换音频播放
  84. const onToggleAudio = (e: any) => {
  85. e.stopPropagation();
  86. if (audioDom.paused) {
  87. audioDom.play();
  88. } else {
  89. audioDom.pause();
  90. }
  91. state.paused = audioDom.paused;
  92. };
  93. // 获取列表
  94. const getStarList = async () => {
  95. try {
  96. if (state.isClick) return;
  97. state.isClick = true;
  98. const res = await api_userMusicStarPage({
  99. userMusicId: state.id,
  100. ...state.params
  101. });
  102. state.listState.loading = false;
  103. const result = res.data || {};
  104. // 处理重复请求数据
  105. if (state.list.length > 0 && result.current === 1) {
  106. return;
  107. }
  108. state.list = state.list.concat(result.rows || []);
  109. state.listState.finished = result.current >= result.pages;
  110. state.params.page = result.current + 1;
  111. state.listState.dataShow = state.list.length > 0;
  112. state.isClick = false;
  113. } catch {
  114. state.listState.dataShow = false;
  115. state.listState.finished = true;
  116. state.isClick = false;
  117. }
  118. };
  119. const initAudio = () => {
  120. audioDom.src = state.musicDetail.videoUrl;
  121. audioDom.load();
  122. audioDom.oncanplaythrough = () => {
  123. state.paused = audioDom.paused;
  124. state.duration = audioDom.duration;
  125. };
  126. // 播放时监听
  127. audioDom.addEventListener('timeupdate', () => {
  128. state.duration = audioDom.duration;
  129. state.currentTime = audioDom.currentTime;
  130. const rate = (state.currentTime / state.duration) * 100;
  131. state.audioWidth = rate > 100 ? 100 : rate;
  132. });
  133. audioDom.addEventListener('ended', () => {
  134. state.paused = audioDom.paused;
  135. });
  136. // const wavesurfer = WaveSurfer.create({
  137. // container: document.querySelector(`#${audioId}`) as HTMLElement,
  138. // waveColor: '#fff',
  139. // progressColor: '#2FA1FD',
  140. // url: state.musicDetail.videoUrl,
  141. // cursorWidth: 0,
  142. // height: 35,
  143. // width: 'auto',
  144. // normalize: true,
  145. // // Set a bar width
  146. // barWidth: 2,
  147. // // Optionally, specify the spacing between bars
  148. // barGap: 2,
  149. // // And the bar radius
  150. // barRadius: 4,
  151. // barHeight: 0.6,
  152. // autoScroll: true,
  153. // /** If autoScroll is enabled, keep the cursor in the center of the waveform during playback */
  154. // autoCenter: true,
  155. // hideScrollbar: false,
  156. // media: audioDom
  157. // });
  158. // wavesurfer.once('interaction', () => {
  159. // // wavesurfer.play();
  160. // });
  161. // wavesurfer.once('ready', () => {
  162. // state.paused = audioDom.paused;
  163. // state.duration = audioDom.duration;
  164. // });
  165. // wavesurfer.on('finish', () => {
  166. // state.paused = true;
  167. // });
  168. // // 播放时监听
  169. // audioDom.addEventListener('timeupdate', () => {
  170. // state.currentTime = audioDom.currentTime;
  171. // });
  172. };
  173. // 删除作品
  174. const onDelete = async () => {
  175. try {
  176. await api_userMusicRemove({ id: state.id });
  177. setTimeout(() => {
  178. state.deleteStatus = false;
  179. showToast('删除成功');
  180. }, 100);
  181. setTimeout(() => {
  182. if (browser().isApp) {
  183. postMessage({
  184. api: 'goBack'
  185. });
  186. } else {
  187. router.back();
  188. }
  189. }, 1200);
  190. } catch {
  191. //
  192. }
  193. };
  194. // 下载
  195. const onDownload = async () => {
  196. await promisefiyPostMessage({
  197. api: 'saveFile',
  198. content: {
  199. url: state.musicDetail.videoUrl
  200. }
  201. });
  202. };
  203. const pageVisibility = usePageVisibility();
  204. watch(pageVisibility, value => {
  205. console.log(value);
  206. if (value === 'hidden') {
  207. if (audioDom) {
  208. audioDom.pause();
  209. state.paused = audioDom.paused;
  210. }
  211. }
  212. });
  213. onMounted(async () => {
  214. try {
  215. const res = await api_userMusicDetail(state.id);
  216. // console.log(res);
  217. if (res.code === 999) {
  218. showDialog({
  219. message: res.message,
  220. theme: 'round-button',
  221. confirmButtonColor:
  222. 'linear-gradient(73deg, #5BECFF 0%, #259CFE 100%)'
  223. }).then(() => {
  224. if (browser().isApp) {
  225. postMessage({
  226. api: 'goBack'
  227. });
  228. } else {
  229. router.back();
  230. }
  231. });
  232. return;
  233. }
  234. state.musicDetail = res.data || {};
  235. getStarList();
  236. // 判断是视频还是音频
  237. if (res.data.videoUrl.lastIndexOf('mp4') !== -1) {
  238. state.playType = 'Video';
  239. } else {
  240. state.playType = 'Audio';
  241. // 初始化
  242. nextTick(() => {
  243. initAudio();
  244. });
  245. }
  246. } catch {
  247. //
  248. }
  249. });
  250. onUnmounted(() => {
  251. if (audioDom) {
  252. audioDom.pause();
  253. state.paused = audioDom.paused;
  254. }
  255. });
  256. return () => (
  257. <div class={styles.creation}>
  258. <MSticky position="top">
  259. <MHeader
  260. border={false}
  261. isBack={route.query.platformType != 'ANALYSIS'}
  262. />
  263. </MSticky>
  264. <div class={styles.playSection}>
  265. {state.playType === 'Video' && (
  266. <MVideo
  267. src={state.musicDetail?.videoUrl}
  268. poster={state.musicDetail?.img}
  269. />
  270. )}
  271. {state.playType === 'Audio' && (
  272. <div class={styles.audioSection}>
  273. <div class={styles.audioContainer}>
  274. {/* <div
  275. id={audioId}
  276. onClick={(e: MouseEvent) => {
  277. e.stopPropagation();
  278. }}></div> */}
  279. <div
  280. class={styles.waveActive}
  281. style={{
  282. width: state.audioWidth + '%'
  283. }}></div>
  284. <div class={styles.waveDefault}></div>
  285. </div>
  286. <div class={styles.audioBox}>
  287. <div
  288. class={[styles.audioPan, state.paused && styles.imgRotate]}>
  289. <Image class={styles.audioImg} src={state.musicDetail?.img} />
  290. </div>
  291. <i class={styles.audioPoint}></i>
  292. <i
  293. class={[styles.audioZhen, state.paused && styles.active]}></i>
  294. </div>
  295. <div
  296. class={[styles.controls]}
  297. onClick={(e: Event) => {
  298. e.stopPropagation();
  299. }}
  300. onTouchmove={(e: TouchEvent) => {
  301. // emit('close');
  302. }}>
  303. <div class={styles.actions}>
  304. <div class={styles.actionBtn} onClick={onToggleAudio}>
  305. <img src={state.paused ? iconPlay : iconPause} />
  306. </div>
  307. </div>
  308. <div class={[styles.slider]}>
  309. <Slider
  310. step={0.01}
  311. class={styles.timeProgress}
  312. v-model={state.currentTime}
  313. max={state.duration}
  314. onUpdate:modelValue={val => {
  315. handleChangeTime(val);
  316. }}
  317. onDragStart={() => {
  318. state.dragStatus = true;
  319. console.log('onDragStart');
  320. }}
  321. onDragEnd={() => {
  322. state.dragStatus = false;
  323. console.log('onDragEnd');
  324. }}
  325. />
  326. </div>
  327. <div class={styles.time}>
  328. <div>{getSecondRPM(state.currentTime)}</div>
  329. <span>/</span>
  330. <div>{getSecondRPM(state.duration)}</div>
  331. </div>
  332. </div>
  333. </div>
  334. )}
  335. </div>
  336. <Cell class={styles.userSection} center border={false}>
  337. {{
  338. icon: () => (
  339. <Image class={styles.userLogo} src={state.musicDetail.avatar} />
  340. ),
  341. title: () => (
  342. <div class={styles.userInfo}>
  343. <p class={styles.name}>
  344. <span>{state.musicDetail?.username}</span>
  345. {state.musicDetail.vipFlag && (
  346. <img src={iconMember} class={styles.iconMember} />
  347. )}
  348. </p>
  349. <p class={styles.sub}>
  350. {state.musicDetail.subjectName}{' '}
  351. {getGradeCh(state.musicDetail.currentGradeNum - 1)}
  352. </p>
  353. </div>
  354. ),
  355. value: () => (
  356. <div class={[styles.zan, styles.zanActive]}>
  357. <img src={iconZanActive} class={styles.iconZan} />
  358. {state.musicDetail.likeNum}
  359. </div>
  360. )
  361. }}
  362. </Cell>
  363. <div class={styles.musicSection}>
  364. <div class={styles.musicName}>
  365. <span class={styles.musicTag}>曲目名称</span>
  366. {state.musicDetail?.musicSheetName}
  367. </div>
  368. {state.musicDetail.desc && (
  369. <div class={styles.musicDesc}>{state.musicDetail.desc}</div>
  370. )}
  371. </div>
  372. <div class={styles.likeSection}>
  373. <div class={styles.likeTitle}>点赞记录</div>
  374. {state.listState.dataShow ? (
  375. <List
  376. finished={state.listState.finished}
  377. finishedText=" "
  378. class={[styles.container, styles.containerInformation]}
  379. onLoad={getStarList}
  380. immediateCheck={false}>
  381. {state.list.map((item: any, index: number) => (
  382. <Cell
  383. class={styles.likeItem}
  384. border={state.list.length - 1 == index ? false : true}>
  385. {{
  386. icon: () => (
  387. <Image src={item.userAvatar} class={styles.userLogo} />
  388. ),
  389. title: () => (
  390. <div class={styles.userInfo}>
  391. <p class={styles.name}>{item.userName}</p>
  392. <p class={styles.sub}>
  393. {item.subjectName}{' '}
  394. {getGradeCh(item.currentGradeNum - 1)}
  395. </p>
  396. </div>
  397. ),
  398. value: () => (
  399. <div class={styles.time}>
  400. {dayjs(item.createTime).format('YYYY-MM-DD HH:mm')}
  401. </div>
  402. )
  403. }}
  404. </Cell>
  405. ))}
  406. </List>
  407. ) : (
  408. <MEmpty description="暂无数据" />
  409. )}
  410. </div>
  411. <MSticky position="bottom">
  412. <div class={styles.bottomSection}>
  413. <div class={styles.bottomShare}>
  414. <p onClick={onDownload}>
  415. <img src={iconDownload} />
  416. <span>下载</span>
  417. </p>
  418. <p onClick={() => (state.shareStatus = true)}>
  419. <img src={iconShare} />
  420. <span>分享</span>
  421. </p>
  422. <p onClick={() => (state.deleteStatus = true)}>
  423. <img src={iconDelete} />
  424. <span>删除</span>
  425. </p>
  426. </div>
  427. <Button
  428. round
  429. class={styles.btnEdit}
  430. type="primary"
  431. onClick={() => {
  432. router.push({
  433. path: '/creation-edit',
  434. query: {
  435. id: state.id
  436. }
  437. });
  438. }}>
  439. 编辑
  440. </Button>
  441. </div>
  442. </MSticky>
  443. <Popup
  444. v-model:show={state.deleteStatus}
  445. round
  446. class={styles.popupContainer}>
  447. <p class={styles.popupContent}>确定删除吗?</p>
  448. <div class={styles.popupBtnGroup}>
  449. <Button round onClick={() => (state.deleteStatus = false)}>
  450. 取消
  451. </Button>
  452. <Button round type="primary" onClick={onDelete}>
  453. 确定
  454. </Button>
  455. </div>
  456. </Popup>
  457. <Popup
  458. position="bottom"
  459. v-model:show={state.shareStatus}
  460. style={{ background: 'transparent' }}>
  461. <ShareModel
  462. musicDetail={state.musicDetail}
  463. onClose={() => (state.shareStatus = false)}
  464. />
  465. </Popup>
  466. </div>
  467. );
  468. }
  469. });