index-share.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. import {
  2. defineComponent,
  3. onMounted,
  4. onUnmounted,
  5. reactive,
  6. ref,
  7. watch
  8. } from 'vue';
  9. // import WaveSurfer from 'wavesurfer.js';
  10. // import Regions from 'wavesurfer.js/dist/plugins/regions.js';
  11. import styles from './index.module.less';
  12. import { Cell, Image, List, Popup, Slider, showDialog } from 'vant';
  13. import iconMember from './images/icon-member.png';
  14. import iconZan from './images/icon-zan.png';
  15. import iconZanActive from './images/icon-zan-active.png';
  16. import iconZ from './images/icon-z.png';
  17. import iconPlay from './images/icon-play.png';
  18. import iconPause from './images/icon-pause.png';
  19. import { browser, getGradeCh, getSecondRPM } from '@/helpers/utils';
  20. import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router';
  21. import {
  22. api_openUserMusicDetail,
  23. api_openUserMusicPage,
  24. api_userMusicStar,
  25. api_verification
  26. } from './api';
  27. import MEmpty from '@/components/m-empty';
  28. import { nextTick } from 'process';
  29. import MVideo from '@/components/m-video';
  30. import LoginModel from './login-model';
  31. import { removeAuth } from '../student-register/layout/utils';
  32. import { setLogout } from '@/state';
  33. import { storage } from '@/helpers/storage';
  34. import { ACCESS_TOKEN } from '@/store/mutation-types';
  35. import MWxTip from '@/components/m-wx-tip';
  36. import { usePageVisibility } from '@vant/use';
  37. import videoBg from './images/video-bg.png';
  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. loginTag: false, // 是否登录标识
  47. loginStatus: false,
  48. playType: '' as 'Audio' | 'Video' | '', // 播放类型
  49. musicDetail: {} as any,
  50. timer: null as any,
  51. paused: true,
  52. audioWidth: 0,
  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. messageStatus: false,
  69. message: ''
  70. });
  71. const wavesurfer = ref();
  72. // window.AudioContext = window.AudioContext || window.webkitAudioContext;
  73. const audioDom = new Audio();
  74. audioDom.controls = true;
  75. audioDom.style.width = '100%';
  76. audioDom.className = styles.audio;
  77. /** 改变播放时间 */
  78. const handleChangeTime = (val: number) => {
  79. state.currentTime = val;
  80. clearTimeout(state.timer);
  81. state.timer = setTimeout(() => {
  82. audioDom.currentTime = val;
  83. state.timer = null;
  84. }, 60);
  85. };
  86. // 切换音频播放
  87. const onToggleAudio = (e: any) => {
  88. e.stopPropagation();
  89. if (audioDom.paused) {
  90. audioDom.play();
  91. } else {
  92. audioDom.pause();
  93. }
  94. state.paused = audioDom.paused;
  95. };
  96. // 点赞
  97. const onStarChange = async () => {
  98. // 是否登录
  99. if (!state.loginTag) {
  100. state.loginStatus = true;
  101. return;
  102. }
  103. try {
  104. await api_userMusicStar({
  105. userMusicId: state.id,
  106. star: !state.musicDetail.starFlag
  107. });
  108. state.musicDetail.starFlag = !state.musicDetail.starFlag;
  109. if (state.musicDetail.starFlag) {
  110. state.musicDetail.likeNum += 1;
  111. } else {
  112. state.musicDetail.likeNum -= 1;
  113. }
  114. } catch {
  115. //
  116. }
  117. };
  118. // 获取列表
  119. const getList = async () => {
  120. try {
  121. if (state.isClick) return;
  122. state.isClick = true;
  123. const res = await api_openUserMusicPage({
  124. type: 'FORMAL',
  125. exclusionId: state.id,
  126. sort: 1,
  127. ...state.params
  128. });
  129. state.listState.loading = false;
  130. const result = res.data || {};
  131. // 处理重复请求数据
  132. if (state.list.length > 0 && result.current === 1) {
  133. return;
  134. }
  135. state.list = state.list.concat(result.rows || []);
  136. state.listState.finished = result.current >= result.pages;
  137. state.params.page = result.current + 1;
  138. state.listState.dataShow = state.list.length > 0;
  139. state.isClick = false;
  140. } catch {
  141. state.listState.dataShow = false;
  142. state.listState.finished = true;
  143. state.isClick = false;
  144. }
  145. };
  146. const onDetail = (item: any) => {
  147. router.push({
  148. path: '/shareCreation',
  149. query: {
  150. id: item.id
  151. }
  152. });
  153. };
  154. const initAudio = () => {
  155. try {
  156. audioDom.src = state.musicDetail.videoUrl;
  157. audioDom.load();
  158. audioDom.oncanplaythrough = () => {
  159. state.paused = audioDom.paused;
  160. state.duration = audioDom.duration;
  161. };
  162. // 播放时监听
  163. audioDom.addEventListener('timeupdate', () => {
  164. state.duration = audioDom.duration;
  165. state.currentTime = audioDom.currentTime;
  166. const rate = (state.currentTime / state.duration) * 100;
  167. state.audioWidth = rate > 100 ? 100 : rate;
  168. });
  169. audioDom.addEventListener('ended', () => {
  170. state.paused = audioDom.paused;
  171. });
  172. // wavesurfer.value = WaveSurfer.create({
  173. // container: document.querySelector(`#${audioId}`) as HTMLElement,
  174. // waveColor: '#fff',
  175. // progressColor: '#2FA1FD',
  176. // url: state.musicDetail.videoUrl,
  177. // cursorWidth: 0,
  178. // height: 35,
  179. // width: 'auto',
  180. // normalize: true,
  181. // // Set a bar width
  182. // barWidth: 2,
  183. // // Optionally, specify the spacing between bars
  184. // barGap: 2,
  185. // // And the bar radius
  186. // barRadius: 4,
  187. // barHeight: 1.2,
  188. // /** If autoScroll is enabled, keep the cursor in the center of the waveform during playback */
  189. // // autoCenter: true,
  190. // hideScrollbar: false,
  191. // media: audioDom
  192. // });
  193. // // console.log(wavesurfer.value);
  194. // // const wsRegions = wavesurfer.value.registerPlugin(Regions.create());
  195. // wavesurfer.value.once('interaction', () => {
  196. // // wavesurfer.value.play();
  197. // });
  198. // wavesurfer.value.once('ready', () => {
  199. // state.paused = audioDom.paused;
  200. // state.duration = audioDom.duration;
  201. // });
  202. // wavesurfer.value.on('finish', () => {
  203. // state.paused = true;
  204. // });
  205. } catch (e) {
  206. //
  207. console.log(e);
  208. }
  209. };
  210. const __init = async () => {
  211. try {
  212. // 判断是否登录
  213. const Authorization = storage.get(ACCESS_TOKEN) || '';
  214. if (Authorization) {
  215. const res = await api_verification({
  216. token: Authorization
  217. });
  218. state.loginTag = res.data;
  219. if (!res.data) {
  220. removeAuth();
  221. setLogout();
  222. }
  223. }
  224. const res = await api_openUserMusicDetail(state.id);
  225. if (res.code === 999) {
  226. state.message = res.message;
  227. state.messageStatus = true;
  228. return;
  229. } else {
  230. state.musicDetail = res.data;
  231. getList();
  232. // 判断是视频还是音频
  233. if (res.data.videoUrl.lastIndexOf('mp4') !== -1) {
  234. state.playType = 'Video';
  235. } else {
  236. state.playType = 'Audio';
  237. // 初始化
  238. nextTick(() => {
  239. initAudio();
  240. });
  241. }
  242. }
  243. } catch (err) {
  244. //
  245. state.listState.dataShow = false;
  246. }
  247. };
  248. const pageVisibility = usePageVisibility();
  249. watch(pageVisibility, value => {
  250. console.log(value);
  251. if (value === 'hidden') {
  252. if (audioDom) {
  253. audioDom.pause();
  254. state.paused = audioDom.paused;
  255. }
  256. }
  257. });
  258. onMounted(async () => {
  259. __init();
  260. });
  261. onUnmounted(() => {
  262. if (audioDom) {
  263. audioDom.pause();
  264. state.paused = audioDom.paused;
  265. }
  266. });
  267. onBeforeRouteUpdate((to: any) => {
  268. state.id = to.query.id;
  269. state.playType = '';
  270. state.params.page = 1;
  271. if (audioDom) {
  272. audioDom.currentTime = 0;
  273. audioDom.pause();
  274. state.paused = audioDom.paused;
  275. }
  276. state.list = [];
  277. __init();
  278. });
  279. return () => (
  280. <div
  281. class={[
  282. styles.creation,
  283. browser().isTablet ? styles.creationTablet : ''
  284. ]}>
  285. {/* <video
  286. src={state.musicDetail.videoUrl}
  287. style="width: 100%;height: 200px"
  288. controls></video> */}
  289. <div class={styles.playSection}>
  290. {state.playType === 'Video' && (
  291. <MVideo
  292. class={styles.videoSection}
  293. src={state.musicDetail.videoUrl}
  294. poster={state.musicDetail?.videoImg || videoBg}
  295. />
  296. )}
  297. {state.playType === 'Audio' && (
  298. <div class={styles.audioSection}>
  299. <div class={styles.audioContainer}>
  300. {/* <div
  301. id={audioId}
  302. onClick={(e: MouseEvent) => {
  303. e.stopPropagation();
  304. }}></div> */}
  305. <div
  306. class={styles.waveActive}
  307. style={{
  308. width: state.audioWidth + '%'
  309. }}></div>
  310. <div class={styles.waveDefault}></div>
  311. </div>
  312. <div class={styles.audioBox}>
  313. <div
  314. class={[styles.audioPan, state.paused && styles.imgRotate]}>
  315. <Image class={styles.audioImg} src={state.musicDetail?.img} />
  316. </div>
  317. <i class={styles.audioPoint}></i>
  318. <i
  319. class={[styles.audioZhen, state.paused && styles.active]}></i>
  320. </div>
  321. <div
  322. class={[styles.controls]}
  323. onClick={(e: Event) => {
  324. e.stopPropagation();
  325. }}
  326. onTouchmove={(e: TouchEvent) => {
  327. // emit('close');
  328. }}>
  329. <div class={styles.actions}>
  330. <div class={styles.actionBtn} onClick={onToggleAudio}>
  331. <img src={state.paused ? iconPlay : iconPause} />
  332. </div>
  333. </div>
  334. <div class={[styles.slider]}>
  335. <Slider
  336. step={0.01}
  337. class={styles.timeProgress}
  338. v-model={state.currentTime}
  339. max={state.duration}
  340. onUpdate:modelValue={val => {
  341. handleChangeTime(val);
  342. }}
  343. onDragStart={() => {
  344. state.dragStatus = true;
  345. console.log('onDragStart');
  346. }}
  347. onDragEnd={() => {
  348. state.dragStatus = false;
  349. console.log('onDragEnd');
  350. }}
  351. />
  352. </div>
  353. <div class={styles.time}>
  354. <div>{getSecondRPM(state.currentTime)}</div>
  355. <span>/</span>
  356. <div>{getSecondRPM(state.duration)}</div>
  357. </div>
  358. </div>
  359. </div>
  360. )}
  361. </div>
  362. <Cell class={styles.userSection} center border={false}>
  363. {{
  364. icon: () => (
  365. <Image class={styles.userLogo} src={state.musicDetail.avatar} />
  366. ),
  367. title: () => (
  368. <div class={styles.userInfo}>
  369. <p class={styles.name}>
  370. <span>{state.musicDetail.username}</span>
  371. {state.musicDetail.vipFlag && (
  372. <img src={iconMember} class={styles.iconMember} />
  373. )}
  374. </p>
  375. <p class={styles.sub}>
  376. {state.musicDetail.subjectName}{' '}
  377. {getGradeCh(state.musicDetail.currentGradeNum - 1)}
  378. </p>
  379. </div>
  380. ),
  381. value: () => (
  382. <div
  383. class={[
  384. styles.zan,
  385. state.musicDetail.starFlag && styles.zanActive
  386. ]}
  387. onClick={onStarChange}>
  388. <img
  389. src={state.musicDetail.starFlag ? iconZanActive : iconZan}
  390. class={styles.iconZan}
  391. />
  392. {state.musicDetail.likeNum}
  393. </div>
  394. )
  395. }}
  396. </Cell>
  397. <div class={styles.musicSection}>
  398. <div class={styles.musicName}>
  399. <span class={styles.musicTag}>曲目名称</span>
  400. {state.musicDetail?.musicSheetName}
  401. </div>
  402. {state.musicDetail?.desc && (
  403. <div class={styles.musicDesc}>{state.musicDetail?.desc}</div>
  404. )}
  405. </div>
  406. <div class={styles.likeSection}>
  407. <div class={styles.likeTitle}>推荐作品</div>
  408. {state.listState.dataShow ? (
  409. <List
  410. finished={state.listState.finished}
  411. finishedText=" "
  412. class={[styles.container, styles.containerInformation]}
  413. onLoad={getList}
  414. immediateCheck={false}>
  415. <div class={styles.cellGroup}>
  416. {[1, 2, 3, 4, , 5, 6, 7, 889, 9, 9, 5, 4, , 3, 3, 2, 2].map(
  417. () => (
  418. <>
  419. {state.list.map((item: any) => (
  420. <div class={styles.cell} onClick={() => onDetail(item)}>
  421. <div class={styles.cellImg}>
  422. <Image
  423. class={styles.cellImage}
  424. src={item.img}
  425. fit="cover"
  426. />
  427. <div class={styles.iconZan}>{item.likeNum}</div>
  428. </div>
  429. <div class={[styles.cellTitle, 'van-ellipsis']}>
  430. {item.musicSheetName}
  431. </div>
  432. <div class={styles.users}>
  433. <Image src={item.avatar} class={styles.userImg} />
  434. <span class={styles.name}>{item.username}</span>
  435. </div>
  436. </div>
  437. ))}
  438. </>
  439. )
  440. )}
  441. </div>
  442. </List>
  443. ) : (
  444. <MEmpty description="暂无数据" />
  445. )}
  446. </div>
  447. <Popup
  448. v-model:show={state.loginStatus}
  449. style={{ background: 'transparent', overflow: 'inherit' }}>
  450. <LoginModel
  451. isRegister
  452. onClose={() => (state.loginStatus = false)}
  453. onConfirm={async (val: boolean) => {
  454. state.loginTag = val;
  455. state.loginStatus = false;
  456. const { data } = await api_openUserMusicDetail(state.id);
  457. state.musicDetail = data;
  458. }}
  459. />
  460. </Popup>
  461. <MWxTip
  462. v-model:show={state.messageStatus}
  463. message={state.message}
  464. showButton={false}
  465. />
  466. </div>
  467. );
  468. }
  469. });