mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-26 17:27:23 +08:00
✨ feat: 添加定时关闭功能,支持按时间、歌曲数和播放列表结束自动停止播放
This commit is contained in:
@@ -73,5 +73,27 @@ export default {
|
|||||||
acoustic: 'Acoustic',
|
acoustic: 'Acoustic',
|
||||||
custom: 'Custom'
|
custom: 'Custom'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
// Sleep timer related
|
||||||
|
sleepTimer: {
|
||||||
|
title: 'Sleep Timer',
|
||||||
|
cancel: 'Cancel Timer',
|
||||||
|
timeMode: 'By Time',
|
||||||
|
songsMode: 'By Songs',
|
||||||
|
playlistEnd: 'After Playlist',
|
||||||
|
afterPlaylist: 'After Playlist Ends',
|
||||||
|
activeUntilEnd: 'Active until end of playlist',
|
||||||
|
minutes: 'min',
|
||||||
|
hours: 'hr',
|
||||||
|
songs: 'songs',
|
||||||
|
set: 'Set',
|
||||||
|
timerSetSuccess: 'Timer set for {minutes} minutes',
|
||||||
|
songsSetSuccess: 'Timer set for {songs} songs',
|
||||||
|
playlistEndSetSuccess: 'Timer set to end after playlist',
|
||||||
|
timerCancelled: 'Sleep timer cancelled',
|
||||||
|
timerEnded: 'Sleep timer ended',
|
||||||
|
playbackStopped: 'Music playback stopped',
|
||||||
|
minutesRemaining: '{minutes} min remaining',
|
||||||
|
songsRemaining: '{count} songs remaining'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -74,5 +74,27 @@ export default {
|
|||||||
acoustic: '原声',
|
acoustic: '原声',
|
||||||
custom: '自定义'
|
custom: '自定义'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
// 定时关闭功能相关
|
||||||
|
sleepTimer: {
|
||||||
|
title: '定时关闭',
|
||||||
|
cancel: '取消定时',
|
||||||
|
timeMode: '按时间关闭',
|
||||||
|
songsMode: '按歌曲数关闭',
|
||||||
|
playlistEnd: '播放完列表后关闭',
|
||||||
|
afterPlaylist: '播放完列表后关闭',
|
||||||
|
activeUntilEnd: '播放至列表结束',
|
||||||
|
minutes: '分钟',
|
||||||
|
hours: '小时',
|
||||||
|
songs: '首歌',
|
||||||
|
set: '设置',
|
||||||
|
timerSetSuccess: '已设置{minutes}分钟后关闭',
|
||||||
|
songsSetSuccess: '已设置播放{songs}首歌后关闭',
|
||||||
|
playlistEndSetSuccess: '已设置播放完列表后关闭',
|
||||||
|
timerCancelled: '已取消定时关闭',
|
||||||
|
timerEnded: '定时关闭已触发',
|
||||||
|
playbackStopped: '音乐播放已停止',
|
||||||
|
minutesRemaining: '剩余{minutes}分钟',
|
||||||
|
songsRemaining: '剩余{count}首歌'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -87,6 +87,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</n-popover>
|
</n-popover>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 定时关闭按钮 -->
|
||||||
|
<SleepTimerPopover mode="mobile" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Mini模式 - 在musicFullVisible为false时显示 -->
|
<!-- Mini模式 - 在musicFullVisible为false时显示 -->
|
||||||
@@ -152,6 +155,7 @@ import { useThrottleFn } from '@vueuse/core';
|
|||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
import SongItem from '@/components/common/SongItem.vue';
|
import SongItem from '@/components/common/SongItem.vue';
|
||||||
|
import SleepTimerPopover from '@/components/player/SleepTimerPopover.vue';
|
||||||
import { allTime, artistList, nowTime, playMusic, sound, textColors } from '@/hooks/MusicHook';
|
import { allTime, artistList, nowTime, playMusic, sound, textColors } from '@/hooks/MusicHook';
|
||||||
import MusicFull from '@/layout/components/MusicFull.vue';
|
import MusicFull from '@/layout/components/MusicFull.vue';
|
||||||
import { audioService } from '@/services/audioService';
|
import { audioService } from '@/services/audioService';
|
||||||
|
|||||||
@@ -144,6 +144,8 @@
|
|||||||
</template>
|
</template>
|
||||||
<eq-control />
|
<eq-control />
|
||||||
</n-popover>
|
</n-popover>
|
||||||
|
<!-- 定时关闭功能 -->
|
||||||
|
<sleep-timer-popover mode="desktop" />
|
||||||
<n-popover
|
<n-popover
|
||||||
trigger="click"
|
trigger="click"
|
||||||
:z-index="99999999"
|
:z-index="99999999"
|
||||||
@@ -194,6 +196,7 @@ import { useI18n } from 'vue-i18n';
|
|||||||
|
|
||||||
import SongItem from '@/components/common/SongItem.vue';
|
import SongItem from '@/components/common/SongItem.vue';
|
||||||
import EqControl from '@/components/EQControl.vue';
|
import EqControl from '@/components/EQControl.vue';
|
||||||
|
import SleepTimerPopover from '@/components/player/SleepTimerPopover.vue';
|
||||||
import {
|
import {
|
||||||
allTime,
|
allTime,
|
||||||
artistList,
|
artistList,
|
||||||
@@ -206,7 +209,10 @@ import {
|
|||||||
import { useArtist } from '@/hooks/useArtist';
|
import { useArtist } from '@/hooks/useArtist';
|
||||||
import MusicFull from '@/layout/components/MusicFull.vue';
|
import MusicFull from '@/layout/components/MusicFull.vue';
|
||||||
import { audioService } from '@/services/audioService';
|
import { audioService } from '@/services/audioService';
|
||||||
import { isBilibiliIdMatch, usePlayerStore } from '@/store/modules/player';
|
import {
|
||||||
|
isBilibiliIdMatch,
|
||||||
|
usePlayerStore
|
||||||
|
} from '@/store/modules/player';
|
||||||
import { useSettingsStore } from '@/store/modules/settings';
|
import { useSettingsStore } from '@/store/modules/settings';
|
||||||
import type { SongResult } from '@/type/music';
|
import type { SongResult } from '@/type/music';
|
||||||
import { getImgUrl, isElectron, isMobile, secondToMinute, setAnimationClass } from '@/utils';
|
import { getImgUrl, isElectron, isMobile, secondToMinute, setAnimationClass } from '@/utils';
|
||||||
|
|||||||
@@ -0,0 +1,478 @@
|
|||||||
|
<template>
|
||||||
|
<n-popover
|
||||||
|
trigger="click"
|
||||||
|
:z-index="99999999"
|
||||||
|
content-class="sleep-timer"
|
||||||
|
raw
|
||||||
|
:show-arrow="false"
|
||||||
|
placement="top"
|
||||||
|
display-directive="show"
|
||||||
|
>
|
||||||
|
<template #trigger>
|
||||||
|
<n-tooltip trigger="hover" :z-index="9999999">
|
||||||
|
<template #trigger>
|
||||||
|
<div class="control-btn timer" :class="{ 'mobile-timer': mode === 'mobile' }">
|
||||||
|
<i
|
||||||
|
class="iconfont ri-time-line"
|
||||||
|
:class="{ 'text-green-500': hasTimerActive }"
|
||||||
|
></i>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
{{ t('player.sleepTimer.title') }}
|
||||||
|
</n-tooltip>
|
||||||
|
</template>
|
||||||
|
<div class="sleep-timer-container" :class="{ 'mobile-sleep-timer-container': mode === 'mobile' }">
|
||||||
|
<div class="sleep-timer-back"></div>
|
||||||
|
|
||||||
|
<div class="sleep-timer-content">
|
||||||
|
<div class="sleep-timer-title">{{ t('player.sleepTimer.title') }}</div>
|
||||||
|
|
||||||
|
<div v-if="hasTimerActive" class="sleep-timer-active">
|
||||||
|
<div class="timer-status">
|
||||||
|
<template v-if="timerType === 'time'">
|
||||||
|
<div class="timer-value countdown-timer">{{ formattedRemainingTime }}</div>
|
||||||
|
<div class="timer-label">{{ t('player.sleepTimer.minutesRemaining', { minutes: Math.ceil(remainingMinutes/60) }) }}</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="timerType === 'songs'">
|
||||||
|
<div class="timer-value">{{ remainingSongs }}</div>
|
||||||
|
<div class="timer-label">{{ t('player.sleepTimer.songsRemaining', { count: remainingSongs }) }}</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="timerType === 'end'">
|
||||||
|
<div class="timer-value">{{ t('player.sleepTimer.activeUntilEnd') }}</div>
|
||||||
|
<div class="timer-label">{{ t('player.sleepTimer.afterPlaylist') }}</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-button type="error" class="cancel-timer-btn" @click="handleCancelTimer" round>
|
||||||
|
{{ t('player.sleepTimer.cancel') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="sleep-timer-options">
|
||||||
|
<!-- 按时间定时 -->
|
||||||
|
<div class="option-section">
|
||||||
|
<h4 class="option-title">{{ t('player.sleepTimer.timeMode') }}</h4>
|
||||||
|
<div class="time-options">
|
||||||
|
<n-button
|
||||||
|
v-for="minutes in [15, 30, 60, 90]"
|
||||||
|
:key="minutes"
|
||||||
|
size="small"
|
||||||
|
class="time-option-btn"
|
||||||
|
@click="handleSetTimeTimer(minutes)"
|
||||||
|
round
|
||||||
|
>
|
||||||
|
{{ minutes }}{{ t('player.sleepTimer.minutes') }}
|
||||||
|
</n-button>
|
||||||
|
<div class="custom-time">
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="customMinutes"
|
||||||
|
:min="1"
|
||||||
|
:max="300"
|
||||||
|
size="small"
|
||||||
|
class="custom-time-input"
|
||||||
|
round
|
||||||
|
/>
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
class="custom-time-btn"
|
||||||
|
:disabled="!customMinutes"
|
||||||
|
@click="handleSetTimeTimer(customMinutes)"
|
||||||
|
round
|
||||||
|
>
|
||||||
|
{{ t('player.sleepTimer.set') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 按歌曲数定时 -->
|
||||||
|
<div class="option-section">
|
||||||
|
<h4 class="option-title">{{ t('player.sleepTimer.songsMode') }}</h4>
|
||||||
|
<div class="songs-options">
|
||||||
|
<n-button
|
||||||
|
v-for="songs in [1, 3, 5, 10]"
|
||||||
|
:key="songs"
|
||||||
|
size="small"
|
||||||
|
class="songs-option-btn"
|
||||||
|
@click="handleSetSongsTimer(songs)"
|
||||||
|
round
|
||||||
|
>
|
||||||
|
{{ songs }}{{ t('player.sleepTimer.songs') }}
|
||||||
|
</n-button>
|
||||||
|
<div class="custom-songs">
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="customSongs"
|
||||||
|
:min="1"
|
||||||
|
:max="50"
|
||||||
|
size="small"
|
||||||
|
class="custom-songs-input"
|
||||||
|
round
|
||||||
|
/>
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
class="custom-songs-btn"
|
||||||
|
:disabled="!customSongs"
|
||||||
|
@click="handleSetSongsTimer(customSongs)"
|
||||||
|
round
|
||||||
|
>
|
||||||
|
{{ t('player.sleepTimer.set') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 播放完列表后关闭 -->
|
||||||
|
<div class="option-section playlist-end-section">
|
||||||
|
<n-button block class="playlist-end-btn" @click="handleSetPlaylistEndTimer" round>
|
||||||
|
{{ t('player.sleepTimer.playlistEnd') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref, onMounted, onUnmounted, watch } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
|
import { usePlayerStore } from '@/store/modules/player';
|
||||||
|
|
||||||
|
// 组件接收一个mode参数,用于标识是移动端还是桌面端
|
||||||
|
defineProps({
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: 'desktop' // 'desktop' 或 'mobile'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const playerStore = usePlayerStore();
|
||||||
|
|
||||||
|
// 从store获取所有相关状态
|
||||||
|
const { sleepTimer} = storeToRefs(playerStore);
|
||||||
|
|
||||||
|
// 本地状态,用于UI展示
|
||||||
|
const customMinutes = ref(30);
|
||||||
|
const customSongs = ref(5);
|
||||||
|
// 添加一个刷新触发变量,用于强制更新倒计时
|
||||||
|
const refreshTrigger = ref(0);
|
||||||
|
|
||||||
|
// 计算属性,判断定时器状态
|
||||||
|
const hasTimerActive = computed(() => {
|
||||||
|
return playerStore.hasSleepTimerActive;
|
||||||
|
});
|
||||||
|
|
||||||
|
const timerType = computed(() => {
|
||||||
|
return sleepTimer.value.type;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 剩余时间(分钟)
|
||||||
|
const remainingMinutes = computed(() => {
|
||||||
|
return playerStore.sleepTimerRemainingTime;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 剩余歌曲数
|
||||||
|
const remainingSongs = computed(() => {
|
||||||
|
return playerStore.sleepTimerRemainingSongs;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理设置时间定时器
|
||||||
|
function handleSetTimeTimer(minutes: number) {
|
||||||
|
playerStore.setSleepTimerByTime(minutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理设置歌曲数定时器
|
||||||
|
function handleSetSongsTimer(songs: number) {
|
||||||
|
playerStore.setSleepTimerBySongs(songs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理设置播放列表结束定时器
|
||||||
|
function handleSetPlaylistEndTimer() {
|
||||||
|
playerStore.setSleepTimerAtPlaylistEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理取消定时器
|
||||||
|
function handleCancelTimer() {
|
||||||
|
playerStore.clearSleepTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化剩余时间为 HH:MM:SS
|
||||||
|
const formattedRemainingTime = computed(() => {
|
||||||
|
// 依赖刷新触发器强制更新
|
||||||
|
void refreshTrigger.value;
|
||||||
|
|
||||||
|
if (timerType.value !== 'time' || !sleepTimer.value.endTime) {
|
||||||
|
return '00:00:00';
|
||||||
|
}
|
||||||
|
|
||||||
|
const remaining = Math.max(0, sleepTimer.value.endTime - Date.now());
|
||||||
|
const totalSeconds = Math.floor(remaining / 1000);
|
||||||
|
|
||||||
|
const hours = Math.floor(totalSeconds / 3600);
|
||||||
|
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||||
|
const seconds = Math.floor(totalSeconds % 60);
|
||||||
|
|
||||||
|
const formattedHours = hours.toString().padStart(2, '0');
|
||||||
|
const formattedMinutes = minutes.toString().padStart(2, '0');
|
||||||
|
const formattedSeconds = seconds.toString().padStart(2, '0');
|
||||||
|
|
||||||
|
return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听剩余时间变化
|
||||||
|
let timerInterval: number | null = null;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 如果当前有定时器,开始更新UI
|
||||||
|
if (hasTimerActive.value && timerType.value === 'time') {
|
||||||
|
startTimerUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听定时器状态变化
|
||||||
|
watch(
|
||||||
|
() => [hasTimerActive.value, timerType.value],
|
||||||
|
([newHasTimer, newType]) => {
|
||||||
|
if (newHasTimer && newType === 'time') {
|
||||||
|
startTimerUpdate();
|
||||||
|
} else {
|
||||||
|
stopTimerUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 启动定时器更新UI
|
||||||
|
function startTimerUpdate() {
|
||||||
|
stopTimerUpdate(); // 先停止之前的计时器
|
||||||
|
|
||||||
|
// 每秒更新UI
|
||||||
|
timerInterval = window.setInterval(() => {
|
||||||
|
// 更新刷新触发器,强制重新计算
|
||||||
|
refreshTrigger.value = Date.now();
|
||||||
|
}, 500) as unknown as number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止定时器更新UI
|
||||||
|
function stopTimerUpdate() {
|
||||||
|
if (timerInterval) {
|
||||||
|
clearInterval(timerInterval);
|
||||||
|
timerInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopTimerUpdate();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.control-btn.timer {
|
||||||
|
@apply cursor-pointer;
|
||||||
|
|
||||||
|
.iconfont {
|
||||||
|
@apply text-2xl m-4 transition-all duration-300 ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@apply text-green-500 transform scale-110;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mobile-timer {
|
||||||
|
@apply flex items-center justify-center;
|
||||||
|
height: 56px;
|
||||||
|
width: 56px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主容器样式
|
||||||
|
.sleep-timer-container {
|
||||||
|
width: 380px;
|
||||||
|
height: auto;
|
||||||
|
@apply relative overflow-hidden;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15), 0 0 1px rgba(0, 0, 0, 0.1);
|
||||||
|
transform-origin: top center;
|
||||||
|
animation: popoverEnter 0.2s cubic-bezier(0.3, 0, 0.2, 1);
|
||||||
|
|
||||||
|
// 入场动画
|
||||||
|
@keyframes popoverEnter {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px) scale(0.98);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动端样式
|
||||||
|
&.mobile-sleep-timer-container {
|
||||||
|
width: 92vw;
|
||||||
|
max-width: 400px;
|
||||||
|
@apply rounded-t-2xl;
|
||||||
|
max-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 背景毛玻璃效果
|
||||||
|
.sleep-timer-back {
|
||||||
|
@apply absolute top-0 left-0 w-full h-full dark:bg-gray-900 dark:bg-opacity-75 dark:border-gray-700;
|
||||||
|
backdrop-filter: blur(25px);
|
||||||
|
-webkit-backdrop-filter: blur(25px);
|
||||||
|
background-color: rgba(255, 255, 255, 0.75);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内容区域
|
||||||
|
.sleep-timer-content {
|
||||||
|
@apply p-6 relative z-10;
|
||||||
|
|
||||||
|
// 标题
|
||||||
|
.sleep-timer-title {
|
||||||
|
@apply text-xl font-bold text-center mb-6 text-gray-800 dark:text-gray-100;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 激活状态显示
|
||||||
|
.sleep-timer-active {
|
||||||
|
@apply flex flex-col items-center;
|
||||||
|
|
||||||
|
// 定时状态卡片
|
||||||
|
.timer-status {
|
||||||
|
@apply flex flex-col items-center justify-center p-8 mb-5 w-full rounded-2xl dark:bg-gray-800 dark:bg-opacity-40 dark:shadow-gray-900/20;
|
||||||
|
background-color: rgba(255, 255, 255, 0.5);
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
// 定时值显示
|
||||||
|
.timer-value {
|
||||||
|
@apply text-4xl font-semibold mb-2 text-green-500;
|
||||||
|
|
||||||
|
&.countdown-timer {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标签文本
|
||||||
|
.timer-label {
|
||||||
|
@apply text-base text-gray-600 dark:text-gray-300;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消按钮
|
||||||
|
.cancel-timer-btn {
|
||||||
|
@apply w-full py-3 text-base rounded-full transition-all duration-200;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@apply transform scale-105 shadow-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
@apply transform scale-95;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定时器选项区域
|
||||||
|
.sleep-timer-options {
|
||||||
|
@apply flex flex-col;
|
||||||
|
|
||||||
|
// 选项部分
|
||||||
|
.option-section {
|
||||||
|
@apply mb-7;
|
||||||
|
|
||||||
|
// 选项标题
|
||||||
|
.option-title {
|
||||||
|
@apply text-base font-medium mb-4 text-gray-700 dark:text-gray-200;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间/歌曲选项容器
|
||||||
|
.time-options, .songs-options {
|
||||||
|
@apply flex flex-wrap gap-2;
|
||||||
|
|
||||||
|
// 选项按钮共享样式
|
||||||
|
.time-option-btn, .songs-option-btn {
|
||||||
|
@apply px-4 py-2 rounded-full text-gray-800 dark:text-gray-200 transition-all duration-200;
|
||||||
|
background-color: rgba(255, 255, 255, 0.5);
|
||||||
|
@apply dark:bg-gray-800 dark:bg-opacity-40 hover:bg-white dark:hover:bg-gray-700;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||||
|
@apply dark:shadow-gray-900/20;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@apply transform scale-105 shadow-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
@apply transform scale-95;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义输入区域
|
||||||
|
.custom-time, .custom-songs {
|
||||||
|
@apply flex items-center space-x-2 mt-4 w-full;
|
||||||
|
|
||||||
|
// 输入框
|
||||||
|
.custom-time-input, .custom-songs-input {
|
||||||
|
@apply flex-1;
|
||||||
|
|
||||||
|
:deep(.n-input) {
|
||||||
|
@apply rounded-full dark:bg-gray-800 dark:bg-opacity-40 dark:border-gray-700;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
background-color: rgba(255, 255, 255, 0.5);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover, &:focus {
|
||||||
|
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置按钮
|
||||||
|
.custom-time-btn, .custom-songs-btn {
|
||||||
|
@apply py-2 px-4 rounded-full transition-all duration-200;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@apply transform scale-105 shadow-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
@apply transform scale-95;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 播放列表结束选项
|
||||||
|
.playlist-end-section {
|
||||||
|
@apply mt-2;
|
||||||
|
|
||||||
|
.playlist-end-btn {
|
||||||
|
@apply py-3 text-base rounded-full text-gray-800 dark:text-gray-200 transition-all duration-200;
|
||||||
|
background-color: rgba(255, 255, 255, 0.5);
|
||||||
|
@apply dark:bg-gray-800 dark:bg-opacity-40 hover:bg-white dark:hover:bg-gray-700;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||||
|
@apply dark:shadow-gray-900/20;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@apply transform scale-105 shadow-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
@apply transform scale-95;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -323,6 +323,23 @@ const loadLrcAsync = async (playMusic: SongResult) => {
|
|||||||
playMusic.lyric = lyrics;
|
playMusic.lyric = lyrics;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 定时关闭类型
|
||||||
|
export enum SleepTimerType {
|
||||||
|
NONE = 'none', // 没有定时
|
||||||
|
TIME = 'time', // 按时间定时
|
||||||
|
SONGS = 'songs', // 按歌曲数定时
|
||||||
|
PLAYLIST_END = 'end' // 播放列表播放完毕定时
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定时关闭信息
|
||||||
|
export interface SleepTimerInfo {
|
||||||
|
type: SleepTimerType;
|
||||||
|
value: number; // 对于TIME类型,值以分钟为单位;对于SONGS类型,值为歌曲数量
|
||||||
|
endTime?: number; // 何时结束(仅TIME类型)
|
||||||
|
startSongIndex?: number; // 开始时的歌曲索引(对于SONGS类型)
|
||||||
|
remainingSongs?: number; // 剩余歌曲数(对于SONGS类型)
|
||||||
|
}
|
||||||
|
|
||||||
export const usePlayerStore = defineStore('player', () => {
|
export const usePlayerStore = defineStore('player', () => {
|
||||||
const play = ref(false);
|
const play = ref(false);
|
||||||
const isPlay = ref(false);
|
const isPlay = ref(false);
|
||||||
@@ -334,7 +351,38 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
const musicFull = ref(false);
|
const musicFull = ref(false);
|
||||||
const favoriteList = ref<Array<number | string>>(getLocalStorageItem('favoriteList', []));
|
const favoriteList = ref<Array<number | string>>(getLocalStorageItem('favoriteList', []));
|
||||||
const savedPlayProgress = ref<number | undefined>();
|
const savedPlayProgress = ref<number | undefined>();
|
||||||
|
|
||||||
|
// 定时关闭相关状态
|
||||||
|
const sleepTimer = ref<SleepTimerInfo>(getLocalStorageItem('sleepTimer', {
|
||||||
|
type: SleepTimerType.NONE,
|
||||||
|
value: 0
|
||||||
|
}));
|
||||||
|
|
||||||
|
const timerInterval = ref<number | null>(null);
|
||||||
|
|
||||||
|
// 当前定时关闭状态
|
||||||
|
const currentSleepTimer = computed(() => sleepTimer.value);
|
||||||
|
|
||||||
|
// 判断是否有活跃的定时关闭
|
||||||
|
const hasSleepTimerActive = computed(() => sleepTimer.value.type !== SleepTimerType.NONE);
|
||||||
|
|
||||||
|
// 获取剩余时间(用于UI显示)
|
||||||
|
const sleepTimerRemainingTime = computed(() => {
|
||||||
|
if (sleepTimer.value.type === SleepTimerType.TIME && sleepTimer.value.endTime) {
|
||||||
|
const remaining = Math.max(0, sleepTimer.value.endTime - Date.now());
|
||||||
|
return Math.ceil(remaining / 60000); // 转换为分钟并向上取整
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取剩余歌曲数(用于UI显示)
|
||||||
|
const sleepTimerRemainingSongs = computed(() => {
|
||||||
|
if (sleepTimer.value.type === SleepTimerType.SONGS) {
|
||||||
|
return sleepTimer.value.remainingSongs || 0;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
const currentSong = computed(() => playMusic.value);
|
const currentSong = computed(() => playMusic.value);
|
||||||
const isPlaying = computed(() => isPlay.value);
|
const isPlaying = computed(() => isPlay.value);
|
||||||
const currentPlayList = computed(() => playList.value);
|
const currentPlayList = computed(() => playList.value);
|
||||||
@@ -499,6 +547,174 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
setPlayList(list);
|
setPlayList(list);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 睡眠定时器功能
|
||||||
|
const setSleepTimerByTime = (minutes: number) => {
|
||||||
|
// 清除现有定时器
|
||||||
|
clearSleepTimer();
|
||||||
|
|
||||||
|
if (minutes <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const endTime = Date.now() + minutes * 60 * 1000;
|
||||||
|
|
||||||
|
sleepTimer.value = {
|
||||||
|
type: SleepTimerType.TIME,
|
||||||
|
value: minutes,
|
||||||
|
endTime
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存到本地存储
|
||||||
|
localStorage.setItem('sleepTimer', JSON.stringify(sleepTimer.value));
|
||||||
|
|
||||||
|
// 设置定时器检查
|
||||||
|
timerInterval.value = window.setInterval(() => {
|
||||||
|
checkSleepTimer();
|
||||||
|
}, 1000) as unknown as number; // 每秒检查一次
|
||||||
|
|
||||||
|
console.log(`设置定时关闭: ${minutes}分钟后`);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 睡眠定时器功能
|
||||||
|
const setSleepTimerBySongs = (songs: number) => {
|
||||||
|
// 清除现有定时器
|
||||||
|
clearSleepTimer();
|
||||||
|
|
||||||
|
if (songs <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sleepTimer.value = {
|
||||||
|
type: SleepTimerType.SONGS,
|
||||||
|
value: songs,
|
||||||
|
startSongIndex: playListIndex.value,
|
||||||
|
remainingSongs: songs
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存到本地存储
|
||||||
|
localStorage.setItem('sleepTimer', JSON.stringify(sleepTimer.value));
|
||||||
|
|
||||||
|
console.log(`设置定时关闭: 再播放${songs}首歌后`);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 睡眠定时器功能
|
||||||
|
const setSleepTimerAtPlaylistEnd = () => {
|
||||||
|
// 清除现有定时器
|
||||||
|
clearSleepTimer();
|
||||||
|
|
||||||
|
sleepTimer.value = {
|
||||||
|
type: SleepTimerType.PLAYLIST_END,
|
||||||
|
value: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存到本地存储
|
||||||
|
localStorage.setItem('sleepTimer', JSON.stringify(sleepTimer.value));
|
||||||
|
|
||||||
|
console.log('设置定时关闭: 播放列表结束时');
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消定时关闭
|
||||||
|
const clearSleepTimer = () => {
|
||||||
|
if (timerInterval.value) {
|
||||||
|
window.clearInterval(timerInterval.value);
|
||||||
|
timerInterval.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
sleepTimer.value = {
|
||||||
|
type: SleepTimerType.NONE,
|
||||||
|
value: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存到本地存储
|
||||||
|
localStorage.setItem('sleepTimer', JSON.stringify(sleepTimer.value));
|
||||||
|
|
||||||
|
console.log('取消定时关闭');
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查定时关闭是否应该触发
|
||||||
|
const checkSleepTimer = () => {
|
||||||
|
if (sleepTimer.value.type === SleepTimerType.NONE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sleepTimer.value.type === SleepTimerType.TIME && sleepTimer.value.endTime) {
|
||||||
|
if (Date.now() >= sleepTimer.value.endTime) {
|
||||||
|
// 时间到,停止播放
|
||||||
|
stopPlayback();
|
||||||
|
}
|
||||||
|
} else if (sleepTimer.value.type === SleepTimerType.PLAYLIST_END) {
|
||||||
|
// 播放列表结束定时由nextPlay方法处理
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 停止播放并清除定时器
|
||||||
|
const stopPlayback = () => {
|
||||||
|
console.log('定时器触发:停止播放');
|
||||||
|
|
||||||
|
if (isPlaying.value) {
|
||||||
|
setIsPlay(false);
|
||||||
|
audioService.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果使用Electron,发送通知
|
||||||
|
if (window.electron?.ipcRenderer) {
|
||||||
|
window.electron.ipcRenderer.send('show-notification', {
|
||||||
|
title: i18n.global.t('player.sleepTimer.timerEnded'),
|
||||||
|
body: i18n.global.t('player.sleepTimer.playbackStopped')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除定时器
|
||||||
|
clearSleepTimer();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听歌曲变化,处理按歌曲数定时和播放列表结束定时
|
||||||
|
const handleSongChange = () => {
|
||||||
|
console.log('歌曲已切换,检查定时器状态:', sleepTimer.value);
|
||||||
|
|
||||||
|
// 处理按歌曲数定时
|
||||||
|
if (sleepTimer.value.type === SleepTimerType.SONGS && sleepTimer.value.remainingSongs !== undefined) {
|
||||||
|
sleepTimer.value.remainingSongs--;
|
||||||
|
console.log(`剩余歌曲数: ${sleepTimer.value.remainingSongs}`);
|
||||||
|
|
||||||
|
// 保存到本地存储
|
||||||
|
localStorage.setItem('sleepTimer', JSON.stringify(sleepTimer.value));
|
||||||
|
|
||||||
|
if (sleepTimer.value.remainingSongs <= 0) {
|
||||||
|
// 歌曲数到达,停止播放
|
||||||
|
console.log('已播放完设定的歌曲数,停止播放');
|
||||||
|
stopPlayback()
|
||||||
|
setTimeout(() => {
|
||||||
|
stopPlayback();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理播放列表结束定时
|
||||||
|
if (sleepTimer.value.type === SleepTimerType.PLAYLIST_END) {
|
||||||
|
// 检查是否到达播放列表末尾
|
||||||
|
const isLastSong = (playListIndex.value === playList.value.length - 1);
|
||||||
|
|
||||||
|
// 如果是列表最后一首歌且不是循环模式,则设置为在这首歌结束后停止
|
||||||
|
if (isLastSong && playMode.value !== 1) { // 1 是循环模式
|
||||||
|
console.log('已到达播放列表末尾,将在当前歌曲结束后停止播放');
|
||||||
|
// 转换为按歌曲数定时,剩余1首
|
||||||
|
sleepTimer.value = {
|
||||||
|
type: SleepTimerType.SONGS,
|
||||||
|
value: 1,
|
||||||
|
remainingSongs: 1
|
||||||
|
};
|
||||||
|
// 保存到本地存储
|
||||||
|
localStorage.setItem('sleepTimer', JSON.stringify(sleepTimer.value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改nextPlay方法,加入定时关闭检查逻辑
|
||||||
const nextPlay = async () => {
|
const nextPlay = async () => {
|
||||||
// 静态标志,防止多次调用造成递归
|
// 静态标志,防止多次调用造成递归
|
||||||
if ((nextPlay as any).isRunning) {
|
if ((nextPlay as any).isRunning) {
|
||||||
@@ -515,6 +731,19 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查是否是播放列表的最后一首且设置了播放列表结束定时
|
||||||
|
if (playMode.value === 0 && playListIndex.value === playList.value.length - 1 &&
|
||||||
|
sleepTimer.value.type === SleepTimerType.PLAYLIST_END) {
|
||||||
|
// 已是最后一首且为顺序播放模式,触发停止
|
||||||
|
stopPlayback();
|
||||||
|
(nextPlay as any).isRunning = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在切换前保存当前播放状态
|
||||||
|
const shouldPlayNext = play.value;
|
||||||
|
console.log('切换到下一首,当前播放状态:', shouldPlayNext ? '播放' : '暂停');
|
||||||
|
|
||||||
let nowPlayListIndex: number;
|
let nowPlayListIndex: number;
|
||||||
|
|
||||||
if (playMode.value === 2) {
|
if (playMode.value === 2) {
|
||||||
@@ -538,8 +767,8 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
console.log('下一首是B站视频,已清除URL强制重新获取');
|
console.log('下一首是B站视频,已清除URL强制重新获取');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试播放,如果失败会返回false
|
// 尝试播放,并明确传递应该播放的状态
|
||||||
const success = await handlePlayMusic(nextSong);
|
const success = await handlePlayMusic(nextSong, shouldPlayNext);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
console.error('播放下一首失败,将从播放列表中移除此歌曲');
|
console.error('播放下一首失败,将从播放列表中移除此歌曲');
|
||||||
@@ -558,6 +787,9 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 歌曲切换成功,触发歌曲变更处理(用于定时关闭功能)
|
||||||
|
handleSongChange();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('切换下一首出错:', error);
|
console.error('切换下一首出错:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -756,6 +988,7 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
try {
|
try {
|
||||||
// 保存当前播放状态
|
// 保存当前播放状态
|
||||||
const shouldPlay = play.value;
|
const shouldPlay = play.value;
|
||||||
|
console.log('播放音频,当前播放状态:', shouldPlay ? '播放' : '暂停');
|
||||||
|
|
||||||
// 检查是否有保存的进度
|
// 检查是否有保存的进度
|
||||||
let initialPosition = 0;
|
let initialPosition = 0;
|
||||||
@@ -788,6 +1021,7 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 播放新音频,传递是否应该播放的状态
|
// 播放新音频,传递是否应该播放的状态
|
||||||
|
console.log('调用audioService.play,播放状态:', shouldPlay);
|
||||||
const newSound = await audioService.play(playMusicUrl.value, playMusic.value, shouldPlay);
|
const newSound = await audioService.play(playMusicUrl.value, playMusic.value, shouldPlay);
|
||||||
|
|
||||||
// 如果有保存的进度,设置播放位置
|
// 如果有保存的进度,设置播放位置
|
||||||
@@ -796,7 +1030,7 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 发布音频就绪事件,让 MusicHook.ts 来处理设置监听器
|
// 发布音频就绪事件,让 MusicHook.ts 来处理设置监听器
|
||||||
window.dispatchEvent(new CustomEvent('audio-ready', { detail: { sound: newSound } }));
|
window.dispatchEvent(new CustomEvent('audio-ready', { detail: { sound: newSound, shouldPlay } }));
|
||||||
|
|
||||||
// 确保状态与 localStorage 同步
|
// 确保状态与 localStorage 同步
|
||||||
localStorage.setItem('currentPlayMusic', JSON.stringify(playMusic.value));
|
localStorage.setItem('currentPlayMusic', JSON.stringify(playMusic.value));
|
||||||
@@ -842,6 +1076,17 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
musicFull,
|
musicFull,
|
||||||
savedPlayProgress,
|
savedPlayProgress,
|
||||||
favoriteList,
|
favoriteList,
|
||||||
|
|
||||||
|
// 定时关闭相关
|
||||||
|
sleepTimer,
|
||||||
|
currentSleepTimer,
|
||||||
|
hasSleepTimerActive,
|
||||||
|
sleepTimerRemainingTime,
|
||||||
|
sleepTimerRemainingSongs,
|
||||||
|
setSleepTimerByTime,
|
||||||
|
setSleepTimerBySongs,
|
||||||
|
setSleepTimerAtPlaylistEnd,
|
||||||
|
clearSleepTimer,
|
||||||
|
|
||||||
currentSong,
|
currentSong,
|
||||||
isPlaying,
|
isPlaying,
|
||||||
|
|||||||
Reference in New Issue
Block a user