|
|
@@ -1,410 +0,0 @@
|
|
|
-<template>
|
|
|
- <!-- 音乐控制面板 -->
|
|
|
- <van-popup
|
|
|
- v-model:show="panelVisible"
|
|
|
- position="bottom"
|
|
|
- round
|
|
|
- :style="{ background: 'var(--bg-secondary, #1a1a2e)' }"
|
|
|
- >
|
|
|
- <div class="music-panel">
|
|
|
- <!-- 金色渐变标题栏 -->
|
|
|
- <div class="panel-header">
|
|
|
- <div class="header-left">
|
|
|
- <svg class="header-icon" viewBox="0 0 24 24" fill="none">
|
|
|
- <path d="M9 18V5l12-2v13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
|
- <circle cx="6" cy="18" r="3" stroke="currentColor" stroke-width="2"/>
|
|
|
- <circle cx="18" cy="16" r="3" stroke="currentColor" stroke-width="2"/>
|
|
|
- </svg>
|
|
|
- <span class="header-title">{{ $t('music.control') }}</span>
|
|
|
- </div>
|
|
|
- <div class="header-close" @click="panelVisible = false">
|
|
|
- <van-icon name="cross" />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="panel-body">
|
|
|
- <!-- 背景音乐开关 — 立即生效 -->
|
|
|
- <div class="control-item">
|
|
|
- <span class="control-label">{{ $t('music.bgMusic') }}</span>
|
|
|
- <div class="control-right">
|
|
|
- <span class="switch-text">{{ musicStore.isEnabled ? $t('music.on') : $t('music.off') }}</span>
|
|
|
- <van-switch
|
|
|
- :model-value="musicStore.isEnabled"
|
|
|
- @update:model-value="musicStore.setEnabled"
|
|
|
- size="22px"
|
|
|
- active-color="#e8a820"
|
|
|
- />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 播放控制栏:上一首 / 播放暂停 / 下一首 / 播放模式 -->
|
|
|
- <div class="control-item playback-controls">
|
|
|
- <span class="control-label">{{ $t('music.playControl') }}</span>
|
|
|
- <div class="playback-btns">
|
|
|
- <!-- 上一首 -->
|
|
|
- <div class="ctrl-btn" @click="musicStore.prev()">
|
|
|
- <svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg>
|
|
|
- </div>
|
|
|
- <!-- 播放/暂停 -->
|
|
|
- <div class="ctrl-btn ctrl-btn-main" @click="onTogglePlay">
|
|
|
- <svg v-if="musicStore.isPlaying" viewBox="0 0 24 24" fill="currentColor"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
|
|
|
- <svg v-else viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
|
|
- </div>
|
|
|
- <!-- 下一首 -->
|
|
|
- <div class="ctrl-btn" @click="musicStore.next()">
|
|
|
- <svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg>
|
|
|
- </div>
|
|
|
- <!-- 播放模式 — 点击循环切换 -->
|
|
|
- <div class="ctrl-btn" @click="musicStore.cyclePlayMode()">
|
|
|
- <!-- 顺序播放 -->
|
|
|
- <svg v-if="musicStore.playMode === 'sequential'" viewBox="0 0 24 24" fill="currentColor">
|
|
|
- <path d="M15 6H3v2h12V6zm0 4H3v2h12v-2zM3 16h8v-2H3v2zM17 6v8.18c-.31-.11-.65-.18-1-.18-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3V8h3V6h-5z"/>
|
|
|
- </svg>
|
|
|
- <!-- 单曲循环 -->
|
|
|
- <svg v-else-if="musicStore.playMode === 'loop'" viewBox="0 0 24 24" fill="currentColor">
|
|
|
- <path d="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4zm-4-2V9h-1l-2 1v1h1.5v4H13z"/>
|
|
|
- </svg>
|
|
|
- <!-- 随机播放 -->
|
|
|
- <svg v-else viewBox="0 0 24 24" fill="currentColor">
|
|
|
- <path d="M10.59 9.17L5.41 4 4 5.41l5.17 5.17 1.42-1.41zM14.5 4l2.04 2.04L4 18.59 5.41 20 17.96 7.46 20 9.5V4h-5.5zm.33 9.41l-1.41 1.41 3.13 3.13L14.5 20H20v-5.5l-2.04 2.04-3.13-3.13z"/>
|
|
|
- </svg>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 当前播放信息 -->
|
|
|
- <div v-if="musicStore.currentSongTitle" class="control-item now-playing">
|
|
|
- <span class="control-label">{{ $t('music.nowPlaying') }}</span>
|
|
|
- <span class="playing-title">{{ musicStore.currentSongTitle }}</span>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 播放模式提示 -->
|
|
|
- <div class="mode-hint">
|
|
|
- {{ playModeLabel }}
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 音乐列表 -->
|
|
|
- <div class="song-list-title">{{ $t('music.songList') }}</div>
|
|
|
- <div class="song-list">
|
|
|
- <div
|
|
|
- v-for="song in musicStore.allSongs"
|
|
|
- :key="song.id"
|
|
|
- class="song-item"
|
|
|
- :class="{ active: musicStore.currentSong?.id === song.id && musicStore.isPlaying }"
|
|
|
- @click="onSelectSong(song)"
|
|
|
- >
|
|
|
- <div class="song-info">
|
|
|
- <div class="song-title">{{ song.title }}</div>
|
|
|
- <div v-if="song.artist" class="song-artist">{{ song.artist }}</div>
|
|
|
- </div>
|
|
|
- <div class="song-right">
|
|
|
- <span v-if="song.duration" class="song-duration">{{ formatDuration(song.duration) }}</span>
|
|
|
- <div v-if="musicStore.currentSong?.id === song.id && musicStore.isPlaying" class="playing-indicator">
|
|
|
- <span></span><span></span><span></span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div v-if="!musicStore.allSongs.length" class="song-empty">{{ $t('music.noSongs') }}</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </van-popup>
|
|
|
-</template>
|
|
|
-
|
|
|
-<script setup lang="ts">
|
|
|
-import { computed, onMounted } from "vue";
|
|
|
-import { useMusicStore } from "@/store/modules/musicStore";
|
|
|
-import { useI18n } from "vue-i18n";
|
|
|
-import type { MusicItem } from "@/api/music";
|
|
|
-
|
|
|
-const { t } = useI18n();
|
|
|
-const musicStore = useMusicStore();
|
|
|
-
|
|
|
-const panelVisible = computed({
|
|
|
- get: () => musicStore.isPanelOpen,
|
|
|
- set: (val: boolean) => { musicStore.isPanelOpen = val; }
|
|
|
-});
|
|
|
-
|
|
|
-const playModeLabel = computed(() => {
|
|
|
- const map: Record<string, string> = {
|
|
|
- sequential: t("music.sequential"),
|
|
|
- shuffle: t("music.shuffle"),
|
|
|
- loop: t("music.loop"),
|
|
|
- };
|
|
|
- return map[musicStore.playMode] || t("music.sequential");
|
|
|
-});
|
|
|
-
|
|
|
-function onTogglePlay() {
|
|
|
- if (!musicStore.isEnabled) {
|
|
|
- musicStore.setEnabled(true);
|
|
|
- } else if (musicStore.isPlaying) {
|
|
|
- musicStore.pause();
|
|
|
- } else {
|
|
|
- musicStore.play();
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-function onSelectSong(song: MusicItem) {
|
|
|
- musicStore.selectSong(song.id);
|
|
|
- if (!musicStore.isEnabled) {
|
|
|
- musicStore.setEnabled(true);
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-function formatDuration(seconds: number): string {
|
|
|
- const m = Math.floor(seconds / 60);
|
|
|
- const s = seconds % 60;
|
|
|
- return `${m}:${s.toString().padStart(2, "0")}`;
|
|
|
-}
|
|
|
-
|
|
|
-onMounted(async () => {
|
|
|
- await musicStore.loadSongs();
|
|
|
- musicStore.restoreSettings();
|
|
|
-});
|
|
|
-</script>
|
|
|
-
|
|
|
-<style lang="scss" scoped>
|
|
|
-.music-panel {
|
|
|
- color: var(--text-primary, #fff);
|
|
|
- max-height: 80vh;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
-
|
|
|
- // ========== 金色渐变标题栏 ==========
|
|
|
- .panel-header {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- padding: 16px 20px;
|
|
|
- background: linear-gradient(135deg, #b8860b 0%, #daa520 50%, #f0c040 100%);
|
|
|
- border-radius: 16px 16px 0 0;
|
|
|
- flex-shrink: 0;
|
|
|
-
|
|
|
- .header-left {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 8px;
|
|
|
-
|
|
|
- .header-icon {
|
|
|
- width: 22px;
|
|
|
- height: 22px;
|
|
|
- color: #fff;
|
|
|
- }
|
|
|
-
|
|
|
- .header-title {
|
|
|
- font-size: 16px;
|
|
|
- font-weight: 700;
|
|
|
- color: #fff;
|
|
|
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- .header-close {
|
|
|
- width: 28px;
|
|
|
- height: 28px;
|
|
|
- border-radius: 50%;
|
|
|
- background: rgba(0, 0, 0, 0.2);
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- cursor: pointer;
|
|
|
- color: #fff;
|
|
|
- font-size: 14px;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // ========== 控制区域 ==========
|
|
|
- .panel-body {
|
|
|
- padding: 8px 20px 24px;
|
|
|
- overflow-y: auto;
|
|
|
- flex: 1;
|
|
|
-
|
|
|
- .control-item {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- padding: 14px 0;
|
|
|
- border-bottom: 1px solid var(--border-color, rgba(255, 255, 255, 0.06));
|
|
|
-
|
|
|
- .control-label {
|
|
|
- font-size: 14px;
|
|
|
- font-weight: 500;
|
|
|
- color: var(--text-primary, #fff);
|
|
|
- flex-shrink: 0;
|
|
|
- }
|
|
|
-
|
|
|
- .control-right {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 10px;
|
|
|
-
|
|
|
- .switch-text {
|
|
|
- font-size: 13px;
|
|
|
- color: var(--text-secondary, rgba(255, 255, 255, 0.5));
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 当前播放
|
|
|
- &.now-playing {
|
|
|
- .playing-title {
|
|
|
- font-size: 13px;
|
|
|
- color: var(--color-accent, #e8a820);
|
|
|
- max-width: 200px;
|
|
|
- overflow: hidden;
|
|
|
- text-overflow: ellipsis;
|
|
|
- white-space: nowrap;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // ========== 播放控制栏 ==========
|
|
|
- .playback-controls {
|
|
|
- .playback-btns {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 8px;
|
|
|
-
|
|
|
- .ctrl-btn {
|
|
|
- width: 36px;
|
|
|
- height: 36px;
|
|
|
- border-radius: 50%;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- background: var(--bg-tertiary, rgba(255, 255, 255, 0.06));
|
|
|
- cursor: pointer;
|
|
|
- transition: background 0.2s, transform 0.15s;
|
|
|
- color: var(--text-primary, #fff);
|
|
|
-
|
|
|
- &:active {
|
|
|
- transform: scale(0.9);
|
|
|
- }
|
|
|
-
|
|
|
- svg {
|
|
|
- width: 18px;
|
|
|
- height: 18px;
|
|
|
- }
|
|
|
-
|
|
|
- &.ctrl-btn-main {
|
|
|
- width: 42px;
|
|
|
- height: 42px;
|
|
|
- background: linear-gradient(135deg, #b8860b 0%, #daa520 50%, #f0c040 100%);
|
|
|
- color: #fff;
|
|
|
-
|
|
|
- svg {
|
|
|
- width: 22px;
|
|
|
- height: 22px;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // ========== 模式提示 ==========
|
|
|
- .mode-hint {
|
|
|
- text-align: center;
|
|
|
- font-size: 12px;
|
|
|
- color: var(--text-secondary, rgba(255, 255, 255, 0.4));
|
|
|
- padding: 6px 0 2px;
|
|
|
- }
|
|
|
-
|
|
|
- // ========== 歌曲列表 ==========
|
|
|
- .song-list-title {
|
|
|
- font-size: 14px;
|
|
|
- font-weight: 600;
|
|
|
- color: var(--text-primary, #fff);
|
|
|
- padding: 14px 0 8px;
|
|
|
- }
|
|
|
-
|
|
|
- .song-list {
|
|
|
- max-height: 280px;
|
|
|
- overflow-y: auto;
|
|
|
-
|
|
|
- .song-item {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- padding: 12px;
|
|
|
- border-radius: 10px;
|
|
|
- cursor: pointer;
|
|
|
- transition: background 0.2s;
|
|
|
-
|
|
|
- &:active {
|
|
|
- background: var(--bg-hover, rgba(255, 255, 255, 0.08));
|
|
|
- }
|
|
|
-
|
|
|
- &.active {
|
|
|
- background: var(--bg-hover, rgba(255, 255, 255, 0.06));
|
|
|
-
|
|
|
- .song-title {
|
|
|
- color: var(--color-accent, #e8a820);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- .song-info {
|
|
|
- flex: 1;
|
|
|
- min-width: 0;
|
|
|
-
|
|
|
- .song-title {
|
|
|
- font-size: 14px;
|
|
|
- color: var(--text-primary, #fff);
|
|
|
- white-space: nowrap;
|
|
|
- overflow: hidden;
|
|
|
- text-overflow: ellipsis;
|
|
|
- }
|
|
|
-
|
|
|
- .song-artist {
|
|
|
- font-size: 12px;
|
|
|
- color: var(--text-secondary, rgba(255, 255, 255, 0.45));
|
|
|
- margin-top: 2px;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- .song-right {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 8px;
|
|
|
- flex-shrink: 0;
|
|
|
-
|
|
|
- .song-duration {
|
|
|
- font-size: 12px;
|
|
|
- color: var(--text-secondary, rgba(255, 255, 255, 0.4));
|
|
|
- }
|
|
|
-
|
|
|
- // 播放指示器动画
|
|
|
- .playing-indicator {
|
|
|
- display: flex;
|
|
|
- align-items: flex-end;
|
|
|
- gap: 2px;
|
|
|
- height: 14px;
|
|
|
-
|
|
|
- span {
|
|
|
- display: block;
|
|
|
- width: 3px;
|
|
|
- background: var(--color-accent, #e8a820);
|
|
|
- border-radius: 1px;
|
|
|
- animation: musicBar 0.8s ease-in-out infinite;
|
|
|
-
|
|
|
- &:nth-child(1) { height: 6px; animation-delay: 0s; }
|
|
|
- &:nth-child(2) { height: 10px; animation-delay: 0.2s; }
|
|
|
- &:nth-child(3) { height: 4px; animation-delay: 0.4s; }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- .song-empty {
|
|
|
- text-align: center;
|
|
|
- padding: 30px 0;
|
|
|
- font-size: 13px;
|
|
|
- color: var(--text-secondary, rgba(255, 255, 255, 0.35));
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-@keyframes musicBar {
|
|
|
- 0%, 100% { transform: scaleY(1); }
|
|
|
- 50% { transform: scaleY(2); }
|
|
|
-}
|
|
|
-</style>
|