Skip to content
Snippets Groups Projects
Commit 3cf07c8c authored by 杭 羽凡's avatar 杭 羽凡
Browse files

Merge remote-tracking branch 'origin/main'

# Conflicts:
#	src/components/MusicAlbumView.vue
#	src/views/HomePage.vue
parents f7a531c2 7fca645f
No related branches found
No related tags found
No related merge requests found
......@@ -18,13 +18,30 @@ export const getPlaylistsByUser = (userInfo) => {
/*
// TODO - modified
- user_name: string
playlist_name: string
- playlist_name: string
+ user_id: number
+ playlist_description: string
- playlist_description: string
*/
export const createPlaylist = (playlistCreateInfo) => {
return axios.post(`${PLAYLIST_MODULE}/create`, playlistCreateInfo,
{ headers: { 'Content-Type': 'application/json' } })
console.log(playlistCreateInfo)
return axios.post(`${PLAYLIST_MODULE}/create`, null,
{ params: playlistCreateInfo })
.then(res => {
return res;
});
}
/*
// TODO - modified
+ id: number
+ title: string
+ description: string
+ picPath: string
*/
export const modifyPlaylist = (playlistModifyInfo) => {
console.log(playlistModifyInfo)
return axios.post(`${PLAYLIST_MODULE}/modify`, playlistModifyInfo,
{headers: {'Content-Type': 'application/json'}})
.then(res => {
return res;
});
......
......@@ -26,4 +26,15 @@ export const removeSongFromPlaylist = (removeSongInfo) => {
.then(res => {
return res;
});
}
/*
// TODO - newly added
+ episode_id: number
*/
export const getSongsByEpisode = (episodeInfo) => {
return axios.get(`${SONG_MODULE}/fetchByEpisode`, { params: episodeInfo })
.then(res => {
return res;
});
}
\ No newline at end of file
src/assets/icons/disk.png

10.4 KiB

<script setup>
import {computed, nextTick, onMounted, onUnmounted, ref, watch} from "vue";
import playButton from "../icon/playButton.vue";
import dots from "../icon/dots.vue";
import checkMark from "../icon/checkMark.vue";
import {ElMessage, ElPopover} from "element-plus";
import {backgroundColor, updateBackground} from "../utils/getBackgroundColor";
import pauseButton from "../icon/pauseButton.vue";
import {removePlaylist, removeSongFromPlaylist} from "../api/playlist";
const emit = defineEmits();
const props = defineProps({
episodeInfo: { // 类型 :id,title,artist,picPath,createTime,songNum
type: Object,
required: true,
},
musicList: { // 类型 :id,title,artist,picPath,upload_time
type: Object,
required: true,
},
playFromLeftBar: null,
currentSongId: Number
});
const gradientColor = computed(() => `linear-gradient(to bottom, ${backgroundColor.value} , #1F1F1F 50%)`)
let musicHoveredIndex = ref(null);
let musicClickedIndex = ref(null);
let musicPlayIndex = ref(null);
let musicPauseIndex = ref(null);
const resizeObserver = ref(null)
// 放缩时的组件处理
const handleResize = () => {
const albums = document.querySelectorAll(".music-album-info");
const albumText = document.querySelectorAll(".album-text");
const albumContent = document.querySelector(".album-content");
// if (window.innerWidth > 0)
// 专辑隐藏
console.log(albumContent.clientWidth);
if (albumContent.clientWidth < 605) {
albums.forEach(album => {
album.style.visibility = "hidden";
});
albumText.forEach(album => {
album.style.visibility = "hidden";
});
} else {
albums.forEach(album => {
album.style.visibility = "visible";
});
albumText.forEach(album => {
album.style.visibility = "visible";
});
}
const albumImage = document.querySelector(".album-image");
const headerAlbumName = document.querySelector(".header-album-name");
// 歌单图片和文字缩放
if (albumContent.clientWidth < 420) {
albumImage.style.width = "120px";
albumImage.style.height = "120px";
headerAlbumName.style.fontSize = "40px";
headerAlbumName.style.marginBottom = "20px";
} else {
albumImage.style.width = "160px";
albumImage.style.height = "160px";
headerAlbumName.style.fontSize = "80px";
headerAlbumName.style.marginBottom = "35px";
}
//🙏
const fixedTipArea = document.querySelector(".fixed-tips");
const fixedPlayArea = document.querySelector(".fixed-play-area");
fixedPlayArea.style.width = (albumContent.clientWidth - 20) + "px";
fixedTipArea.style.width = (albumContent.clientWidth - 16) + "px";
}
const debounce = (fn, delay) => {
let timer
return (...args) => {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn(...args)
}, delay)
}
}
onMounted(() => {
resizeObserver.value = new ResizeObserver(debounce(handleResize, 50));
console.log(resizeObserver.value)
nextTick(() => {
const albumContent = document.querySelector(".album-content");
if (albumContent) {
resizeObserver.value.observe(albumContent);
}
})
})
onUnmounted(() => {
if (resizeObserver.value) {
resizeObserver.value.disconnect();
}
popovers.value = null;
})
const handelScroll = (event) => {
const playArea = document.querySelector(".play-area");
const fixedPlayArea = document.querySelector(".fixed-play-area");
const tipArea = document.querySelector(".tips");
const fixedTipArea = document.querySelector(".fixed-tips");
const albumContent = document.querySelector(".album-content");
const offsetHeight = albumContent.offsetTop;
const stickyPlayY = playArea.offsetTop - offsetHeight;
const stickyTipY = tipArea.offsetTop - offsetHeight;
const curOffset = offsetHeight - albumContent.getBoundingClientRect().top;
console.log(stickyPlayY, stickyTipY);
if (curOffset >= stickyPlayY) {
fixedPlayArea.style.opacity = "1";
fixedPlayArea.style.top = offsetHeight + "px";
fixedPlayArea.style.width = (albumContent.clientWidth - 20) + "px";
} else {
fixedPlayArea.style.opacity = "0";
}
if (curOffset + fixedPlayArea.scrollHeight >= stickyTipY) {
fixedTipArea.style.display = "flex";
fixedTipArea.style.top = offsetHeight + fixedPlayArea.scrollHeight + 'px';
} else {
fixedTipArea.style.display = "none";
}
}
window.onscroll = () => {
const playArea = document.querySelector(".play-area");
const fixedPlayArea = document.querySelector(".fixed-play-area");
const tipArea = document.querySelector(".tips");
const fixedTipArea = document.querySelector(".fixed-tips");
const stickyPlayY = playArea.offsetTop;
const stickyTipY = tipArea.offsetTop;
if (window.scrollY >= stickyPlayY) {
fixedPlayArea.style.opacity = "1";
} else {
fixedPlayArea.style.opacity = "0";
}
if (window.scrollY + fixedPlayArea.scrollHeight >= stickyTipY) {
fixedTipArea.style.display = "flex";
fixedTipArea.style.top = fixedPlayArea.scrollHeight + 'px';
} else {
fixedTipArea.style.display = "none";
}
}
watch(props.playFromLeftBar, () => {
playFromId(props.playFromLeftBar)
})
const popovers = ref([])
const getPopoverIndex = (popover) => {
if (popover) {
popovers.value.push(popover);
}
}
const closePopover = (e) => {
popovers.value.forEach((item) => {
item.hide();
})
}
//TODO:
const enterArtistSpace = () => {
}
const removeAlbum = (albumId) => {
removePlaylist({
playlist_id: albumId,
}).then(res => {
console.log("Hi")
})
}
const playFromId = (musicId) => {
if (musicId === null) {
// 从头开始播放
musicPlayIndex.value = props.musicList[0].id;
} else {
musicPlayIndex.value = musicId;
}
emit('switchSongs', props.albumInfo, musicPlayIndex.value);
musicPauseIndex = null;
}
const addToFavorite = (musicId) => {
}
const removeMusicFromAlbum = (albumId, songId) => {
removeSongFromPlaylist({
playlist_id: albumId,
song_id: songId,
})
}
const enterMusicDescription = (musicId) => {
}
const enterAuthorDescription = (authorName) => {
}
const pauseMusic = (musicId) => {
musicPauseIndex = musicId;
}
const addRecommendMusic = (musicId) => {
console.log(musicId);
//TODO:添加歌曲到指定的歌单
ElMessage({
message: "添加至: " + props.albumInfo.title,
grouping: true,
type: 'info',
offset: 16,
customClass: "reco-message",
duration: 4000,
})
}
</script>
<template>
<div class="album-content" :style="{backgroundImage: gradientColor}" @mousewheel="handelScroll">
<div class="header">
<img :src="episodeInfo.picPath" alt="" class="album-image" @load="updateBackground(episodeInfo.picPath)"/>
<div class="header-content">
<p style="text-align: left;margin:20px 0 0 15px">专辑</p>
<p class="header-album-name" style="font-weight: bolder;font-size:80px;margin:10px 0 35px 10px;">
{{ episodeInfo.title }}</p>
<div class="header-content-detail">
<p class="header-creator" @click="enterArtistSpace">{{ episodeInfo.artist }}</p>
<p style="padding-right: 6px "></p>
<p v-if="episodeInfo.createTime !== undefined">
{{ episodeInfo.createTime.substring(0, 10) }} </p>
<p style="padding: 0 2px 0 6px"></p>
<p style="margin-left:6px">{{ musicList.length }} 首歌曲</p>
</div>
</div>
</div>
<div class="content">
<div class="play-area">
<div class="play-button">
<play-button v-if="musicPlayIndex===null||musicPauseIndex!==null"
@click="playFromId(musicPauseIndex)"
style="position: absolute; top:20%;left:24%;color: #000000"/>
<pause-button v-if="musicPlayIndex!==null&&musicPauseIndex===null"
@click="pauseMusic(musicPlayIndex)"
style="position: absolute; top:24%;left:25%;color: #000000"/>
</div>
<!-- :ref中函数执行时,组件已经渲染好,并将本组件作为参数传入函数-->
<el-popover
style="border-radius: 10px"
:width="400"
trigger="click"
popper-class="my-popover"
:ref="getPopoverIndex"
:hide-after=0>
<template #reference>
<dots class="more-info"/>
</template>
<ul @click="closePopover">
<li @click="">删除歌单</li>
</ul>
</el-popover>
</div>
<div class="fixed-play-area" :style="{background :`${backgroundColor}`}">
<div class="opacity-wrapper">
<div class="play-button">
<play-button v-if="musicPlayIndex===null||musicPauseIndex!==null"
@click="playFromId(musicPauseIndex)"
style="position: absolute; top:20%;left:24%;color: #000000"/>
<pause-button v-if="musicPlayIndex!==null&&musicPauseIndex===null"
@click="pauseMusic(musicPlayIndex)"
style="position: absolute; top:24%;left:25%;color: #000000"/>
</div>
<p style="padding-left: 10px;font-weight: bold;font-size: 26px">{{ episodeInfo.title }}</p>
</div>
</div>
<div class="tips">
<p style="position:absolute; left:45px">#</p>
<p style="position:absolute; left:140px">标题</p>
<p style="margin-left: auto; margin-right:55px">时间</p>
</div>
<div class="fixed-tips">
<p style="position:absolute; left:45px">#</p>
<p style="position:absolute; left:140px">标题</p>
<p style="margin-left: auto; margin-right:75px">时间</p>
</div>
<div class="musicList">
<div class="music-item"
v-for="music in musicList"
:key="music.id"
:aria-selected="musicClickedIndex === music.id ? 'True':'False'"
@mouseenter="()=>{musicHoveredIndex = music.id;}"
@mouseleave="()=>{musicHoveredIndex = -1}"
@click="musicClickedIndex=music.id"
@dblclick="playFromId(music.id)"
:style="{backgroundColor: musicClickedIndex===music.id? '#404040':
musicHoveredIndex === music.id ? 'rgba(54,54,54,0.7)' :'rgba(0,0,0,0)',
}"> <!--@click事件写在script中的函数里 无法及时触发:style中的样式!!!-->
<div
:style="{visibility: musicHoveredIndex === music.id||musicPlayIndex === music.id ? 'hidden' : 'visible' }">
{{
musicList.indexOf(music) + 1
}}
</div>
<play-button @click="playFromId(music.id)" style="position: absolute;left: 14px;cursor: pointer"
v-if="(musicHoveredIndex === music.id&&musicPlayIndex!==music.id)||musicPauseIndex===music.id"
:style="{color: musicPauseIndex===music.id ? '#1ed660' : 'white'}"/>
<pause-button @click="pauseMusic(music.id)"
style="color:#1ed660 ;position: absolute;left: 17px;cursor: pointer"
v-if="musicPlayIndex===music.id&&musicHoveredIndex === music.id&&musicPauseIndex!==music.id"/>
<img width="17" height="17" alt=""
style="position: absolute;left: 24px;"
v-if="musicPlayIndex===music.id&&musicHoveredIndex !== music.id&&musicPauseIndex!==music.id"
src="https://open.spotifycdn.com/cdn/images/equaliser-animated-green.f5eb96f2.gif">
<div class="music-detailed-info">
<img class="music-image"
:src="music.picPath"
alt="歌曲图片"/>
<div class="music-name-author" style="padding-left: 5px;">
<p @click="enterMusicDescription(music.id)" class="music-name"
:style="{color : musicPlayIndex ===music.id? '#1ED660':''}"
:class="[musicPlayIndex === music.id ? 'music-after-click' : '']"
>{{ music.title }}</p>
<p @click="enterAuthorDescription(music.artist)" class="music-author"
:style="{color:musicHoveredIndex === music.id? 'white' : '#b2b2b2'}">
{{ music.artist }}</p>
</div>
</div>
<div class="music-right-info">
<el-popover
:ref="getPopoverIndex"
class="music-dropdown-options"
popper-class="my-popover"
:width="400"
trigger="click"
:hide-after=0
>
<template #reference>
<check-mark class="check-mark"
:style="{visibility: musicHoveredIndex === music.id ? 'visible' : 'hidden'}"/>
</template>
<ul @click="closePopover">
<!-- TODO: 这里需要所有的歌单-->
<li @click="addToFavorite(music.id)"></li>
</ul>
</el-popover>
<div style="margin-left: auto;margin-right: 15px; color: #b2b2b2"
:style="{color:musicHoveredIndex === music.id? 'white' : '#b2b2b2'}">{{
music.upload_time
}}
<!--TODO: 解决播放时间问题-->
</div>
<el-popover
:ref="getPopoverIndex"
class="music-dropdown-options"
popper-class="my-popover"
:width="400"
trigger="click"
:hide-after=0
>
<template #reference>
<dots class="music-more-info"/>
</template>
<ul @click="closePopover">
<li @click="removeMusicFromAlbum(music.id)">删除歌曲</li>
</ul>
</el-popover>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
li {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
li:hover {
background-color: #363636;
border-radius: 12px;
}
p {
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin: 0;
}
.header, .play-area, .tips, .musicList {
z-index: 0;
padding: 20px;
width: 100%;
box-sizing: border-box;
user-select: none;
}
.album-content {
margin: 0;
padding: 0;
color: white;
background-color: rgb(31, 31, 31);
transition: background-color ease 0.6s;
display: flex;
flex-direction: column;
width: 100%;
overflow-x: auto; /*千万不能删,不然背景黑一半*/
}
.header {
display: flex;
flex-direction: row;
}
.content {
z-index: 1;
background-color: rgba(0, 0, 0, 0.20);
}
.album-image {
border-radius: 6%;
width: 160px;
height: 160px;
margin-top: 30px;
margin-left: 15px;
margin-right: 20px;
}
.header-content {
display: flex;
flex-direction: column;
height: 100%;
position: relative;
flex-grow: 1;
min-width: 0;
}
.header-content-detail {
padding: 10px;
align-items: center;
display: flex;
flex-direction: row;
}
.header-creator {
margin: 0 6px;
cursor: pointer;
font-weight: bolder
}
.header-creator:hover {
text-decoration: underline;
}
.play-area {
position: relative;
}
.more-info {
font-size: 35px;
position: absolute;
z-index: 13;
top: 33px;
left: 160px;
transition: width 0.1s ease-in-out;
}
.more-info:focus {
outline: none;
}
.more-info:hover {
cursor: pointer;
transform: scale(1.15);
}
.fixed-play-area {
top: 0;
z-index: 11;
opacity: 0;
transition: opacity 0.2s ease-out;
border-radius: 12px 12px 0 0;
position: fixed; /**/
display: flex;
padding: 10px 0 10px 20px;
width: inherit;
}
.opacity-wrapper {
display: flex;
align-items: center;
width: 100%;
height: 100%;
margin: -10px 0 -10px -20px;
padding: 10px 0 10px 20px;
background-color: rgba(0, 0, 0, 0.50);
}
.play-button {
margin-left: 40px;
background-color: #1ed660;
border-radius: 50%;
width: 60px;
height: 60px;
position: relative;
align-items: center;
justify-content: center;
transition: all 0.1s;
}
.play-button:hover {
cursor: pointer;
transform: scale(1.05);
background-color: #1ed683;
}
.tips {
z-index: 0;
display: flex;
position: relative;
padding: 5px 8px 5px 8px;
}
.fixed-tips {
z-index: 11;
width: 100%;
justify-content: space-between;
display: none;
padding: 10px 8px 10px 8px;
position: fixed;
background-color: #1f1f1f;
border-bottom: 1px solid #363636;
}
.musicList {
border-top: 1px solid #363636;
margin-top: 10px;
}
/*每行音乐的样式*/
.music-item {
position: relative;
border-radius: 10px;
display: flex;
align-items: center;
padding: 10px 0 10px 25px;
flex-grow: 1;
min-width: 0;
}
/*音乐被点击后的样式*/
.music-after-click {
color: #1ed660;
}
/*左侧信息*/
.music-detailed-info {
display: flex;
flex-direction: row;
}
.music-image {
padding-left: 30px;
height: 50px;
width: 50px; /* Adjust as needed */
border-radius: 10px;
}
.music-name {
padding-bottom: 5px;
font-size: 18px
}
.music-name:hover {
cursor: pointer;
text-decoration: underline;
}
.music-author {
color: #b2b2b2;
font-size: 15px
}
.music-author:hover {
cursor: pointer;
text-decoration: underline;
}
/*右侧信息*/
.music-right-info {
margin-left: auto;
display: flex;
align-items: center;
flex-direction: row;
}
.check-mark {
width: 20px;
height: auto;
margin-right: 40px;
color: black;
font-weight: bolder;
border-radius: 50%;
}
.check-mark:hover {
cursor: pointer;
}
.check-mark:focus {
outline: none;
}
.music-more-info {
margin-right: 14px;
font-size: 22px;
transition: width 0.1s ease-in-out;
}
.music-more-info:focus {
outline: none;
transform: scale(1.05);
}
.music-more-info:hover {
cursor: pointer;
}
.music-dropdown-options {
border-radius: 6px;
}
ul {
background-color: #282828;
list-style-type: none;
padding: 0;
margin: 0;
border-radius: 10px;
}
li {
color: white;
padding: 15px 12px;
}
li:hover {
cursor: pointer;
text-decoration: underline;
}
</style>
<script setup>
import {useRouter} from "vue-router";
import {ref} from "vue";
import {searchSongByKeyword, searchPlaylistByKeyword} from "../api/search";
import {searchPlaylistByKeyword, searchSongByKeyword} from "../api/search";
const router = useRouter();
const emit = defineEmits(['headData']);
const emit = defineEmits(['headData','home']);
const props = defineProps({
allowSearch: {
......@@ -54,7 +54,7 @@ function callSearch() {
songResult.value = [];
playlistResult.value = [];
showSearch.value = true
searchSongByKeyword({
keyword: searchInput.value
}).then(res => {
......@@ -80,6 +80,10 @@ function callSearch() {
console.log("Failed to fetch playlists!")
})
}
function callHome() {
emit('home');
}
</script>
<template>
......@@ -91,11 +95,12 @@ function callSearch() {
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="feather feather-search"
@click="callSearch">
@click="callSearch">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<input type="text" v-model="searchInput" :placeholder="searchPlaceHolders[Math.floor(Math.random() * 5)]" @keyup.enter="callSearch"/>
<input type="text" v-model="searchInput" :placeholder="searchPlaceHolders[Math.floor(Math.random() * 5)]"
@keyup.enter="callSearch"/>
</div>
<div style="display: flex; flex-direction: row">
......@@ -105,14 +110,14 @@ function callSearch() {
d="M480 480V128a32 32 0 0 1 64 0v352h352a32 32 0 1 1 0 64H544v352a32 32 0 1 1-64 0V544H128a32 32 0 0 1 0-64z"></path>
</svg>
</router-link>
<router-link to="/home" class="home-btn">
<div @click="callHome" class="home-btn">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="feather feather-home">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
<polyline points="9 22 9 12 15 12 15 22"></polyline>
</svg>
</router-link>
</div>
<div @click="toggleIcons" class="more-btn">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path fill="currentColor"
......@@ -166,9 +171,9 @@ function callSearch() {
margin-right: 10px;
color: #fff;
text-decoration: none;
cursor: pointer;
border-radius: 50%;
transition: width, color, background-color ease-in-out 0.2s;
cursor: pointer;
border-radius: 50%;
transition: width, color, background-color ease-in-out 0.2s;
}
.home-btn svg {
......@@ -178,22 +183,22 @@ function callSearch() {
}
.home-btn:hover {
transform: translateY(-2px);
transform: translateY(-2px);
}
.home-btn:hover::after {
content: '回到主页';
position: absolute;
top: 35px;
left: 50%;
transform: translateX(-50%);
background-color: #282828;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 14px;
white-space: nowrap;
z-index: 1000;
content: '回到主页';
position: absolute;
top: 35px;
left: 50%;
transform: translateX(-50%);
background-color: #282828;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 14px;
white-space: nowrap;
z-index: 1000;
}
.manage-btn {
......@@ -202,9 +207,9 @@ function callSearch() {
margin-right: 10px;
color: #fff;
text-decoration: none;
cursor: pointer;
border-radius: 50%;
transition: width, color, background-color ease-in-out 0.2s;
cursor: pointer;
border-radius: 50%;
transition: width, color, background-color ease-in-out 0.2s;
}
.manage-btn svg {
......@@ -214,25 +219,25 @@ function callSearch() {
}
.manage-btn:hover {
transform: translateY(-2px);
transform: translateY(-2px);
}
/*
添加悬停样式
*/
.manage-btn:hover::after {
content: '增加歌曲';
position: absolute;
top: 35px;
left: 50%;
transform: translateX(-50%);
background-color: #282828;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 14px;
white-space: nowrap;
z-index: 1000;
content: '增加歌曲';
position: absolute;
top: 35px;
left: 50%;
transform: translateX(-50%);
background-color: #282828;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 14px;
white-space: nowrap;
z-index: 1000;
}
.search-box {
......@@ -279,39 +284,39 @@ input[type="text"]:focus {
}
.more-btn {
display: flex;
align-items: center;
margin-right: 10px;
color: #fff;
text-decoration: none;
cursor: pointer;
border-radius: 50%;
transition: width, color, background-color ease-in-out 0.2s;
display: flex;
align-items: center;
margin-right: 10px;
color: #fff;
text-decoration: none;
cursor: pointer;
border-radius: 50%;
transition: width, color, background-color ease-in-out 0.2s;
}
.more-btn svg {
width: 32px;
height: 32px;
margin-right: 10px;
width: 32px;
height: 32px;
margin-right: 10px;
}
.more-btn:hover {
transform: scale(1.1);
transform: scale(1.1);
}
.more-btn:hover::after {
content: '更多';
position: absolute;
top: 32px;
left: 38%;
transform: translateX(-50%);
background-color: #282828;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 14px;
white-space: nowrap;
z-index: 1000;
content: '更多';
position: absolute;
top: 32px;
left: 38%;
transform: translateX(-50%);
background-color: #282828;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 14px;
white-space: nowrap;
z-index: 1000;
}
.role-icon {
......@@ -319,9 +324,9 @@ input[type="text"]:focus {
opacity: 0;
transform: translateX(-20px); /* 初始位置稍微偏左 */
animation: slideIn 0.5s forwards;
cursor: pointer;
border-radius: 50%;
transition: width, color, background-color ease-in-out 0.2s;
cursor: pointer;
border-radius: 50%;
transition: width, color, background-color ease-in-out 0.2s;
}
.role-icon svg {
......@@ -332,95 +337,95 @@ input[type="text"]:focus {
}
.role-icon svg:hover {
transform: translateY(-2px);
transform: translateY(-2px);
}
.role-icon:hover::after {
content: '个人主页';
position: absolute;
top: 35px;
left: 50%;
transform: translateX(-50%);
background-color: #282828;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 14px;
white-space: nowrap;
z-index: 1000;
content: '个人主页';
position: absolute;
top: 35px;
left: 50%;
transform: translateX(-50%);
background-color: #282828;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 14px;
white-space: nowrap;
z-index: 1000;
}
.set-icon {
margin: 10px 0;
opacity: 0;
transform: translateX(-20px); /* 初始位置稍微偏左 */
animation: slideIn 0.5s forwards;
cursor: pointer;
border-radius: 50%;
transition: width, color, background-color ease-in-out 0.2s;
margin: 10px 0;
opacity: 0;
transform: translateX(-20px); /* 初始位置稍微偏左 */
animation: slideIn 0.5s forwards;
cursor: pointer;
border-radius: 50%;
transition: width, color, background-color ease-in-out 0.2s;
}
.set-icon svg {
color: #d5d5d5;
width: 32px;
height: 32px;
margin-right: 10px;
color: #d5d5d5;
width: 32px;
height: 32px;
margin-right: 10px;
}
.set-icon svg:hover {
transform: translateY(-2px);
transform: translateY(-2px);
}
.set-icon:hover::after {
content: '设置';
position: absolute;
top: 35px;
left: 38%;
transform: translateX(-50%);
background-color: #282828;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 14px;
white-space: nowrap;
z-index: 1000;
content: '设置';
position: absolute;
top: 35px;
left: 38%;
transform: translateX(-50%);
background-color: #282828;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 14px;
white-space: nowrap;
z-index: 1000;
}
.exit-icon {
margin: 10px 0;
opacity: 0;
transform: translateX(-20px); /* 初始位置稍微偏左 */
animation: slideIn 0.5s forwards;
cursor: pointer;
border-radius: 50%;
transition: width, color, background-color ease-in-out 0.2s;
margin: 10px 0;
opacity: 0;
transform: translateX(-20px); /* 初始位置稍微偏左 */
animation: slideIn 0.5s forwards;
cursor: pointer;
border-radius: 50%;
transition: width, color, background-color ease-in-out 0.2s;
}
.exit-icon svg {
color: #d5d5d5;
width: 32px;
height: 32px;
margin-right: 10px;
color: #d5d5d5;
width: 32px;
height: 32px;
margin-right: 10px;
}
.exit-icon svg:hover {
color: red;
transform: translateY(-2px);
color: red;
transform: translateY(-2px);
}
.exit-icon:hover::after {
content: '退出';
color: red;
position: absolute;
top: 35px;
left: 38%;
transform: translateX(-50%);
background-color: #282828;
padding: 4px 8px;
border-radius: 4px;
font-size: 14px;
white-space: nowrap;
z-index: 1000;
content: '退出';
color: red;
position: absolute;
top: 35px;
left: 38%;
transform: translateX(-50%);
background-color: #282828;
padding: 4px 8px;
border-radius: 4px;
font-size: 14px;
white-space: nowrap;
z-index: 1000;
}
@keyframes slideIn {
......
......@@ -5,7 +5,7 @@ import musicAlbumClosed from "../icon/musicAlbumClosed.vue";
import searchIcon from "../icon/searchIcon.vue";
import plusIcon from "../icon/plusIcon.vue";
import playButton from "../icon/playButton.vue";
import {getPlaylistsByUser} from "../api/playlist";
import {createPlaylist, getPlaylistsByUser} from "../api/playlist";
import {ElPopover} from "element-plus";
const emit = defineEmits();
......@@ -28,39 +28,50 @@ const userToken = ref(JSON.parse(sessionStorage.getItem('user-token')));
const currentUserId = ref(userToken.value.id);
function toggleSideBar() {
isSideBarOpen = !isSideBarOpen;
sideBarWidth.value = isSideBarOpen ? criticalWidth : minWidth;
isSideBarOpen = !isSideBarOpen;
sideBarWidth.value = isSideBarOpen ? criticalWidth : minWidth;
}
function startResizing(event) {
event.preventDefault();
const initialWidth = sideBarWidth.value;
const initialMouseX = event.clientX;
const onMouseMove = (moveEvent) => {
sideBarWidth.value = initialWidth + (moveEvent.clientX - initialMouseX);
// 确保宽度不小于最小值
if (sideBarWidth.value <= criticalWidth) {
isSideBarOpen = false;
sideBarWidth.value = minWidth;
}
// 确保宽度不大于最大值
else if (sideBarWidth.value >= maximumWidth) {
sideBarWidth.value = maximumWidth;
} else
isSideBarOpen = true;
};
const onMouseUp = () => {
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
};
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
event.preventDefault();
const initialWidth = sideBarWidth.value;
const initialMouseX = event.clientX;
const onMouseMove = (moveEvent) => {
sideBarWidth.value = initialWidth + (moveEvent.clientX - initialMouseX);
// 确保宽度不小于最小值
if (sideBarWidth.value <= criticalWidth) {
isSideBarOpen = false;
sideBarWidth.value = minWidth;
}
// 确保宽度不大于最大值
else if (sideBarWidth.value >= maximumWidth) {
sideBarWidth.value = maximumWidth;
} else
isSideBarOpen = true;
};
const onMouseUp = () => {
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
};
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
}
function addAlbum() {
//TODO:添加新的空歌单
console.log("Hi")
createPlaylist({
user_id: currentUserId.value,
}).then(() => {
getPlaylistsByUser({
user_id: currentUserId.value
}).then(res => {
musicAlbums.value = res.data.result || []
}).catch(e => {
})
})
}
function toggleSearchBar() {
......@@ -94,7 +105,7 @@ onMounted(() => {
const popover1 = ref(null)
const closePopover = () => {
popover1.value.hide();
popover1.value.hide();
}
defineProps({
......@@ -113,11 +124,11 @@ defineProps({
</div>
<el-popover v-if="isSideBarOpen" class="dropdown-options"
ref="popover1"
ref="popover1"
:width="200"
trigger="click"
:hide-after=0
popper-class="left-popover">
popper-class="left-popover">
<template #reference>
<div class="add-album">
<plus-icon class="plus-icon"/>
......@@ -142,16 +153,16 @@ defineProps({
@mouseenter="()=>{hoverOnAlbum=true}"
@mouseleave="()=>{hoverOnAlbum=false}"
:style="{ scrollbarWidth : hoverOnAlbum? 'auto':'none'}"
>
>
<div v-if="musicAlbums !== undefined" v-for="album in musicAlbums"
:key="album.id"
@mouseenter="()=>{albumHoveredIndex = album.id}"
@mouseleave="()=>{albumHoveredIndex = null}"
:style="{backgroundColor: albumHoveredIndex === album.id ? '#1F1F1F' : '#171717' }"
@click="emit('setCurrentPlaylist', album);"
class="musicAlbum-item">
@click="emit('setCurrentPlaylist', album);"
class="musicAlbum-item">
<img
:src="album.picPath"
alt="playlist"
......@@ -163,7 +174,8 @@ defineProps({
<div class="musicAlbum-description">
<p style="padding-bottom: 5px;font-size: 18px">{{ album.title }}</p>
<p v-if="album.title !== '我喜欢的歌曲'" style="color: #b2b2b2;font-size: 13px">歌单 • {{ userToken.username }}</p>
<p v-if="album.title !== '我喜欢的歌曲'" style="color: #b2b2b2;font-size: 13px">歌单 •
{{ userToken.username }}</p>
<p v-else style="color: #b2b2b2;font-size: 13px">默认收藏夹</p>
</div>
</div>
......@@ -194,7 +206,7 @@ ul {
li {
color: white;
padding: 10px 12px;
border-radius: 10px;
border-radius: 10px;
}
li:hover {
......
......@@ -40,7 +40,7 @@ getSongsByPlaylist({
<div v-for="song in songs"
:key="song.id"
class="song">
<img :src="song.picPath" alt="">
<img class="recommend-song-img" :src="song.picPath" alt="">
<h2>{{ song.title }}</h2>
<p>{{ song.artist }}</p>
</div>
......@@ -126,4 +126,8 @@ getSongsByPlaylist({
background-color: #fff;
color: #000;
}
.recommend-song-img:hover {
transform: rotate(3deg) scale(1.02);
}
</style>
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
<script setup>
import {defineEmits, onMounted, ref} from "vue";
import musicAlbumOpened from "../icon/musicAlbumOpened.vue";
import musicAlbumClosed from "../icon/musicAlbumClosed.vue";
import searchIcon from "../icon/searchIcon.vue";
import plusIcon from "../icon/plusIcon.vue";
import playButton from "../icon/playButton.vue";
import {getPlaylistsByUser} from "../api/playlist";
import {ElPopover} from "element-plus";
const emit = defineEmits();
const musicAlbums = ref([]);
let albumHoveredIndex = ref(null);
let hoverOnAlbum = ref(false);
let isSideBarOpen = ref(true);
let sideBarWidth = ref(250);
let user = ref("HanG");
const criticalWidth = 180;
const maximumWidth = 310;
const minWidth = 75;
/*
USER
*/
const userToken = ref(JSON.parse(sessionStorage.getItem('user-token')));
const currentUserId = ref(userToken.value.id);
function toggleSideBar() {
isSideBarOpen = !isSideBarOpen;
sideBarWidth.value = isSideBarOpen ? criticalWidth : minWidth;
}
function startResizing(event) {
event.preventDefault();
const initialWidth = sideBarWidth.value;
const initialMouseX = event.clientX;
const onMouseMove = (moveEvent) => {
sideBarWidth.value = initialWidth + (moveEvent.clientX - initialMouseX);
// 确保宽度不小于最小值
if (sideBarWidth.value <= criticalWidth) {
isSideBarOpen = false;
sideBarWidth.value = minWidth;
}
// 确保宽度不大于最大值
else if (sideBarWidth.value >= maximumWidth) {
sideBarWidth.value = maximumWidth;
} else
isSideBarOpen = true;
};
const onMouseUp = () => {
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
};
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
}
function addAlbum() {
//TODO:添加新的空歌单
}
function toggleSearchBar() {
const input = document.querySelector(".search-input");
input.classList.toggle('active'); // 切换输入框的显示状态
input.focus(); // 点击搜索图标后聚焦输入框
}
function blurSearchBar() {
const input = document.querySelector(".search-input");
if (input.classList.contains('active'))
input.classList.remove('active');
}
function searchAlbum() {
const input = document.querySelector(".search-input").value;
// TODO:提交表单
}
onMounted(() => {
getPlaylistsByUser({
user_id: currentUserId.value
}).then(res => {
musicAlbums.value = res.data.result || []
}).catch(e => {
})
})
defineProps({
callParentFunction: Function
})
</script>
<template>
<nav class="main-menu resizable-box" :style="{ width: sideBarWidth + 'px'}">
<div class="top-control">
<div class="toggle-button" @click="toggleSideBar">
<music-album-opened v-if="isSideBarOpen"/>
<music-album-closed v-if="!isSideBarOpen"/>
<p style="margin-left: 15px;font:normal small-caps bold 20px Arial, sans-serif ;" v-if="isSideBarOpen">
音乐库</p>
</div>
<el-popover v-if="isSideBarOpen" class="dropdown-options"
:width="200"
trigger="click"
:hide-after=0>
<template #reference>
<div class="add-album">
<plus-icon class="plus-icon"/>
</div>
</template>
<ul>
<li @click="addAlbum">添加歌单</li>
</ul>
</el-popover>
</div>
<div class="search-container">
<div class="search-album" v-if="isSideBarOpen" @click="toggleSearchBar">
<search-icon class="search-icon"/>
</div>
<input type="text" class="search-input" placeholder="输入搜索歌单" @keydown.enter="searchAlbum"
@blur="blurSearchBar"/>
</div>
<div class="musicAlbums"
@mouseenter="()=>{hoverOnAlbum=true}"
@mouseleave="()=>{hoverOnAlbum=false}"
:style="{ scrollbarWidth : hoverOnAlbum? 'auto':'none'}">
<div v-if="musicAlbums !== undefined" v-for="album in musicAlbums"
:key="album.id"
@mouseenter="()=>{albumHoveredIndex = album.id}"
@mouseleave="()=>{albumHoveredIndex = null}"
:style="{backgroundColor: albumHoveredIndex === album.id ? '#1F1F1F' : '#171717' }"
class="musicAlbum-item">
<img
:src="album.picPath"
alt="playlist"
class="musicAlbum-image"
:style="{opacity:albumHoveredIndex === album.id ? 0.4 :1}"
/>
<play-button @click="emit('setCurrentPlaylist', album);" v-if="albumHoveredIndex === album.id"
class="play-button"/>
<div class="musicAlbum-description">
<p style="padding-bottom: 5px;font-size: 18px">{{ album.title }}</p>
<p v-if="album.title !== '我喜欢的歌曲'" style="color: #b2b2b2;font-size: 13px">歌单 • {{ userToken.username }}</p>
<p v-else style="color: #b2b2b2;font-size: 13px">默认收藏夹</p>
</div>
</div>
</div>
<div class="resizer" @mousedown="startResizing" :style="{left:(sideBarWidth+8) +'px'}"></div>
</nav>
</template>
<style scoped>
*,
*::before,
*::after {
box-sizing: border-box;
padding: 0;
margin: 0;
}
ul {
background-color: #282828;
list-style-type: none;
margin: 0;
border-radius: 10px;
padding: 12px 8px;
}
li {
color: white;
padding: 10px 12px;
}
li:hover {
cursor: pointer;
background-color: #404040;
text-decoration: underline;
}
nav {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
}
.main-menu.resizable-box {
/* position: relative; /*使得子元素的absotute是相对于该元素 这里不设置,位置通过js控制 不然的话resizer会被gap遮蔽*/
display: flex;
flex-direction: column;
border: none;
border-radius: 12px;
padding: 12px 0 20px;
overflow: hidden;
font-family: inherit;
background-color: #171717;
}
.top-control {
margin-top: 10px;
display: flex;
align-items: center;
justify-content: space-between;
}
.add-album {
color: #b2b2b2;
margin-top: 5px;
margin-right: 10px;
width: 30px;
cursor: pointer;
}
.search-container {
margin: 12px 10px;
display: flex;
align-items: center;
cursor: pointer;
}
.search-icon {
font-size: 24px;
transition: margin-right 0.3s ease;
}
.search-input {
background-color: #404040;
font-size: 15px;
color: #ffffff;
height: 35px;
border: none;
width: 0;
opacity: 0;
transition: width 0.3s ease, opacity 0.3s ease;
padding: 5px;
border-radius: 4px;
outline: none;
}
.search-input.active {
width: 70%; /* 设置输入框拉伸后的宽度 */
opacity: 1;
}
.search-album {
display: flex;
color: #b2b2b2;
width: 35px;
height: 35px;
cursor: pointer;
align-items: center;
justify-content: center;
}
.plus-icon {
border-radius: 50%;
transition: width, color, background-color ease-in-out 0.2s;
}
.plus-icon:hover {
transform: scale(1.1);
color: #efeeee;
background-color: #404040;
}
.search-icon {
padding: 3px;
border-radius: 50%;
transition: width, color, background-color ease-in-out 0.2s;
}
.search-icon:hover {
transform: scale(1.05);
color: #efeeee;
background-color: #404040;
}
.toggle-button:hover {
color: #efeeee;
}
.toggle-button {
display: flex;
align-items: center;
width: 120px;
margin: 0 0 0 16px;
background-color: transparent;
cursor: pointer;
transition: color 0.2s ease;
color: #b2b2b2;
}
.resizer {
position: absolute;
width: 7px;
background-color: transparent;
height: 92%;
top: 85px;
cursor: ew-resize; /* 水平调整光标 */
&::before {
opacity: 0;
position: absolute;
content: '';
left: 3px;
width: 1px;
height: 100%;
background-color: #9d9d9d;
transition: opacity 0.2s ease-in-out;
}
&:hover::before {
opacity: 1;
}
}
.musicAlbums {
height: 100%;
padding: 0 10px 0 10px;
overflow: auto;
/*默认情况 滚动条消失*/
}
/*滑动条*/
.musicAlbums::-webkit-scrollbar {
width: 12px; /* 滚动条宽度 */
}
.musicAlbums::-webkit-scrollbar:window-inactive {
}
.musicAlbums::-webkit-scrollbar-track {
background: transparent; /* 滚动条背景 */
}
.musicAlbums::-webkit-scrollbar-thumb {
background: #888; /* 滚动条颜色 */
border-radius: 10px; /* 圆角 */
}
.musicAlbums::-webkit-scrollbar-thumb:hover {
background-color: #555; /* 悬停时的颜色 */
}
.musicAlbum-item {
display: flex;
position: relative;
align-items: center;
border-radius: 10px;
padding: 10px 0;
color: #f6f6f6;
}
.musicAlbum-description {
text-align: left;
display: flex;
flex-direction: column;
}
.musicAlbum-image {
width: 55px; /* 调整图片大小 */
height: 55px; /* 调整图片大小 */
margin-right: 10px;
border-radius: 10px;
}
.play-button {
position: absolute;
top: 20px;
left: 10px;
color: #f1f1f1; /* 播放键的颜色 */
cursor: pointer; /* 鼠标移动到播放键上时显示为点击手型 */
transition: transform 0.3s ease;
}
.play-button:hover {
transform: scale(1.1);
}
.musicAlbum-description {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; /* 单行显示文本 */
}
</style>
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment