feat: 单曲页面

main
fadeaway 6 months ago
parent 0510cb0ba3
commit 6ce2f585d3

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

@ -10,11 +10,13 @@ import type { Metadata } from 'next';
export const metadata: Metadata = { title: '雀乐期刊' };
// 获取期刊信息
async function getJournalInfo(journalId: string) {
const res = await fetch(`http://39.103.180.196:9012/luoo-music/journal/${journalId}`);
return res.json();
}
// 获取期刊歌曲列表
async function getSongList(journalId: string) {
const res = await fetch(`http://39.103.180.196:9012/luoo-music/song/journalId/${journalId}`);
return res.json();

@ -0,0 +1,32 @@
/**
*
*/
import Image from 'next/image';
import EnterQueyueBtn from '@/components/EnterQueyueBtn';
import MusicPanel from '@/components/MusicPanel';
async function getMusic(songId: string) {
const res = await fetch(`http://39.103.180.196:9012/luoo-music/song/${songId}`);
return res.json();
}
export default async function Music({ searchParams: { id } }: any) {
const res = await getMusic(id);
const musicInfo = res?.data;
return (
<main className="max-w-screen-sm min-h-screen mx-auto flex flex-col items-center text-white font-normal bg-gradient-to-b from-[#030303] to-[#1f1f20] px-[18px]">
<div className="flex items-center w-full bg-[#1d1d1d] text-[#ffffffb2] text-[14px] py-[12px] px-[18px] rounded-[6px] mt-[4px]">
<Image className="w-[28px] h-[28px]" width={28} height={28} src="/img/app_icon_white_bg.svg" alt="queyue" />
<span className="flex-1 pl-[12px]"></span>
<Image className="w-[24px] h-[24px]" width={24} height={24} src="/img/jourrnal_icon-5.svg" alt="right-arrow" />
</div>
<MusicPanel musicInfo={musicInfo} />
<div className="mb-[30px]">
<EnterQueyueBtn />
</div>
</main>
);
}

@ -0,0 +1,22 @@
.audioPlayer input[type='range'] {
width: 100%;
height: 2px;
-webkit-appearance: none;
background:
-webkit-linear-gradient(#fff, #fff) no-repeat,
#ffffff33;
background-size: 0%;
position: relative;
outline: none;
box-sizing: border-box;
border-radius: 8px;
}
.audioPlayerInput::-webkit-slider-thumb {
-webkit-appearance: none;
background-color: #fff;
width: 10px;
height: 10px;
border-radius: 50%;
cursor: pointer;
}

@ -0,0 +1,100 @@
'use client';
import { useState, useRef, useEffect, forwardRef, useImperativeHandle } from 'react';
import { formatTime } from '@/utils/formatTime';
import styles from './index.module.css';
export interface IAudioPlayerRef {
audio: HTMLAudioElement | null;
}
export default forwardRef(function AudioPlayer({ src }: { src?: string }, ref: React.Ref<IAudioPlayerRef>) {
const [curTime, setCurTime] = useState('00:00');
const [totalTime, setTotalTime] = useState('00:00');
const audioRef = useRef<HTMLAudioElement>(null);
const sliderRef = useRef<HTMLInputElement>(null);
const isDragingRef = useRef(false);
const setSliderBgSize = () => {
if (sliderRef?.current) {
sliderRef.current.style.backgroundSize = `${(Number(sliderRef.current.value) / (audioRef?.current?.duration || 0)) * 100}%`;
}
};
const handleLoadedMetadata = () => {
setCurTime(formatTime(audioRef?.current?.currentTime));
setTotalTime(formatTime(audioRef?.current?.duration));
};
const handleTimeUpdate = () => {
requestAnimationFrame(() => {
const currentTime = audioRef?.current?.currentTime || 0;
if (!isDragingRef?.current) {
if (sliderRef?.current) {
sliderRef.current.value = `${currentTime}`;
setSliderBgSize();
}
}
setCurTime(formatTime(currentTime));
setTotalTime(formatTime(audioRef?.current?.duration || 0));
});
};
const handleSlideStart = () => {
isDragingRef.current = true;
};
const handleSlideEnd = () => {
if (audioRef?.current) {
audioRef.current.currentTime = Number(sliderRef?.current?.value || 0);
}
isDragingRef.current = false;
};
const handleSlideChange = () => requestAnimationFrame(() => setSliderBgSize());
useEffect(() => {
audioRef?.current?.play();
}, []);
useImperativeHandle(
ref,
() => ({
audio: audioRef.current,
}),
[],
);
return (
<div className={`w-full ${styles.audioPlayer}`}>
<audio
ref={audioRef}
preload="auto"
autoPlay
src={src}
onLoadedMetadata={handleLoadedMetadata}
onTimeUpdate={handleTimeUpdate}
/>
<input
ref={sliderRef}
className={styles.audioPlayerInput}
type="range"
step="1"
defaultValue="0"
min="0"
max={`${audioRef?.current?.duration || 0}`}
onMouseDown={handleSlideStart}
onMouseUp={handleSlideEnd}
onTouchStart={handleSlideStart}
onTouchEnd={handleSlideEnd}
onChange={handleSlideChange}
/>
<div className="flex justify-between">
<div className="text-[12px] leading-[16.8px] text-[#ffffff66]">{curTime}</div>
<div className="text-[12px] leading-[16.8px] text-[#ffffff66]">{totalTime}</div>
</div>
</div>
);
});

@ -0,0 +1,25 @@
'use client';
import { useRouter } from 'next/navigation';
export default function EnterQueyueBtn() {
const router = useRouter();
const handleEnterQueyue = () => {
const result = confirm('离开微信,打开第三方应用');
if (result) {
alert('打开应用无效跳转下载页');
window.location.href = 'queyue://'; // 打开某手机上的某个app应用
setTimeout(function () {
router.push('/download');
}, 500);
}
};
return (
<button
className="w-[283px] h-[44px] flex items-center justify-center bg-[#B44343] font-medium text-[15px] rounded-full"
onClick={handleEnterQueyue}
>
</button>
);
}

@ -0,0 +1,68 @@
'use client';
import { useState, useRef, useEffect } from 'react';
import Image from 'next/image';
import AudioPlayer from '@/components/AudioPlayer';
import type { IAudioPlayerRef } from '@/components/AudioPlayer';
export default function MusicPanel(props: { musicInfo: ISong }) {
const { musicInfo } = props || {};
const audioPlayerRef = useRef<IAudioPlayerRef>(null);
const [playing, setPlaying] = useState(false);
const togglePlay = () => {
setPlaying(!playing);
if (playing) {
audioPlayerRef.current?.audio?.pause();
} else {
audioPlayerRef.current?.audio?.play();
}
};
useEffect(() => {
setPlaying(!!!audioPlayerRef.current?.audio?.paused);
}, []);
return (
<>
<div className="relative w-[100%] aspect-square mt-[12px]">
<Image className="rounded-[6px] object-cover" unoptimized fill src={musicInfo?.pic} alt="cover" />
{playing ? (
<Image
className="absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] w-[60px] h-[60px]"
width={60}
height={60}
unoptimized
src="/img/icon_pause_w.png"
alt="pause"
onClick={togglePlay}
/>
) : (
<Image
className="absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] w-[60px] h-[60px]"
width={60}
height={60}
unoptimized
src="/img/icon_play_w.png"
alt="play"
onClick={togglePlay}
/>
)}
</div>
<div className="w-full mt-[20px]">
<div className="text-[12px] h-[22px] w-[fit-content] flex items-center text-white rounded-full px-[8px] py-[0] bg-[#2b2b2c]">
VOL&nbsp;{musicInfo?.journalNo}
</div>
</div>
<h5 className="mt-[9px] font-medium text-[20px] leading-[28px] w-full">{musicInfo?.title}</h5>
<p className="mt-[3px] text-[12px] text-[#ffffffb2] w-full">{musicInfo?.artist}</p>
<div className="w-full mt-[18px] mb-[46px]">
<AudioPlayer ref={audioPlayerRef} src={musicInfo?.src} />
</div>
</>
);
}

@ -1,15 +1,25 @@
'use client';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
export default function SongItem(props: { data: ISong }) {
const { pic, title, artist } = props?.data || {};
const { id, pic, title, artist } = props?.data || {};
const router = useRouter();
const handleClick = () => {
// TODO:
router.push(`/music?id=${id}`);
};
return (
<div className="flex items-center py-[12px]" onClick={handleClick}>
<Image className="w-[48px] h-[48px]" width={48} height={48} unoptimized src={pic} alt="歌曲封面" />
<div className="flex items-center py-[12px]">
<Image
className="w-[48px] h-[48px] rounded-[3px]"
width={48}
height={48}
unoptimized
src={pic}
alt="歌曲封面"
onClick={handleClick}
/>
<div className="flex flex-1 flex-col pl-[15px] pr-[15px] overflow-hidden">
<div className="font-medium text-[15px] leading-[21px] mb-[2px] text-[#000000f2] truncate">{title}</div>
<div className="text-[12px] leading-[16.8px] text-[#000000b2] truncate">{artist}</div>

@ -0,0 +1,11 @@
export const formatTime = (seconds = 0) => {
let h: any = Math.floor(seconds / 3600);
let m: any = Math.floor((seconds % 3600) / 60);
let s: any = Math.floor(seconds % 60);
if (h > 0) {
h = h < 10 ? '0' + h : h;
}
m = m < 10 ? '0' + m : m;
s = s < 10 ? '0' + s : s;
return (h > 0 ? h + ':' : '') + m + ':' + s;
};
Loading…
Cancel
Save