index.tsx 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778
  1. import {
  2. defineComponent,
  3. nextTick,
  4. onMounted,
  5. onUnmounted,
  6. reactive,
  7. watch,
  8. ref
  9. } from 'vue';
  10. // import WaveSurfer from 'wavesurfer.js';
  11. import styles from './index.module.less';
  12. import MSticky from '@/components/m-sticky';
  13. import MHeader from '@/components/m-header';
  14. import {
  15. Button,
  16. Cell,
  17. Image,
  18. List,
  19. Popup,
  20. Slider,
  21. showDialog,
  22. showToast,
  23. Sticky,
  24. TextEllipsis
  25. } from 'vant';
  26. import iconDownload from './images/icon-download.png';
  27. import iconShare from './images/icon-share.png';
  28. import iconDelete from './images/icon-delete.png';
  29. import iconEdit from './images/edit.png';
  30. import iconUpward from './images/upward.png';
  31. import iconMember from './images/icon-member.png';
  32. import iconZan from './images/icon-zan.png';
  33. import promptImg from './images/prompt.png';
  34. import confirmImg from './images/confirm.png';
  35. import canceImg from './images/cance.png';
  36. import iconZanActive from './images/icon-zan-active.png';
  37. import iconPlay from './images/icon-play.png';
  38. import iconPause from './images/icon-pause.png';
  39. import { postMessage, promisefiyPostMessage } from '@/helpers/native-message';
  40. import { browser, getGradeCh, getSecondRPM, vaildMusicScoreUrl } from '@/helpers/utils';
  41. import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router';
  42. import {
  43. api_userMusicDetail,
  44. api_userMusicRemove,
  45. api_userMusicStarPage
  46. } from './api';
  47. import MEmpty from '@/components/m-empty';
  48. import dayjs from 'dayjs';
  49. import MVideo from '@/components/m-video';
  50. import ShareModel from './share-model';
  51. import { usePageVisibility, useEventListener } from '@vant/use';
  52. import "plyr/dist/plyr.css";
  53. import Plyr from "plyr";
  54. import { Vue3Lottie } from "vue3-lottie";
  55. import audioBga from "./images/audioBga.json";
  56. import audioBga1 from "./images/leftCloud.json";
  57. import audioBga2 from "./images/rightCloud.json";
  58. import videobg from "./images/videobg.png";
  59. import playProgressData from "./playCreation/playProgress"
  60. import Loading from './loading';
  61. import { generateMixedData } from "./audioVisualDraw"
  62. export default defineComponent({
  63. name: 'creation-detail',
  64. setup() {
  65. const route = useRoute();
  66. const router = useRouter();
  67. const isScreenScroll = ref(false)
  68. const mStickyBottom = ref()
  69. const mStickyUpward = ref()
  70. const state = reactive({
  71. id: route.query.id,
  72. deleteStatus: false,
  73. shareStatus: false,
  74. playType: '' as 'Audio' | 'Video' | '', // 播放类型
  75. musicDetail: {} as any,
  76. isClick: false,
  77. list: [] as any,
  78. listState: {
  79. dataShow: true, // 判断是否有数据
  80. loading: false,
  81. finished: false
  82. },
  83. params: {
  84. page: 1,
  85. rows: 20
  86. },
  87. _plrl: null as any,
  88. heightV:0,
  89. heightB:0
  90. });
  91. const plyrState = reactive({
  92. duration: 0,
  93. currentTime: 0,
  94. mediaTimeShow: false,
  95. playIngShow: true
  96. })
  97. // 谱面
  98. const staffState = reactive({
  99. staffSrc: "",
  100. isShow: false,
  101. height:"initial",
  102. speedRate:1,
  103. musicRenderType:"staff",
  104. partIndex: 0
  105. })
  106. const staffDom= ref<HTMLIFrameElement>()
  107. const {playStaff, pauseStaff, updateProgressStaff} = staffMoveInstance()
  108. // 获取列表
  109. const getStarList = async () => {
  110. try {
  111. if (state.isClick) return;
  112. state.isClick = true;
  113. const res = await api_userMusicStarPage({
  114. userMusicId: state.id,
  115. ...state.params
  116. });
  117. state.listState.loading = false;
  118. const result = res.data || {};
  119. // 处理重复请求数据
  120. if (state.list.length > 0 && result.current === 1) {
  121. return;
  122. }
  123. state.list = state.list.concat(result.rows || []);
  124. state.listState.finished = result.current >= result.pages;
  125. state.params.page = result.current + 1;
  126. state.listState.dataShow = state.list.length > 0;
  127. state.isClick = false;
  128. } catch {
  129. state.listState.dataShow = false;
  130. state.listState.finished = true;
  131. state.isClick = false;
  132. }
  133. };
  134. // 删除作品
  135. const onDelete = async () => {
  136. try {
  137. await api_userMusicRemove({ id: state.id });
  138. setTimeout(() => {
  139. state.deleteStatus = false;
  140. showToast('删除成功');
  141. }, 100);
  142. setTimeout(() => {
  143. if (browser().isApp) {
  144. postMessage({
  145. api: 'goBack'
  146. });
  147. } else {
  148. router.back();
  149. }
  150. }, 1200);
  151. } catch {
  152. //
  153. }
  154. };
  155. // 下载
  156. const onDownload = async () => {
  157. await promisefiyPostMessage({
  158. api: 'saveFile',
  159. content: {
  160. url: state.musicDetail.videoUrl
  161. }
  162. });
  163. };
  164. // 滚动事件
  165. const cleanScrollEvent = useEventListener('scroll', () => {
  166. const height =
  167. window.scrollY ||
  168. document.documentElement.scrollTop
  169. // 防止多次调用
  170. if(height > 0 && isScreenScroll.value === false){
  171. isScreenScroll.value = true
  172. setStatusBarTextColor(false)
  173. }
  174. if(height <= 0){
  175. isScreenScroll.value = false
  176. setStatusBarTextColor(true)
  177. }
  178. })
  179. // 设置导航栏颜色
  180. function setStatusBarTextColor(isWhite:boolean){
  181. postMessage({
  182. api: 'setStatusBarTextColor',
  183. content: { statusBarTextColor: isWhite }
  184. })
  185. }
  186. // 初始化 媒体播放
  187. function initMediaPlay(){
  188. const id = state.playType === "Audio" ? "#audioMediaSrc" : "#videoMediaSrc";
  189. state._plrl = new Plyr(id, {
  190. controls: ["progress"],
  191. fullscreen: {
  192. enabled: false,
  193. fallback: false
  194. }
  195. });
  196. const player = state._plrl
  197. // 创建音波数据
  198. if(state.playType === "Audio"){
  199. const audioDom = document.querySelector("#audioMediaSrc") as HTMLAudioElement
  200. const canvasDom = document.querySelector("#audioVisualizer") as HTMLCanvasElement
  201. const { pauseVisualDraw, playVisualDraw } = audioVisualDraw(audioDom, canvasDom)
  202. player.on('play', () => {
  203. playVisualDraw()
  204. });
  205. player.on('pause', () => {
  206. pauseVisualDraw()
  207. });
  208. }
  209. player.on('loadedmetadata', () => {
  210. player.currentTime = playProgressData.playProgress
  211. });
  212. player.on("timeupdate", ()=>{
  213. plyrState.currentTime = player.currentTime
  214. })
  215. player.on('play', () => {
  216. plyrState.playIngShow = false
  217. playStaff()
  218. });
  219. player.on('pause', () => {
  220. plyrState.playIngShow = true
  221. pauseStaff()
  222. });
  223. player.on('ended', () => {
  224. player.currentTime = 0
  225. if(!player.playing){
  226. setTimeout(() => {
  227. updateProgressStaff(player.currentTime)
  228. }, 100);
  229. }
  230. });
  231. // 处理按压事件
  232. const handleStart = () => {
  233. plyrState.duration = player.duration
  234. plyrState.mediaTimeShow = true
  235. };
  236. // 处理松开事件
  237. const handleEnd = () => {
  238. plyrState.mediaTimeShow = false
  239. // 暂停的时候调用
  240. if(!player.playing){
  241. updateProgressStaff(player.currentTime)
  242. }
  243. };
  244. const progressDom = document.querySelector("#playMediaSection .plyr__controls .plyr__progress__container") as HTMLElement
  245. progressDom.addEventListener('mousedown', handleStart);
  246. progressDom.addEventListener('touchstart', handleStart);
  247. progressDom.addEventListener('mouseup', handleEnd);
  248. progressDom.addEventListener('touchend', handleEnd);
  249. }
  250. //点击改变播放状态
  251. function handlerClickPlay(){
  252. if (state._plrl.playing) {
  253. state._plrl.pause();
  254. } else {
  255. state._plrl.play();
  256. }
  257. }
  258. /**
  259. * 音频可视化
  260. * @param audioDom
  261. * @param canvasDom
  262. * @param fftSize 2的幂数,最小为32
  263. */
  264. function audioVisualDraw(audioDom: HTMLAudioElement, canvasDom: HTMLCanvasElement, fftSize = 128) {
  265. type propsType = { canvWidth: number; canvHeight: number; canvFillColor: string; lineColor: string; lineGap: number }
  266. // canvas
  267. const canvasCtx = canvasDom.getContext("2d")!
  268. const { width, height } = canvasDom.getBoundingClientRect()
  269. canvasDom.width = width
  270. canvasDom.height = height
  271. // audio
  272. // let audioCtx : AudioContext | null = null
  273. // let analyser : AnalyserNode | null = null
  274. // let source : MediaElementAudioSourceNode | null = null
  275. // const dataArray = new Uint8Array(fftSize / 2)
  276. const draw = (data: Uint8Array, ctx: CanvasRenderingContext2D, { lineGap, canvWidth, canvHeight, canvFillColor, lineColor }: propsType) => {
  277. if (!ctx) return
  278. const w = canvWidth
  279. const h = canvHeight
  280. fillCanvasBackground(ctx, w, h, canvFillColor)
  281. // 可视化
  282. const dataLen = data.length
  283. let step = (w / 2 - lineGap * dataLen) / dataLen
  284. step < 1 && (step = 1)
  285. const midX = w / 2
  286. const midY = h / 2
  287. let xLeft = midX
  288. for (let i = 0; i < dataLen; i++) {
  289. const value = data[i]
  290. const percent = value / 255 // 最大值为255
  291. const barHeight = percent * midY
  292. canvasCtx.fillStyle = lineColor
  293. // 中间加间隙
  294. if (i === 0) {
  295. xLeft -= lineGap / 2
  296. }
  297. canvasCtx.fillRect(xLeft - step, midY - barHeight, step, barHeight)
  298. canvasCtx.fillRect(xLeft - step, midY, step, barHeight)
  299. xLeft -= step + lineGap
  300. }
  301. let xRight = midX
  302. for (let i = 0; i < dataLen; i++) {
  303. const value = data[i]
  304. const percent = value / 255 // 最大值为255
  305. const barHeight = percent * midY
  306. canvasCtx.fillStyle = lineColor
  307. if (i === 0) {
  308. xRight += lineGap / 2
  309. }
  310. canvasCtx.fillRect(xRight, midY - barHeight, step, barHeight)
  311. canvasCtx.fillRect(xRight, midY, step, barHeight)
  312. xRight += step + lineGap
  313. }
  314. }
  315. const fillCanvasBackground = (ctx: CanvasRenderingContext2D, w: number, h: number, colors: string) => {
  316. ctx.clearRect(0, 0, w, h)
  317. ctx.fillStyle = colors
  318. ctx.fillRect(0, 0, w, h)
  319. }
  320. const requestAnimationFrameFun = () => {
  321. // requestAnimationFrame(() => {
  322. // //analyser?.getByteFrequencyData(dataArray)
  323. // draw(generateMixedData(48), canvasCtx, {
  324. // lineGap: 2,
  325. // canvWidth: width,
  326. // canvHeight: height,
  327. // canvFillColor: "transparent",
  328. // lineColor: "rgba(255, 255, 255, 0.7)"
  329. // })
  330. // if (!isPause) {
  331. // requestAnimationFrameFun()
  332. // }
  333. // })
  334. const _time = setInterval(() => {
  335. if (isPause) {
  336. clearInterval(_time)
  337. return
  338. }
  339. //analyser?.getByteFrequencyData(dataArray)
  340. draw(generateMixedData(38), canvasCtx, {
  341. lineGap: 3,
  342. canvWidth: width,
  343. canvHeight: height,
  344. canvFillColor: "transparent",
  345. lineColor: "rgba(255, 255, 255, 0.7)"
  346. })
  347. }, 300);
  348. }
  349. let isPause = true
  350. const playVisualDraw = () => {
  351. // if (!audioCtx) {
  352. // audioCtx = new AudioContext()
  353. // source = audioCtx.createMediaElementSource(audioDom)
  354. // analyser = audioCtx.createAnalyser()
  355. // analyser.fftSize = fftSize
  356. // source?.connect(analyser)
  357. // analyser.connect(audioCtx.destination)
  358. // }
  359. //audioCtx.resume() // 重新更新状态 加了暂停和恢复音频音质发生了变化 所以这里取消了
  360. isPause = false
  361. requestAnimationFrameFun()
  362. }
  363. const pauseVisualDraw = () => {
  364. isPause = true
  365. requestAnimationFrame(()=>{
  366. canvasCtx.clearRect(0, 0, width, height);
  367. })
  368. //audioCtx?.suspend() // 暂停 加了暂停和恢复音频音质发生了变化 所以这里取消了
  369. // source?.disconnect()
  370. // analyser?.disconnect()
  371. }
  372. return {
  373. playVisualDraw,
  374. pauseVisualDraw
  375. }
  376. }
  377. function handlerLandscapeScreen(event:any){
  378. event.stopPropagation()
  379. playProgressData.playState = !!state._plrl?.playing
  380. playProgressData.playProgress = state._plrl?.currentTime || 0
  381. router.push({
  382. path:"/playCreation",
  383. query:{
  384. resourceUrl:encodeURIComponent(state.musicDetail?.videoUrl),
  385. videoBgUrl:encodeURIComponent(state.musicDetail?.videoImg || ""),
  386. musicSheetName:encodeURIComponent(state.musicDetail?.musicSheetName),
  387. username:encodeURIComponent(state.musicDetail?.username),
  388. musicSheetId:encodeURIComponent(state.musicDetail?.musicSheetId),
  389. speedRate:encodeURIComponent(staffState.speedRate),
  390. musicRenderType:encodeURIComponent(staffState.musicRenderType),
  391. partIndex:encodeURIComponent(staffState.partIndex),
  392. }
  393. })
  394. }
  395. // 初始化五线谱
  396. function initStaff(){
  397. const src = `${vaildMusicScoreUrl()}/instrument/#/simple-detail?id=${state.musicDetail.musicSheetId}&musicRenderType=${staffState.musicRenderType}&part-index=${staffState.partIndex}`;
  398. //const src = `http://192.168.3.122:3000/instrument.html#/simple-detail?id=${state.musicDetail.musicSheetId}&musicRenderType=${staffState.musicRenderType}&part-index=${staffState.partIndex}`;
  399. staffState.staffSrc = src
  400. window.addEventListener('message', (event) => {
  401. const { api, height } = event.data;
  402. if (api === 'api_musicPage') {
  403. staffState.isShow = true
  404. staffState.height = height + "px"
  405. // 如果是播放中自动开始播放 不是播放 自动跳转到当前位置
  406. if(playProgressData.playState){
  407. handlerClickPlay()
  408. }else{
  409. updateProgressStaff(state._plrl.currentTime)
  410. }
  411. }
  412. });
  413. }
  414. function staffMoveInstance(){
  415. let isPause = true
  416. const requestAnimationFrameFun = () => {
  417. requestAnimationFrame(() => {
  418. staffDom.value?.contentWindow?.postMessage(
  419. {
  420. api: 'api_playProgress',
  421. content: {
  422. currentTime: state._plrl.currentTime * staffState.speedRate
  423. }
  424. },
  425. "*"
  426. )
  427. if (!isPause) {
  428. requestAnimationFrameFun()
  429. }
  430. })
  431. }
  432. const playStaff = () => {
  433. // 没渲染不执行
  434. if(!staffState.isShow) return
  435. isPause = false
  436. staffDom.value?.contentWindow?.postMessage(
  437. {
  438. api: 'api_play'
  439. },
  440. "*"
  441. )
  442. requestAnimationFrameFun()
  443. }
  444. const pauseStaff = () => {
  445. // 没渲染不执行
  446. if(!staffState.isShow) return
  447. isPause = true
  448. staffDom.value?.contentWindow?.postMessage(
  449. {
  450. api: 'api_paused'
  451. },
  452. "*"
  453. )
  454. }
  455. const updateProgressStaff = (currentTime: number) => {
  456. // 没渲染不执行
  457. if(!staffState.isShow) return
  458. staffDom.value?.contentWindow?.postMessage(
  459. {
  460. api: 'api_updateProgress',
  461. content: {
  462. currentTime: currentTime * staffState.speedRate
  463. }
  464. },
  465. "*"
  466. )
  467. }
  468. return {
  469. playStaff,
  470. pauseStaff,
  471. updateProgressStaff
  472. }
  473. }
  474. onMounted(async () => {
  475. setStatusBarTextColor(true)
  476. try {
  477. const res = await api_userMusicDetail(state.id);
  478. // console.log(res);
  479. if (res.code === 999) {
  480. showDialog({
  481. message: res.message,
  482. theme: 'round-button',
  483. confirmButtonColor:
  484. 'linear-gradient(73deg, #5BECFF 0%, #259CFE 100%)'
  485. }).then(() => {
  486. if (browser().isApp) {
  487. postMessage({
  488. api: 'goBack'
  489. });
  490. } else {
  491. router.back();
  492. }
  493. });
  494. return;
  495. }
  496. state.musicDetail = res.data || {};
  497. try{
  498. const jsonConfig = JSON.parse(res.data.jsonConfig)
  499. jsonConfig.speedRate && (staffState.speedRate = jsonConfig.speedRate)
  500. jsonConfig.musicRenderType && (staffState.musicRenderType = jsonConfig.musicRenderType)
  501. jsonConfig.partIndex && (staffState.partIndex = jsonConfig.partIndex)
  502. }catch{
  503. }
  504. // 五线谱
  505. initStaff()
  506. getStarList();
  507. // 判断是视频还是音频
  508. if (res.data.videoUrl.lastIndexOf('mp4') !== -1) {
  509. state.playType = 'Video';
  510. } else {
  511. state.playType = 'Audio';
  512. }
  513. nextTick(()=>{
  514. initMediaPlay()
  515. })
  516. } catch {
  517. //
  518. }
  519. requestAnimationFrame(()=>{
  520. mStickyBottom.value?.onChnageHeight()
  521. mStickyBottom.value?.onChnageHeight()
  522. })
  523. });
  524. onUnmounted(() => {
  525. cleanScrollEvent()
  526. state._plrl?.destroy()
  527. });
  528. onBeforeRouteLeave((to, from, next)=>{
  529. if(to.path !== "/playCreation"){
  530. playProgressData.playProgress = 0
  531. playProgressData.playState = false
  532. }
  533. next()
  534. })
  535. return () => (
  536. <div
  537. style={
  538. {
  539. '--barheight':state.heightV + "px"
  540. }
  541. }
  542. class={[
  543. styles.creation,
  544. browser().isTablet && styles.creationTablet,
  545. isScreenScroll.value && styles.isScreenScroll
  546. ]}>
  547. <div class={styles.creationBg}></div>
  548. <MSticky position="top"
  549. onBarHeight={(height: any) => {
  550. console.log(height, 'height', height)
  551. state.heightV = height
  552. }}
  553. >
  554. <MHeader
  555. color={isScreenScroll.value ? "#333333" : "#ffffff"}
  556. background={isScreenScroll.value ? `rgb(255,255,255` : "transparent"}
  557. title={state.musicDetail?.musicSheetName}
  558. border={false}
  559. isBack={route.query.platformType != 'ANALYSIS'}
  560. onLeftClick={()=>{ setStatusBarTextColor(false) }}
  561. />
  562. </MSticky>
  563. <div class={styles.singer}>
  564. 演奏:{state.musicDetail?.username}
  565. </div>
  566. <Sticky offsetTop={state.heightV - 1 + "px"}>
  567. <div class={[styles.playSection, plyrState.mediaTimeShow && styles.mediaTimeShow]} id="playMediaSection" onClick={handlerClickPlay}>
  568. {
  569. state.playType === 'Audio' &&
  570. <div class={styles.audioBox}>
  571. <canvas class={styles.audioVisualizer} id="audioVisualizer"></canvas>
  572. <Vue3Lottie class={styles.audioBga} animationData={audioBga} autoPlay={true} loop={true}></Vue3Lottie>
  573. <Vue3Lottie class={styles.audioBga1} animationData={audioBga1} autoPlay={true} loop={true}></Vue3Lottie>
  574. <Vue3Lottie class={styles.audioBga2} animationData={audioBga2} autoPlay={true} loop={true}></Vue3Lottie>
  575. <audio
  576. crossorigin="anonymous"
  577. id="audioMediaSrc"
  578. src={state.musicDetail?.videoUrl}
  579. controls="false"
  580. preload="metadata"
  581. playsinline
  582. webkit-playsinline
  583. />
  584. </div>
  585. }
  586. {
  587. state.playType === 'Video' &&
  588. <video
  589. id="videoMediaSrc"
  590. class={styles.videoBox}
  591. src={state.musicDetail?.videoUrl}
  592. data-poster={ state.musicDetail?.videoImg || videobg}
  593. preload="metadata"
  594. playsinline
  595. />
  596. }
  597. <div class={[styles.playLarge, !plyrState.mediaTimeShow && plyrState.playIngShow && styles.playIngShow]}></div>
  598. <div class={styles.mediaTimeCon}>
  599. <div class={styles.mediaTime}>
  600. <div>
  601. {getSecondRPM(plyrState.currentTime)}
  602. </div>
  603. <div class={styles.note}>/</div>
  604. <div class={styles.duration}>
  605. {getSecondRPM(plyrState.duration)}
  606. </div>
  607. </div>
  608. </div>
  609. <div class={styles.landscapeScreen} onClick={handlerLandscapeScreen}></div>
  610. {/* 谱面 */}
  611. {
  612. staffState.staffSrc &&
  613. <div class={[styles.staffBoxCon, staffState.isShow && styles.staffBoxShow]}>
  614. <div
  615. class={styles.staffBox}
  616. style={
  617. {
  618. '--staffBoxHeight':staffState.height
  619. }
  620. }
  621. >
  622. <div class={styles.mask}></div>
  623. <iframe
  624. ref={staffDom}
  625. class={styles.staff}
  626. frameborder="0"
  627. src={staffState.staffSrc}>
  628. </iframe>
  629. </div>
  630. </div>
  631. }
  632. </div>
  633. </Sticky>
  634. <div class={styles.musicSection}>
  635. <div class={styles.avatarInfoBox}>
  636. <div class={styles.avatar}>
  637. <Image class={styles.userLogo} src={state.musicDetail.avatar} />
  638. <div class={styles.infoCon}>
  639. <div class={styles.info}>
  640. <span class={styles.userName}>{state.musicDetail?.username}</span>
  641. {state.musicDetail.vipFlag && (
  642. <img src={iconMember} class={styles.iconMember} />
  643. )}
  644. </div>
  645. <div class={styles.sub}>
  646. {state.musicDetail.subjectName}{' '}
  647. {getGradeCh(state.musicDetail.currentGradeNum - 1)}
  648. </div>
  649. </div>
  650. </div>
  651. <div class={styles.linkes}>
  652. <img src={iconZan} class={styles.iconZan} />
  653. <span>{state.musicDetail.likeNum}</span>
  654. </div>
  655. </div>
  656. <TextEllipsis class={styles.textEllipsis} rows={2} content={state.musicDetail?.desc} expand-text="展开" collapse-text="收起" />
  657. </div>
  658. <div class={styles.likeSection}>
  659. <div class={styles.likeTitle}>点赞记录</div>
  660. {state.listState.dataShow ? (
  661. <List
  662. finished={state.listState.finished}
  663. finishedText=" "
  664. onLoad={getStarList}
  665. immediateCheck={false}>
  666. {state.list.map((item: any, index: number) => (
  667. <Cell
  668. class={[styles.likeItem, index===state.list.length-1&&styles.likeItemLast]}
  669. border={false}
  670. >
  671. {{
  672. icon: () => (
  673. <Image src={item.userAvatar} class={styles.userLogo} />
  674. ),
  675. title: () => (
  676. <div class={styles.userInfo}>
  677. <p class={styles.name}>{item.userName}</p>
  678. <p class={styles.sub}>
  679. {item.subjectName}{' '}
  680. {getGradeCh(item.currentGradeNum - 1)}
  681. </p>
  682. </div>
  683. ),
  684. value: () => (
  685. <div class={styles.time}>
  686. {dayjs(item.createTime).format('YYYY-MM-DD HH:mm')}
  687. </div>
  688. )
  689. }}
  690. </Cell>
  691. ))}
  692. </List>
  693. ) : (
  694. <MEmpty class={styles.mEmpty} image={"empty2"} description="暂无点赞记录" />
  695. )}
  696. </div>
  697. {
  698. !isScreenScroll.value &&
  699. <MSticky ref={mStickyUpward} position="bottom" offsetBottom={state.heightB - 1 + "px"} >
  700. <div class={styles.upward}>
  701. <img src={iconUpward} />
  702. </div>
  703. </MSticky>
  704. }
  705. <MSticky ref={mStickyBottom} position="bottom" onBarHeight={(height: any) => {
  706. console.log(height, 'height', height)
  707. state.heightB = height
  708. }}>
  709. <div class={styles.bottomSection}>
  710. <div class={styles.bottomShare}>
  711. <p onClick={onDownload}>
  712. <img src={iconDownload} />
  713. <span>下载</span>
  714. </p>
  715. <p onClick={() => (state.shareStatus = true)}>
  716. <img src={iconShare} />
  717. <span>分享</span>
  718. </p>
  719. <p onClick={() => (state.deleteStatus = true)}>
  720. <img src={iconDelete} />
  721. <span>删除</span>
  722. </p>
  723. </div>
  724. <img src={iconEdit}
  725. class={styles.btnEdit}
  726. onClick={() => {
  727. router.push({
  728. path: '/creation-edit',
  729. query: {
  730. id: state.id
  731. }
  732. });
  733. }}
  734. />
  735. </div>
  736. </MSticky>
  737. <Popup
  738. v-model:show={state.deleteStatus}
  739. overlay-style={
  740. {
  741. backgroundColor:"rgba(0,0,0,.5)"
  742. }
  743. }
  744. round
  745. class={styles.popupContainer}>
  746. <img class={styles.prompt} src={promptImg} />
  747. <div class={styles.deleteBox}>
  748. <p class={styles.popupContent}>确定删除吗?</p>
  749. <div class={styles.popupBtnGroup}>
  750. <img src={canceImg} onClick={() => (state.deleteStatus = false)} />
  751. <img src={confirmImg} onClick={onDelete} />
  752. </div>
  753. </div>
  754. </Popup>
  755. <Popup
  756. position="bottom"
  757. v-model:show={state.shareStatus}
  758. style={{ background: 'transparent' }}>
  759. <ShareModel
  760. playType={state.playType}
  761. musicDetail={state.musicDetail}
  762. onClose={() => (state.shareStatus = false)}
  763. />
  764. </Popup>
  765. {
  766. !staffState.isShow && <Loading></Loading>
  767. }
  768. </div>
  769. );
  770. }
  771. });