mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-24 08:07:23 +08:00
🦄 refactor: 重构整个项目 优化打包 修改后台服务为本地运行 添加更新版本检测功能
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div class="layout-page">
|
||||
<div id="layout-main" class="layout-main">
|
||||
<title-bar v-if="isElectron" />
|
||||
<div class="layout-main-page" :class="isElectron ? '' : 'pt-6'">
|
||||
<!-- 侧边菜单栏 -->
|
||||
<app-menu v-if="!isMobile" class="menu" :menus="menus" />
|
||||
<div class="main">
|
||||
<!-- 搜索栏 -->
|
||||
<search-bar />
|
||||
<!-- 主页面路由 -->
|
||||
<div class="main-content" :native-scrollbar="false">
|
||||
<router-view
|
||||
v-slot="{ Component }"
|
||||
class="main-page"
|
||||
:class="route.meta.noScroll && !isMobile ? 'pr-3' : ''"
|
||||
>
|
||||
<keep-alive :include="keepAliveInclude">
|
||||
<component :is="Component" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
<play-bottom height="5rem" />
|
||||
<app-menu v-if="isMobile" class="menu" :menus="menus" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 底部音乐播放 -->
|
||||
<play-bar v-if="isPlay" />
|
||||
</div>
|
||||
<install-app-modal></install-app-modal>
|
||||
<update-modal />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import InstallAppModal from '@/components/common/InstallAppModal.vue';
|
||||
import PlayBottom from '@/components/common/PlayBottom.vue';
|
||||
import homeRouter from '@/router/home';
|
||||
import { isElectron, isMobile } from '@/utils';
|
||||
import UpdateModal from '@/components/common/UpdateModal.vue';
|
||||
|
||||
const keepAliveInclude = computed(() =>
|
||||
homeRouter
|
||||
.filter((item) => {
|
||||
return item.meta.keepAlive;
|
||||
})
|
||||
.map((item) => {
|
||||
return item.name.charAt(0).toUpperCase() + item.name.slice(1);
|
||||
})
|
||||
);
|
||||
|
||||
const AppMenu = defineAsyncComponent(() => import('./components/AppMenu.vue'));
|
||||
const PlayBar = defineAsyncComponent(() => import('./components/PlayBar.vue'));
|
||||
const SearchBar = defineAsyncComponent(() => import('./components/SearchBar.vue'));
|
||||
const TitleBar = defineAsyncComponent(() => import('./components/TitleBar.vue'));
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const isPlay = computed(() => store.state.isPlay as boolean);
|
||||
const { menus } = store.state;
|
||||
const route = useRoute();
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('initializeSettings');
|
||||
store.dispatch('initializeTheme');
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-page {
|
||||
@apply w-screen h-screen overflow-hidden bg-light dark:bg-black;
|
||||
}
|
||||
|
||||
.layout-main {
|
||||
@apply w-full h-full relative text-gray-900 dark:text-white;
|
||||
}
|
||||
|
||||
.layout-main-page {
|
||||
@apply flex h-full;
|
||||
}
|
||||
|
||||
.menu {
|
||||
@apply h-full;
|
||||
}
|
||||
|
||||
.main {
|
||||
@apply overflow-hidden flex-1 flex flex-col;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
@apply flex-1 overflow-hidden;
|
||||
}
|
||||
|
||||
.main-page {
|
||||
@apply h-full;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
.main-content {
|
||||
height: calc(100vh - 146px);
|
||||
overflow: auto;
|
||||
display: block;
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- menu -->
|
||||
<div class="app-menu" :class="{ 'app-menu-expanded': isText }">
|
||||
<div class="app-menu-header">
|
||||
<div class="app-menu-logo" @click="isText = !isText">
|
||||
<img :src="icon" class="w-9 h-9" alt="logo" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="app-menu-list">
|
||||
<div v-for="(item, index) in menus" :key="item.path" class="app-menu-item">
|
||||
<router-link class="app-menu-item-link" :to="item.path">
|
||||
<i
|
||||
class="iconfont app-menu-item-icon"
|
||||
:style="iconStyle(index)"
|
||||
:class="item.meta.icon"
|
||||
></i>
|
||||
<span
|
||||
v-if="isText"
|
||||
class="app-menu-item-text ml-3"
|
||||
:class="isChecked(index) ? 'text-green-500' : ''"
|
||||
>{{ item.meta.title }}</span
|
||||
>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import icon from '@/assets/icon.png';
|
||||
|
||||
const props = defineProps({
|
||||
size: {
|
||||
type: String,
|
||||
default: '26px'
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '#aaa'
|
||||
},
|
||||
selectColor: {
|
||||
type: String,
|
||||
default: '#10B981'
|
||||
},
|
||||
menus: {
|
||||
type: Array as any,
|
||||
default: () => []
|
||||
}
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const path = ref(route.path);
|
||||
watch(
|
||||
() => route.path,
|
||||
async (newParams) => {
|
||||
path.value = newParams;
|
||||
}
|
||||
);
|
||||
|
||||
const isChecked = (index: number) => {
|
||||
return path.value === props.menus[index].path;
|
||||
};
|
||||
|
||||
const iconStyle = (index: number) => {
|
||||
const style = {
|
||||
fontSize: props.size,
|
||||
color: isChecked(index) ? props.selectColor : props.color
|
||||
};
|
||||
return style;
|
||||
};
|
||||
|
||||
const isText = ref(false);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-menu {
|
||||
@apply flex-col items-center justify-center transition-all duration-300 w-[100px] px-1;
|
||||
}
|
||||
|
||||
.app-menu-expanded {
|
||||
@apply w-[160px];
|
||||
.app-menu-item {
|
||||
@apply hover:bg-gray-100 dark:hover:bg-gray-800 rounded mr-4;
|
||||
}
|
||||
}
|
||||
|
||||
.app-menu-item-link,
|
||||
.app-menu-header {
|
||||
@apply flex items-center w-[200px] overflow-hidden ml-2 px-5;
|
||||
}
|
||||
|
||||
.app-menu-header {
|
||||
@apply ml-1;
|
||||
}
|
||||
|
||||
.app-menu-item-link {
|
||||
@apply mb-6 mt-6;
|
||||
}
|
||||
|
||||
.app-menu-item-icon {
|
||||
@apply transition-all duration-200 text-gray-500 dark:text-gray-400;
|
||||
|
||||
&:hover {
|
||||
@apply text-green-500 scale-105 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile {
|
||||
.app-menu {
|
||||
max-width: 100%;
|
||||
width: 100vw;
|
||||
position: relative;
|
||||
z-index: 999999;
|
||||
@apply bg-light dark:bg-black border-t border-gray-200 dark:border-gray-700;
|
||||
|
||||
&-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-list {
|
||||
@apply flex justify-between;
|
||||
}
|
||||
|
||||
&-item {
|
||||
&-link {
|
||||
@apply my-4 w-auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,330 @@
|
||||
<template>
|
||||
<n-drawer
|
||||
:show="musicFull"
|
||||
height="100%"
|
||||
placement="bottom"
|
||||
:style="{ background: currentBackground || background }"
|
||||
:to="`#layout-main`"
|
||||
>
|
||||
<div id="drawer-target">
|
||||
<div class="drawer-back"></div>
|
||||
<div
|
||||
class="music-img"
|
||||
:style="{ color: textColors.theme === 'dark' ? '#000000' : '#ffffff' }"
|
||||
>
|
||||
<n-image
|
||||
ref="PicImgRef"
|
||||
:src="getImgUrl(playMusic?.picUrl, '500y500')"
|
||||
class="img"
|
||||
lazy
|
||||
preview-disabled
|
||||
/>
|
||||
<div>
|
||||
<div class="music-content-name">{{ playMusic.name }}</div>
|
||||
<div class="music-content-singer">
|
||||
<span v-for="(item, index) in playMusic.ar || playMusic.song.artists" :key="index">
|
||||
{{ item.name
|
||||
}}{{ index < (playMusic.ar || playMusic.song.artists).length - 1 ? ' / ' : '' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="music-content">
|
||||
<n-layout
|
||||
ref="lrcSider"
|
||||
class="music-lrc"
|
||||
style="height: 60vh"
|
||||
:native-scrollbar="false"
|
||||
@mouseover="mouseOverLayout"
|
||||
@mouseleave="mouseLeaveLayout"
|
||||
>
|
||||
<div ref="lrcContainer">
|
||||
<div
|
||||
v-for="(item, index) in lrcArray"
|
||||
:id="`music-lrc-text-${index}`"
|
||||
:key="index"
|
||||
class="music-lrc-text"
|
||||
:class="{ 'now-text': index === nowIndex, 'hover-text': item.text }"
|
||||
@click="setAudioTime(index)"
|
||||
>
|
||||
<span :style="getLrcStyle(index)">{{ item.text }}</span>
|
||||
<div class="music-lrc-text-tr">{{ item.trText }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 无歌词 -->
|
||||
<div v-if="!lrcArray.length" class="music-lrc-text mt-40">
|
||||
<span>暂无歌词, 请欣赏</span>
|
||||
</div>
|
||||
</div>
|
||||
</n-layout>
|
||||
<!-- 时间矫正 -->
|
||||
<!-- <div class="music-content-time">
|
||||
<n-button @click="reduceCorrectionTime(0.2)">-</n-button>
|
||||
<n-button @click="addCorrectionTime(0.2)">+</n-button>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</n-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
import { onBeforeUnmount, ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
lrcArray,
|
||||
nowIndex,
|
||||
playMusic,
|
||||
setAudioTime,
|
||||
textColors,
|
||||
useLyricProgress
|
||||
} from '@/hooks/MusicHook';
|
||||
import { getImgUrl } from '@/utils';
|
||||
import { animateGradient, getHoverBackgroundColor, getTextColors } from '@/utils/linearColor';
|
||||
|
||||
// 定义 refs
|
||||
const lrcSider = ref<any>(null);
|
||||
const isMouse = ref(false);
|
||||
const lrcContainer = ref<HTMLElement | null>(null);
|
||||
const currentBackground = ref('');
|
||||
const animationFrame = ref<number | null>(null);
|
||||
const isDark = ref(false);
|
||||
|
||||
const props = defineProps({
|
||||
musicFull: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
background: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
// 歌词滚动方法
|
||||
const lrcScroll = (behavior = 'smooth') => {
|
||||
const nowEl = document.querySelector(`#music-lrc-text-${nowIndex.value}`);
|
||||
if (props.musicFull && !isMouse.value && nowEl && lrcContainer.value) {
|
||||
const containerRect = lrcContainer.value.getBoundingClientRect();
|
||||
const nowElRect = nowEl.getBoundingClientRect();
|
||||
const relativeTop = nowElRect.top - containerRect.top;
|
||||
const scrollTop = relativeTop - lrcSider.value.$el.getBoundingClientRect().height / 2;
|
||||
lrcSider.value.scrollTo({ top: scrollTop, behavior });
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedLrcScroll = useDebounceFn(lrcScroll, 200);
|
||||
|
||||
const mouseOverLayout = () => {
|
||||
isMouse.value = true;
|
||||
};
|
||||
const mouseLeaveLayout = () => {
|
||||
setTimeout(() => {
|
||||
isMouse.value = false;
|
||||
lrcScroll();
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
watch(nowIndex, () => {
|
||||
debouncedLrcScroll();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.musicFull,
|
||||
() => {
|
||||
if (props.musicFull) {
|
||||
nextTick(() => {
|
||||
lrcScroll('instant');
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 监听背景变化
|
||||
watch(
|
||||
() => props.background,
|
||||
(newBg) => {
|
||||
if (!newBg) {
|
||||
textColors.value = getTextColors();
|
||||
document.documentElement.style.setProperty(
|
||||
'--hover-bg-color',
|
||||
getHoverBackgroundColor(false)
|
||||
);
|
||||
document.documentElement.style.setProperty('--text-color-primary', textColors.value.primary);
|
||||
document.documentElement.style.setProperty('--text-color-active', textColors.value.active);
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentBackground.value) {
|
||||
if (animationFrame.value) {
|
||||
cancelAnimationFrame(animationFrame.value);
|
||||
}
|
||||
animationFrame.value = animateGradient(currentBackground.value, newBg, (gradient) => {
|
||||
currentBackground.value = gradient;
|
||||
});
|
||||
} else {
|
||||
currentBackground.value = newBg;
|
||||
}
|
||||
|
||||
textColors.value = getTextColors(newBg);
|
||||
isDark.value = textColors.value.active === '#000000';
|
||||
|
||||
document.documentElement.style.setProperty(
|
||||
'--hover-bg-color',
|
||||
getHoverBackgroundColor(isDark.value)
|
||||
);
|
||||
document.documentElement.style.setProperty('--text-color-primary', textColors.value.primary);
|
||||
document.documentElement.style.setProperty('--text-color-active', textColors.value.active);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 修改 useLyricProgress 的使用方式
|
||||
const { getLrcStyle: originalLrcStyle } = useLyricProgress();
|
||||
|
||||
// 修改 getLrcStyle 函数
|
||||
const getLrcStyle = (index: number) => {
|
||||
const colors = textColors.value || getTextColors;
|
||||
const originalStyle = originalLrcStyle(index);
|
||||
|
||||
if (index === nowIndex.value) {
|
||||
// 当前播放的歌词,使用渐变效果
|
||||
return {
|
||||
...originalStyle,
|
||||
backgroundImage: originalStyle.backgroundImage
|
||||
?.replace(/#ffffff/g, colors.active)
|
||||
.replace(/#ffffff8a/g, `${colors.primary}`),
|
||||
backgroundClip: 'text',
|
||||
WebkitBackgroundClip: 'text',
|
||||
color: 'transparent'
|
||||
};
|
||||
}
|
||||
|
||||
// 非当前播放的歌词,使用普通颜色
|
||||
return {
|
||||
color: colors.primary
|
||||
};
|
||||
};
|
||||
|
||||
// 组件卸载时清理动画
|
||||
onBeforeUnmount(() => {
|
||||
if (animationFrame.value) {
|
||||
cancelAnimationFrame(animationFrame.value);
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
lrcScroll
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@keyframes round {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.drawer-back {
|
||||
@apply absolute bg-cover bg-center;
|
||||
z-index: -1;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
}
|
||||
|
||||
.drawer-back.paused {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
#drawer-target {
|
||||
@apply top-0 left-0 absolute overflow-hidden rounded px-24 flex items-center justify-center w-full h-full pb-8;
|
||||
animation-duration: 300ms;
|
||||
|
||||
.music-img {
|
||||
@apply flex-1 flex justify-center mr-16 flex-col;
|
||||
max-width: 360px;
|
||||
max-height: 360px;
|
||||
.img {
|
||||
@apply rounded-xl w-full h-full shadow-2xl;
|
||||
}
|
||||
}
|
||||
|
||||
.music-content {
|
||||
@apply flex flex-col justify-center items-center relative;
|
||||
|
||||
&-name {
|
||||
@apply font-bold text-xl pb-1 pt-4;
|
||||
}
|
||||
|
||||
&-singer {
|
||||
@apply text-base;
|
||||
}
|
||||
}
|
||||
|
||||
.music-content-time {
|
||||
display: none;
|
||||
@apply flex justify-center items-center;
|
||||
}
|
||||
.music-lrc {
|
||||
background-color: inherit;
|
||||
width: 500px;
|
||||
height: 550px;
|
||||
mask-image: linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%);
|
||||
&-text {
|
||||
@apply text-2xl cursor-pointer font-bold px-2 py-4;
|
||||
transition: all 0.3s ease;
|
||||
background-color: transparent;
|
||||
|
||||
span {
|
||||
background-clip: text !important;
|
||||
-webkit-background-clip: text !important;
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
&-tr {
|
||||
@apply font-normal;
|
||||
opacity: 0.7;
|
||||
color: var(--text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.hover-text {
|
||||
&:hover {
|
||||
@apply font-bold opacity-100 rounded-xl;
|
||||
background-color: var(--hover-bg-color);
|
||||
|
||||
span {
|
||||
color: var(--text-color-active) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile {
|
||||
#drawer-target {
|
||||
@apply flex-col p-4 pt-8 justify-start;
|
||||
.music-img {
|
||||
display: none;
|
||||
}
|
||||
.music-lrc {
|
||||
height: calc(100vh - 260px) !important;
|
||||
width: 100vw;
|
||||
span {
|
||||
padding-right: 0px !important;
|
||||
}
|
||||
}
|
||||
.music-lrc-text {
|
||||
@apply text-xl text-center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.music-drawer {
|
||||
transition: none; // 移除之前的过渡效果,现在使用 JS 动画
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,561 @@
|
||||
<template>
|
||||
<!-- 展开全屏 -->
|
||||
<music-full ref="MusicFullRef" v-model:music-full="musicFullVisible" :background="background" />
|
||||
<!-- 底部播放栏 -->
|
||||
|
||||
<div
|
||||
class="music-play-bar"
|
||||
:class="
|
||||
setAnimationClass('animate__bounceInUp') + ' ' + (musicFullVisible ? 'play-bar-opcity' : '')
|
||||
"
|
||||
:style="{
|
||||
color: musicFullVisible
|
||||
? textColors.theme === 'dark'
|
||||
? '#000000'
|
||||
: '#ffffff'
|
||||
: store.state.theme === 'dark'
|
||||
? '#ffffff'
|
||||
: '#000000'
|
||||
}"
|
||||
>
|
||||
<div class="music-time custom-slider">
|
||||
<n-slider
|
||||
v-model:value="timeSlider"
|
||||
:step="1"
|
||||
:max="allTime"
|
||||
:min="0"
|
||||
:format-tooltip="formatTooltip"
|
||||
></n-slider>
|
||||
</div>
|
||||
<div class="play-bar-img-wrapper" @click="setMusicFull">
|
||||
<n-image
|
||||
:src="getImgUrl(playMusic?.picUrl, '500y500')"
|
||||
class="play-bar-img"
|
||||
lazy
|
||||
preview-disabled
|
||||
/>
|
||||
<div class="hover-arrow">
|
||||
<div class="hover-content">
|
||||
<!-- <i class="ri-arrow-up-s-line text-3xl" :class="{ 'ri-arrow-down-s-line': musicFullVisible }"></i> -->
|
||||
<i
|
||||
class="text-3xl"
|
||||
:class="musicFullVisible ? 'ri-arrow-down-s-line' : 'ri-arrow-up-s-line'"
|
||||
></i>
|
||||
<span class="hover-text">{{ musicFullVisible ? '收起' : '展开' }}歌词</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="music-content">
|
||||
<div class="music-content-title">
|
||||
<n-ellipsis class="text-ellipsis" line-clamp="1">
|
||||
{{ playMusic.name }}
|
||||
</n-ellipsis>
|
||||
</div>
|
||||
<div class="music-content-name">
|
||||
<n-ellipsis class="text-ellipsis" line-clamp="1">
|
||||
<span
|
||||
v-for="(artists, artistsindex) in playMusic.ar || playMusic.song.artists"
|
||||
:key="artistsindex"
|
||||
>{{ artists.name
|
||||
}}{{
|
||||
artistsindex < (playMusic.ar || playMusic.song.artists).length - 1 ? ' / ' : ''
|
||||
}}</span
|
||||
>
|
||||
</n-ellipsis>
|
||||
</div>
|
||||
</div>
|
||||
<div class="music-buttons">
|
||||
<div class="music-buttons-prev" @click="handlePrev">
|
||||
<i class="iconfont icon-prev"></i>
|
||||
</div>
|
||||
<div class="music-buttons-play" @click="playMusicEvent">
|
||||
<i class="iconfont icon" :class="play ? 'icon-stop' : 'icon-play'"></i>
|
||||
</div>
|
||||
<div class="music-buttons-next" @click="handleNext">
|
||||
<i class="iconfont icon-next"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="audio-button">
|
||||
<div class="audio-volume custom-slider">
|
||||
<div class="volume-icon" @click="mute">
|
||||
<i class="iconfont" :class="getVolumeIcon"></i>
|
||||
</div>
|
||||
<div class="volume-slider">
|
||||
<n-slider v-model:value="volumeSlider" :step="0.01" :tooltip="false" vertical></n-slider>
|
||||
</div>
|
||||
</div>
|
||||
<n-tooltip v-if="!isMobile" trigger="hover" :z-index="9999999">
|
||||
<template #trigger>
|
||||
<i class="iconfont" :class="playModeIcon" @click="togglePlayMode"></i>
|
||||
</template>
|
||||
{{ playModeText }}
|
||||
</n-tooltip>
|
||||
<n-tooltip v-if="!isMobile" trigger="hover" :z-index="9999999">
|
||||
<template #trigger>
|
||||
<i
|
||||
class="iconfont icon-likefill"
|
||||
:class="{ 'like-active': isFavorite }"
|
||||
@click="toggleFavorite"
|
||||
></i>
|
||||
</template>
|
||||
喜欢
|
||||
</n-tooltip>
|
||||
<n-tooltip v-if="isElectron" class="music-lyric" trigger="hover" :z-index="9999999">
|
||||
<template #trigger>
|
||||
<i
|
||||
class="iconfont ri-netease-cloud-music-line"
|
||||
:class="{ 'text-green-500': isLyricWindowOpen }"
|
||||
@click="openLyricWindow"
|
||||
></i>
|
||||
</template>
|
||||
歌词
|
||||
</n-tooltip>
|
||||
<n-popover
|
||||
trigger="click"
|
||||
:z-index="99999999"
|
||||
content-class="music-play"
|
||||
raw
|
||||
:show-arrow="false"
|
||||
:delay="200"
|
||||
arrow-wrapper-style=" border-radius:1.5rem"
|
||||
@update-show="scrollToPlayList"
|
||||
>
|
||||
<template #trigger>
|
||||
<n-tooltip trigger="manual" :z-index="9999999">
|
||||
<template #trigger>
|
||||
<i class="iconfont icon-list"></i>
|
||||
</template>
|
||||
播放列表
|
||||
</n-tooltip>
|
||||
</template>
|
||||
<div class="music-play-list">
|
||||
<div class="music-play-list-back"></div>
|
||||
<n-virtual-list ref="palyListRef" :item-size="62" item-resizable :items="playList">
|
||||
<template #default="{ item }">
|
||||
<div class="music-play-list-content">
|
||||
<song-item :key="item.id" :item="item" mini></song-item>
|
||||
</div>
|
||||
</template>
|
||||
</n-virtual-list>
|
||||
</div>
|
||||
</n-popover>
|
||||
</div>
|
||||
<!-- 播放音乐 -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useThrottleFn } from '@vueuse/core';
|
||||
import { useTemplateRef } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import SongItem from '@/components/common/SongItem.vue';
|
||||
import {
|
||||
allTime,
|
||||
isLyricWindowOpen,
|
||||
nowTime,
|
||||
openLyric,
|
||||
sound,
|
||||
textColors
|
||||
} from '@/hooks/MusicHook';
|
||||
import type { SongResult } from '@/type/music';
|
||||
import { getImgUrl, isMobile, secondToMinute, setAnimationClass, isElectron } from '@/utils';
|
||||
|
||||
import MusicFull from './MusicFull.vue';
|
||||
|
||||
const store = useStore();
|
||||
|
||||
// 播放的音乐信息
|
||||
const playMusic = computed(() => store.state.playMusic as SongResult);
|
||||
// 是否播放
|
||||
const play = computed(() => store.state.play as boolean);
|
||||
const playList = computed(() => store.state.playList as SongResult[]);
|
||||
|
||||
const background = ref('#000');
|
||||
|
||||
watch(
|
||||
() => store.state.playMusic,
|
||||
async () => {
|
||||
background.value = playMusic.value.backgroundColor as string;
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
// 使用 useThrottleFn 创建节流版本的 seek 函数
|
||||
const throttledSeek = useThrottleFn((value: number) => {
|
||||
if (!sound.value) return;
|
||||
sound.value.seek(value);
|
||||
nowTime.value = value;
|
||||
}, 50); // 50ms 的节流延迟
|
||||
|
||||
// 修改 timeSlider 计算属性
|
||||
const timeSlider = computed({
|
||||
get: () => nowTime.value,
|
||||
set: throttledSeek
|
||||
});
|
||||
|
||||
const formatTooltip = (value: number) => {
|
||||
return `${secondToMinute(value)} / ${secondToMinute(allTime.value)}`;
|
||||
};
|
||||
|
||||
// 音量条
|
||||
const audioVolume = ref(
|
||||
localStorage.getItem('volume') ? parseFloat(localStorage.getItem('volume') as string) : 1
|
||||
);
|
||||
const getVolumeIcon = computed(() => {
|
||||
// 0 静音 ri-volume-mute-line 0.5 ri-volume-down-line 1 ri-volume-up-line
|
||||
if (audioVolume.value === 0) {
|
||||
return 'ri-volume-mute-line';
|
||||
}
|
||||
if (audioVolume.value <= 0.5) {
|
||||
return 'ri-volume-down-line';
|
||||
}
|
||||
return 'ri-volume-up-line';
|
||||
});
|
||||
|
||||
const volumeSlider = computed({
|
||||
get: () => audioVolume.value * 100,
|
||||
set: (value) => {
|
||||
if (!sound.value) return;
|
||||
localStorage.setItem('volume', (value / 100).toString());
|
||||
sound.value.volume(value / 100);
|
||||
audioVolume.value = value / 100;
|
||||
}
|
||||
});
|
||||
|
||||
// 静音
|
||||
const mute = () => {
|
||||
if (volumeSlider.value === 0) {
|
||||
volumeSlider.value = 30;
|
||||
} else {
|
||||
volumeSlider.value = 0;
|
||||
}
|
||||
};
|
||||
|
||||
// 播放模式
|
||||
const playMode = computed(() => store.state.playMode);
|
||||
const playModeIcon = computed(() => {
|
||||
switch (playMode.value) {
|
||||
case 0:
|
||||
return 'ri-repeat-2-line';
|
||||
case 1:
|
||||
return 'ri-repeat-one-line';
|
||||
case 2:
|
||||
return 'ri-shuffle-line';
|
||||
default:
|
||||
return 'ri-repeat-2-line';
|
||||
}
|
||||
});
|
||||
const playModeText = computed(() => {
|
||||
switch (playMode.value) {
|
||||
case 0:
|
||||
return '列表循环';
|
||||
case 1:
|
||||
return '单曲循环';
|
||||
case 2:
|
||||
return '随机播放';
|
||||
default:
|
||||
return '列表循环';
|
||||
}
|
||||
});
|
||||
|
||||
// 切换播放模式
|
||||
const togglePlayMode = () => {
|
||||
store.commit('togglePlayMode');
|
||||
};
|
||||
|
||||
function handleNext() {
|
||||
store.commit('nextPlay');
|
||||
}
|
||||
|
||||
function handlePrev() {
|
||||
store.commit('prevPlay');
|
||||
}
|
||||
|
||||
const MusicFullRef = ref<any>(null);
|
||||
|
||||
// 播放暂停按钮事件
|
||||
const playMusicEvent = async () => {
|
||||
if (play.value) {
|
||||
if (sound.value) {
|
||||
sound.value.pause();
|
||||
}
|
||||
store.commit('setPlayMusic', false);
|
||||
} else {
|
||||
if (sound.value) {
|
||||
sound.value.play();
|
||||
}
|
||||
store.commit('setPlayMusic', true);
|
||||
}
|
||||
};
|
||||
|
||||
const musicFullVisible = ref(false);
|
||||
|
||||
// 设置musicFull
|
||||
const setMusicFull = () => {
|
||||
musicFullVisible.value = !musicFullVisible.value;
|
||||
};
|
||||
|
||||
const palyListRef = useTemplateRef('palyListRef');
|
||||
|
||||
const scrollToPlayList = (val: boolean) => {
|
||||
if (!val) return;
|
||||
setTimeout(() => {
|
||||
palyListRef.value?.scrollTo({ top: store.state.playListIndex * 62 });
|
||||
}, 50);
|
||||
};
|
||||
|
||||
const isFavorite = computed(() => {
|
||||
return store.state.favoriteList.includes(playMusic.value.id);
|
||||
});
|
||||
|
||||
const toggleFavorite = async (e: Event) => {
|
||||
e.stopPropagation();
|
||||
if (isFavorite.value) {
|
||||
store.commit('removeFromFavorite', playMusic.value.id);
|
||||
} else {
|
||||
store.commit('addToFavorite', playMusic.value.id);
|
||||
}
|
||||
};
|
||||
|
||||
const openLyricWindow = () => {
|
||||
openLyric();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.text-ellipsis {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.music-play-bar {
|
||||
@apply h-20 w-full absolute bottom-0 left-0 flex items-center box-border px-6 py-2 pt-3;
|
||||
@apply bg-light dark:bg-dark shadow-2xl shadow-gray-300;
|
||||
z-index: 9999;
|
||||
animation-duration: 0.5s !important;
|
||||
|
||||
.music-content {
|
||||
width: 160px;
|
||||
@apply ml-4;
|
||||
|
||||
&-title {
|
||||
@apply text-base;
|
||||
}
|
||||
|
||||
&-name {
|
||||
@apply text-xs mt-1 opacity-80;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.play-bar-opcity {
|
||||
@apply bg-transparent !important;
|
||||
box-shadow: 0 0 20px 5px #0000001d;
|
||||
}
|
||||
|
||||
.play-bar-img {
|
||||
@apply w-14 h-14 rounded-2xl;
|
||||
}
|
||||
|
||||
.music-buttons {
|
||||
@apply mx-6 flex-1 flex justify-center;
|
||||
|
||||
.iconfont {
|
||||
@apply text-2xl transition;
|
||||
@apply hover:text-green-500;
|
||||
}
|
||||
|
||||
.icon {
|
||||
@apply text-3xl;
|
||||
@apply hover:text-green-500;
|
||||
}
|
||||
|
||||
@apply flex items-center;
|
||||
|
||||
> div {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
|
||||
&-play {
|
||||
@apply flex justify-center items-center w-20 h-12 rounded-full mx-4 transition text-gray-500;
|
||||
@apply bg-gray-100 bg-opacity-60 hover:bg-gray-200;
|
||||
}
|
||||
}
|
||||
|
||||
.audio-volume {
|
||||
@apply flex items-center relative;
|
||||
&:hover {
|
||||
.volume-slider {
|
||||
@apply opacity-100 visible;
|
||||
}
|
||||
}
|
||||
.volume-icon {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@apply text-2xl transition;
|
||||
@apply hover:text-green-500;
|
||||
}
|
||||
|
||||
.volume-slider {
|
||||
@apply absolute opacity-0 invisible transition-all duration-300 bottom-[30px] left-1/2 -translate-x-1/2 h-[180px] px-2 py-4 rounded-xl;
|
||||
@apply bg-light dark:bg-gray-800;
|
||||
@apply border border-gray-200 dark:border-gray-700;
|
||||
}
|
||||
}
|
||||
|
||||
.audio-button {
|
||||
@apply flex items-center mx-4;
|
||||
|
||||
.iconfont {
|
||||
@apply text-2xl transition cursor-pointer m-4;
|
||||
@apply hover:text-green-500;
|
||||
}
|
||||
}
|
||||
|
||||
.music-play {
|
||||
&-list {
|
||||
height: 50vh;
|
||||
width: 300px;
|
||||
@apply relative rounded-3xl overflow-hidden py-2;
|
||||
&-back {
|
||||
backdrop-filter: blur(20px);
|
||||
@apply absolute top-0 left-0 w-full h-full;
|
||||
@apply bg-light dark:bg-black bg-opacity-75;
|
||||
}
|
||||
&-content {
|
||||
@apply mx-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile {
|
||||
.music-play-bar {
|
||||
@apply px-4;
|
||||
bottom: 70px;
|
||||
}
|
||||
.music-time {
|
||||
display: none;
|
||||
}
|
||||
.ri-netease-cloud-music-line {
|
||||
display: none;
|
||||
}
|
||||
.audio-volume {
|
||||
display: none;
|
||||
}
|
||||
.audio-button {
|
||||
@apply mx-0;
|
||||
}
|
||||
.music-buttons {
|
||||
@apply m-0;
|
||||
&-prev,
|
||||
&-next {
|
||||
display: none;
|
||||
}
|
||||
&-play {
|
||||
@apply m-0;
|
||||
}
|
||||
}
|
||||
.music-content {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义滑块样式
|
||||
.custom-slider {
|
||||
:deep(.n-slider) {
|
||||
--n-rail-height: 4px;
|
||||
--n-rail-color: theme('colors.gray.200');
|
||||
--n-rail-color-dark: theme('colors.gray.700');
|
||||
--n-fill-color: theme('colors.green.500');
|
||||
--n-handle-size: 12px;
|
||||
--n-handle-color: theme('colors.green.500');
|
||||
|
||||
&.n-slider--vertical {
|
||||
height: 100%;
|
||||
|
||||
.n-slider-rail {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.n-slider-rail {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.n-slider-handle {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.n-slider-rail {
|
||||
@apply overflow-hidden transition-all duration-200;
|
||||
@apply bg-gray-500 dark:bg-dark-300 bg-opacity-10 !important;
|
||||
}
|
||||
|
||||
.n-slider-handle {
|
||||
@apply transition-all duration-200;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover .n-slider-handle {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.play-bar-img-wrapper {
|
||||
@apply relative cursor-pointer w-14 h-14;
|
||||
|
||||
.hover-arrow {
|
||||
@apply absolute inset-0 flex items-center justify-center opacity-0 transition-opacity duration-300 rounded-2xl;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
|
||||
.hover-content {
|
||||
@apply flex flex-col items-center justify-center;
|
||||
|
||||
i {
|
||||
@apply text-white mb-0.5;
|
||||
}
|
||||
|
||||
.hover-text {
|
||||
@apply text-white text-xs scale-90;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.hover-arrow {
|
||||
@apply opacity-100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-content {
|
||||
@apply text-sm py-1 px-2;
|
||||
}
|
||||
|
||||
.play-bar-img {
|
||||
@apply w-14 h-14 rounded-2xl;
|
||||
}
|
||||
|
||||
.like-active {
|
||||
@apply text-red-500 hover:text-red-600 !important;
|
||||
}
|
||||
|
||||
.icon-loop,
|
||||
.icon-single-loop {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.music-time .n-slider {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,319 @@
|
||||
<template>
|
||||
<div class="search-box flex">
|
||||
<div class="search-box-input flex-1">
|
||||
<n-input
|
||||
v-model:value="searchValue"
|
||||
size="medium"
|
||||
round
|
||||
:placeholder="hotSearchKeyword"
|
||||
class="border dark:border-gray-600 border-gray-200"
|
||||
@keydown.enter="search"
|
||||
>
|
||||
<template #prefix>
|
||||
<i class="iconfont icon-search"></i>
|
||||
</template>
|
||||
<template #suffix>
|
||||
<n-dropdown trigger="hover" :options="searchTypeOptions" @select="selectSearchType">
|
||||
<div class="w-20 px-3 flex justify-between items-center">
|
||||
<div>
|
||||
{{ searchTypeOptions.find((item) => item.key === store.state.searchType)?.label }}
|
||||
</div>
|
||||
<i class="iconfont icon-xiasanjiaoxing"></i>
|
||||
</div>
|
||||
</n-dropdown>
|
||||
</template>
|
||||
</n-input>
|
||||
</div>
|
||||
<n-popover trigger="hover" placement="bottom" :show-arrow="false" raw>
|
||||
<template #trigger>
|
||||
<div class="user-box">
|
||||
<n-avatar
|
||||
v-if="store.state.user"
|
||||
class="cursor-pointer"
|
||||
circle
|
||||
size="medium"
|
||||
:src="getImgUrl(store.state.user.avatarUrl)"
|
||||
@click="selectItem('user')"
|
||||
/>
|
||||
<div v-else class="mx-2 rounded-full cursor-pointer text-sm" @click="toLogin">登录</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="user-popover">
|
||||
<div v-if="store.state.user" class="user-header" @click="selectItem('user')">
|
||||
<n-avatar circle size="small" :src="getImgUrl(store.state.user?.avatarUrl)" />
|
||||
<span class="username">{{ store.state.user?.nickname || 'Theodore' }}</span>
|
||||
</div>
|
||||
<div class="menu-items">
|
||||
<div v-if="!store.state.user" class="menu-item" @click="toLogin">
|
||||
<i class="iconfont ri-login-box-line"></i>
|
||||
<span>去登录</span>
|
||||
</div>
|
||||
<!-- 切换主题 -->
|
||||
<div class="menu-item" @click="selectItem('set')">
|
||||
<i class="iconfont ri-settings-3-line"></i>
|
||||
<span>设置</span>
|
||||
</div>
|
||||
<div class="menu-item">
|
||||
<i class="iconfont" :class="isDarkTheme ? 'ri-moon-line' : 'ri-sun-line'"></i>
|
||||
<span>主题</span>
|
||||
<n-switch v-model:value="isDarkTheme" class="ml-auto">
|
||||
<template #checked>
|
||||
<i class="ri-moon-line"></i>
|
||||
</template>
|
||||
<template #unchecked>
|
||||
<i class="ri-sun-line"></i>
|
||||
</template>
|
||||
</n-switch>
|
||||
</div>
|
||||
<div class="menu-item" @click="restartApp">
|
||||
<i class="iconfont ri-restart-line"></i>
|
||||
<span>重启</span>
|
||||
</div>
|
||||
<div class="menu-item" @click="toGithubRelease">
|
||||
<i class="iconfont ri-refresh-line"></i>
|
||||
<span>当前版本</span>
|
||||
<div class="version-info">
|
||||
<span class="version-number">{{ updateInfo.currentVersion }}</span>
|
||||
<n-tag v-if="updateInfo.hasUpdate" type="success" size="small" class="ml-1">
|
||||
New {{ updateInfo.latestVersion }}
|
||||
</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-popover>
|
||||
|
||||
<coffee :alipay-q-r="alipay" :wechat-q-r="wechat">
|
||||
<div class="github" @click="toGithub">
|
||||
<i class="ri-github-fill"></i>
|
||||
</div>
|
||||
</coffee>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, watchEffect, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import { getSearchKeyword } from '@/api/home';
|
||||
import { getUserDetail, logout } from '@/api/login';
|
||||
import alipay from '@/assets/alipay.png';
|
||||
import wechat from '@/assets/wechat.png';
|
||||
import Coffee from '@/components/Coffee.vue';
|
||||
import { SEARCH_TYPES, USER_SET_OPTIONS } from '@/const/bar-const';
|
||||
import { getImgUrl, checkUpdate } from '@/utils';
|
||||
|
||||
import config from '../../../../package.json';
|
||||
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
const userSetOptions = ref(USER_SET_OPTIONS);
|
||||
|
||||
// 推荐热搜词
|
||||
const hotSearchKeyword = ref('搜索点什么吧...');
|
||||
const hotSearchValue = ref('');
|
||||
const loadHotSearchKeyword = async () => {
|
||||
const { data } = await getSearchKeyword();
|
||||
hotSearchKeyword.value = data.data.showKeyword;
|
||||
hotSearchValue.value = data.data.realkeyword;
|
||||
};
|
||||
|
||||
const loadPage = async () => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
const { data } = await getUserDetail();
|
||||
store.state.user = data.profile;
|
||||
localStorage.setItem('user', JSON.stringify(data.profile));
|
||||
};
|
||||
|
||||
loadPage();
|
||||
|
||||
watchEffect(() => {
|
||||
if (store.state.user) {
|
||||
userSetOptions.value = USER_SET_OPTIONS;
|
||||
} else {
|
||||
userSetOptions.value = USER_SET_OPTIONS.filter((item) => item.key !== 'logout');
|
||||
}
|
||||
});
|
||||
|
||||
const restartApp = () => {
|
||||
window.electron.ipcRenderer.send('restart');
|
||||
};
|
||||
|
||||
const toLogin = () => {
|
||||
router.push('/login');
|
||||
};
|
||||
|
||||
// 页面初始化
|
||||
onMounted(() => {
|
||||
loadHotSearchKeyword();
|
||||
loadPage();
|
||||
checkForUpdates();
|
||||
});
|
||||
|
||||
const isDarkTheme = computed({
|
||||
get: () => store.state.theme === 'dark',
|
||||
set: () => store.commit('toggleTheme')
|
||||
});
|
||||
|
||||
// 搜索词
|
||||
const searchValue = ref('');
|
||||
const search = () => {
|
||||
const { value } = searchValue;
|
||||
if (value === '') {
|
||||
searchValue.value = hotSearchValue.value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (router.currentRoute.value.path === '/search') {
|
||||
store.state.searchValue = value;
|
||||
return;
|
||||
}
|
||||
|
||||
router.push({
|
||||
path: '/search',
|
||||
query: {
|
||||
keyword: value
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const selectSearchType = (key: number) => {
|
||||
store.state.searchType = key;
|
||||
};
|
||||
|
||||
const searchTypeOptions = ref(SEARCH_TYPES);
|
||||
|
||||
const selectItem = async (key: string) => {
|
||||
// switch 判断
|
||||
switch (key) {
|
||||
case 'logout':
|
||||
logout().then(() => {
|
||||
store.state.user = null;
|
||||
localStorage.clear();
|
||||
router.push('/login');
|
||||
});
|
||||
break;
|
||||
case 'login':
|
||||
router.push('/login');
|
||||
break;
|
||||
case 'set':
|
||||
router.push('/set');
|
||||
break;
|
||||
case 'user':
|
||||
router.push('/user');
|
||||
break;
|
||||
default:
|
||||
}
|
||||
};
|
||||
|
||||
const toGithub = () => {
|
||||
window.open('https://github.com/algerkong/AlgerMusicPlayer', '_blank');
|
||||
};
|
||||
|
||||
const updateInfo = ref({
|
||||
hasUpdate: false,
|
||||
latestVersion: '',
|
||||
currentVersion: config.version,
|
||||
releaseInfo: null
|
||||
});
|
||||
|
||||
const checkForUpdates = async () => {
|
||||
try {
|
||||
const result = await checkUpdate();
|
||||
updateInfo.value = result;
|
||||
} catch (error) {
|
||||
console.error('检查更新失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const toGithubRelease = () => {
|
||||
if (updateInfo.value.hasUpdate) {
|
||||
window.open('https://github.com/algerkong/AlgerMusicPlayer/releases/latest', '_blank');
|
||||
} else {
|
||||
window.open('https://github.com/algerkong/AlgerMusicPlayer/releases', '_blank');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-box {
|
||||
@apply ml-4 flex text-lg justify-center items-center rounded-full transition-colors duration-200;
|
||||
@apply border dark:border-gray-600 border-gray-200 hover:border-gray-400 dark:hover:border-gray-400;
|
||||
@apply bg-light dark:bg-gray-800;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
@apply pb-4 pr-4;
|
||||
}
|
||||
|
||||
.search-box-input {
|
||||
@apply relative;
|
||||
|
||||
:deep(.n-input) {
|
||||
@apply bg-gray-50 dark:bg-black;
|
||||
|
||||
.n-input__input-el {
|
||||
@apply text-gray-900 dark:text-white;
|
||||
}
|
||||
|
||||
.n-input__prefix {
|
||||
@apply text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile {
|
||||
.search-box {
|
||||
@apply pl-4;
|
||||
}
|
||||
}
|
||||
|
||||
.github {
|
||||
@apply cursor-pointer text-gray-900 dark:text-gray-100 hover:text-gray-600 dark:hover:text-gray-400 text-xl ml-4 rounded-full flex justify-center items-center px-2 h-full;
|
||||
@apply border dark:border-gray-600 border-gray-200 bg-light dark:bg-black;
|
||||
}
|
||||
|
||||
.user-popover {
|
||||
@apply min-w-[280px] p-0 rounded-xl overflow-hidden;
|
||||
@apply bg-light dark:bg-black;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.user-header {
|
||||
@apply flex items-center gap-2 p-3 cursor-pointer;
|
||||
@apply border-b dark:border-gray-700 border-gray-100 hover:bg-gray-100 dark:hover:bg-gray-700;
|
||||
|
||||
.username {
|
||||
@apply text-sm font-medium text-gray-900 dark:text-gray-200;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-items {
|
||||
@apply py-1;
|
||||
|
||||
.menu-item {
|
||||
@apply flex items-center px-3 py-2 text-sm cursor-pointer;
|
||||
@apply text-gray-700 dark:text-gray-300;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
@apply bg-gray-100 dark:bg-gray-700;
|
||||
}
|
||||
|
||||
i {
|
||||
@apply mr-1 text-lg text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
|
||||
.version-info {
|
||||
@apply ml-auto flex items-center;
|
||||
|
||||
.version-number {
|
||||
@apply text-xs px-2 py-0.5 rounded;
|
||||
@apply bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div id="title-bar" @mousedown="drag">
|
||||
<div id="title">Alger Music</div>
|
||||
<div id="buttons">
|
||||
<button @click="minimize">
|
||||
<i class="iconfont icon-minisize"></i>
|
||||
</button>
|
||||
<button @click="close">
|
||||
<i class="iconfont icon-close"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDialog } from 'naive-ui';
|
||||
|
||||
import { isElectron } from '@/utils';
|
||||
|
||||
const dialog = useDialog();
|
||||
|
||||
const minimize = () => {
|
||||
if (!isElectron) {
|
||||
return;
|
||||
}
|
||||
window.api.minimize();
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
if (!isElectron) {
|
||||
return;
|
||||
}
|
||||
dialog.warning({
|
||||
title: '提示',
|
||||
content: '确定要退出吗?',
|
||||
positiveText: '最小化',
|
||||
negativeText: '关闭',
|
||||
onPositiveClick: () => {
|
||||
window.api.minimize();
|
||||
},
|
||||
onNegativeClick: () => {
|
||||
window.api.close();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const drag = (event: MouseEvent) => {
|
||||
if (!isElectron) {
|
||||
return;
|
||||
}
|
||||
window.api.dragStart(event as unknown as string);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
#title-bar {
|
||||
-webkit-app-region: drag;
|
||||
@apply flex justify-between px-6 py-2 select-none relative;
|
||||
@apply text-dark dark:text-white;
|
||||
z-index: 9999999;
|
||||
}
|
||||
|
||||
#buttons {
|
||||
@apply flex gap-4;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
button {
|
||||
@apply text-gray-600 dark:text-gray-400 hover:text-green-500;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,5 @@
|
||||
import AppMenu from './AppMenu.vue';
|
||||
import PlayBar from './PlayBar.vue';
|
||||
import SearchBar from './SearchBar.vue';
|
||||
|
||||
export { AppMenu, PlayBar, SearchBar };
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="lrc-full">
|
||||
{{ lrcIndex }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
lrcList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
lrcIndex: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
lrcTime: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
Reference in New Issue
Block a user