index-share.tsx 16 KB

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