仰望星辰工作室

starmusic-pc

starmusic-pc/ frontend/src/components/MiniPlayer.vue 6.6 KB · 282 行 原始文件
1<script setup>
2/**
3 * MiniPlayer — 迷你播放器
4 *
5 * 进入迷你模式后窗口被缩到 460×148 并置顶,只保留最核心的播放控制:
6 * 封面 / 歌名歌手 / 上一曲 / 播放暂停 / 下一曲 / 进度 / 还原按钮。
7 */
8import { computed, ref } from 'vue'
9import Icon from './Icon.vue'
10import CoverImage from './CoverImage.vue'
11import { playerState, playerActions, PLAY_MODE_ICON } from '../composables/usePlayer.js'
12import { miniMode, openNowPlaying, closeNowPlaying, nowPlayingOpen } from '../composables/useShell.js'
13import { setMiniMode } from '../uni/native.js'
14import { formatPlayTime2 } from '../common/utils.js'
15import { toastErr } from '../composables/useFeedback.js'
16
17const bounceKey = ref(0)
18
19const hasMusic = computed(() => !!playerState.musicInfo)
20const isPlay = computed(() => playerState.isPlay)
21
22const progress = computed(() => {
23 const { currentTime, duration } = playerState
24 if (!duration || duration <= 0) return 0
25 return Math.min(100, Math.max(0, (currentTime / duration) * 100))
26})
27
28const handleToggle = () => {
29 if (!hasMusic.value) return
30 bounceKey.value++
31 playerActions.togglePlay()
32}
33
34const restore = async () => {
35 try {
36 const on = await setMiniMode(false)
37 miniMode.value = on
38 } catch (e) {
39 toastErr('还原窗口失败')
40 }
41}
42
43const eqBars = [0, 1, 2, 3]
44</script>
45
46<template>
47 <div class="mini">
48 <!-- 顶部拖拽条 -->
49 <div class="mini-drag">
50 <span class="mini-brand">
51 <Icon name="music2" :size="12" :width="2.2" />
52 星音乐
53 </span>
54 <div class="mini-win no-drag">
55 <button class="mini-win-btn" title="还原窗口" @click="restore">
56 <Icon name="restore-win" :size="13" />
57 </button>
58 </div>
59 </div>
60
61 <div class="mini-body">
62 <CoverImage
63 :src="playerState.pic"
64 :size="72"
65 radius="var(--r-lg)"
66 :active="isPlay"
67 :icon-size="28"
68 class="mini-cover"
69 @click="hasMusic && openNowPlaying()"
70 />
71
72 <div class="mini-main">
73 <div class="mini-title-row">
74 <span class="mini-name ellipsis">{{ hasMusic ? playerState.musicInfo.name || '未知歌曲' : '未在播放' }}</span>
75 <span v-if="isPlay" class="mini-eq">
76 <i v-for="b in eqBars" :key="b" :style="{ animationDelay: `${b * 130}ms` }" />
77 </span>
78 </div>
79 <span class="mini-singer ellipsis">{{ hasMusic ? playerState.musicInfo.singer || '未知歌手' : '选一首歌开始' }}</span>
80
81 <div class="mini-controls">
82 <button class="mc" :title="PLAY_MODE_ICON[playerState.playMethod]" @click="playerActions.togglePlayMethod()">
83 <Icon :name="PLAY_MODE_ICON[playerState.playMethod] || 'repeat'" :size="14" />
84 </button>
85 <button class="mc" title="上一曲" :disabled="!hasMusic" @click="playerActions.playPrev()">
86 <Icon name="prev" :size="14" />
87 </button>
88 <button
89 :key="bounceKey"
90 class="mc is-primary"
91 :class="{ 'a-bounce': bounceKey > 0 }"
92 :title="isPlay ? '暂停' : '播放'"
93 :disabled="!hasMusic"
94 @click="handleToggle"
95 >
96 <Icon :name="isPlay ? 'pause' : 'play'" :size="15" />
97 </button>
98 <button class="mc" title="下一曲" :disabled="!hasMusic" @click="playerActions.playNext(false)">
99 <Icon name="next" :size="14" />
100 </button>
101 <span class="mini-time">{{ formatPlayTime2(playerState.currentTime) }}</span>
102 </div>
103 </div>
104 </div>
105
106 <!-- 底部进度细条 -->
107 <div class="mini-progress">
108 <span class="mini-progress-fill" :style="{ width: `${progress}%` }" />
109 </div>
110 </div>
111</template>
112
113<style scoped>
114.mini {
115 height: 100vh;
116 display: flex;
117 flex-direction: column;
118 background: var(--surface-1);
119 position: relative;
120 overflow: hidden;
121}
122
123.mini::after {
124 content: '';
125 position: absolute;
126 left: 0;
127 right: 0;
128 top: 0;
129 height: 2px;
130 background: linear-gradient(90deg, var(--theme-color), rgba(var(--theme-color-rgb), 0.2) 60%, transparent);
131}
132
133.mini-drag {
134 --wails-draggable: drag;
135 flex: none;
136 height: 30px;
137 display: flex;
138 align-items: center;
139 justify-content: space-between;
140 padding: 0 6px 0 12px;
141}
142
143.mini-brand {
144 display: inline-flex;
145 align-items: center;
146 gap: 6px;
147 font-size: 11.5px;
148 font-weight: 600;
149 letter-spacing: 0.6px;
150 color: var(--text-4);
151}
152
153.mini-win {
154 --wails-draggable: no-drag;
155}
156
157.mini-win-btn {
158 display: grid;
159 place-items: center;
160 width: 26px;
161 height: 24px;
162 border-radius: var(--r-sm);
163 color: var(--text-3);
164 transition: background-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
165}
166
167.mini-win-btn:hover {
168 background: var(--surface-3);
169 color: var(--theme-color);
170}
171
172.mini-body {
173 flex: 1;
174 min-height: 0;
175 display: flex;
176 align-items: center;
177 gap: 12px;
178 padding: 0 14px 6px;
179}
180
181.mini-cover {
182 cursor: pointer;
183 flex: none;
184}
185
186.mini-main {
187 flex: 1;
188 min-width: 0;
189 display: flex;
190 flex-direction: column;
191 gap: 2px;
192}
193
194.mini-title-row {
195 display: flex;
196 align-items: center;
197 gap: 7px;
198 min-width: 0;
199}
200
201.mini-name {
202 font-size: 13.5px;
203 font-weight: 600;
204 color: var(--text-1);
205}
206
207.mini-eq {
208 display: flex;
209 align-items: flex-end;
210 gap: 2px;
211 height: 11px;
212 flex: none;
213}
214
215.mini-eq i {
216 width: 2px;
217 height: 100%;
218 border-radius: 2px;
219 background: var(--theme-color);
220 transform-origin: bottom center;
221 animation: kEq 900ms var(--ease-in-out) infinite;
222}
223
224.mini-singer {
225 font-size: 11px;
226 color: var(--text-4);
227}
228
229.mini-controls {
230 display: flex;
231 align-items: center;
232 gap: 3px;
233 margin-top: 3px;
234}
235
236.mc {
237 display: grid;
238 place-items: center;
239 width: 27px;
240 height: 27px;
241 border-radius: var(--r-full);
242 color: var(--text-3);
243 transition: background-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out),
244 transform var(--t-fast) var(--ease-out);
245}
246
247.mc:hover:not(:disabled) {
248 background: var(--surface-3);
249 color: var(--text-1);
250}
251
252.mc:disabled {
253 opacity: 0.38;
254 cursor: not-allowed;
255}
256
257.mc.is-primary {
258 color: #fff;
259 background: linear-gradient(135deg, var(--theme-color), var(--theme-color-dark));
260 box-shadow: 0 3px 10px rgba(var(--theme-color-rgb), 0.36);
261}
262
263.mini-time {
264 margin-left: auto;
265 font-size: 10.5px;
266 font-variant-numeric: tabular-nums;
267 color: var(--text-4);
268}
269
270.mini-progress {
271 flex: none;
272 height: 3px;
273 background: var(--surface-4);
274}
275
276.mini-progress-fill {
277 display: block;
278 height: 100%;
279 background: linear-gradient(90deg, var(--theme-color), var(--theme-color-dark));
280 transition: width 220ms linear;
281}
282</style>