starmusic-pc
1<script setup>
2/**
3 * AppTitleBar — 无边框窗口标题栏
4 *
5 * Wails 的 Frameless 窗口通过 CSS 变量 --wails-draggable 指定拖拽区域:
6 * 标题栏本体为 drag,所有可点击控件必须显式声明 no-drag,否则点击会被拖拽吞掉。
7 */
8import { ref, onMounted, onBeforeUnmount } from 'vue'
9import Icon from './Icon.vue'
10import { miniMode, currentNav } from '../composables/useShell.js'
11import { toast, toastErr } from '../composables/useFeedback.js'
12import {
13 windowMinimise,
14 windowToggleMaximise,
15 windowClose,
16 windowIsMaximised,
17 setMiniMode,
18 setAlwaysOnTop,
19 hasNative,
20} from '../uni/native.js'
21
22const maximised = ref(false)
23const pinned = ref(false)
24const busy = ref(false)
25
26const syncMaximised = async () => {
27 if (!hasNative()) return
28 try {
29 maximised.value = await windowIsMaximised()
30 } catch (e) {
31 /* 忽略 */
32 }
33}
34
35let resizeTimer = null
36const onWindowResize = () => {
37 if (resizeTimer) clearTimeout(resizeTimer)
38 resizeTimer = setTimeout(syncMaximised, 160)
39}
40
41onMounted(() => {
42 syncMaximised()
43 window.addEventListener('resize', onWindowResize)
44})
45
46onBeforeUnmount(() => {
47 window.removeEventListener('resize', onWindowResize)
48 if (resizeTimer) clearTimeout(resizeTimer)
49})
50
51const handleMinimise = () => windowMinimise()
52const handleMaximise = async () => {
53 await windowToggleMaximise()
54 setTimeout(syncMaximised, 60)
55}
56const handleClose = () => windowClose()
57
58const handlePin = async () => {
59 pinned.value = !pinned.value
60 await setAlwaysOnTop(pinned.value)
61 toast(pinned.value ? '窗口已置顶' : '已取消置顶', 'none', 1400)
62}
63
64const handleMini = async () => {
65 if (busy.value) return
66 busy.value = true
67 try {
68 const on = await setMiniMode(!miniMode.value)
69 miniMode.value = on
70 if (on) toast('已进入迷你模式,点击唱片图标可还原', 'none', 2600)
71 } catch (e) {
72 toastErr('切换迷你模式失败')
73 } finally {
74 busy.value = false
75 }
76}
77</script>
78
79<template>
80 <header class="titlebar">
81 <!-- 品牌 -->
82 <div class="brand">
83 <span class="mark">
84 <Icon name="music2" :size="15" :width="2" />
85 </span>
86 <span class="brand-text">
87 <b>星音乐</b>
88 <em>star-music</em>
89 </span>
90 </div>
91
92 <!-- 拖拽区(同时承载当前视图标题,视觉上更饱满) -->
93 <div class="drag-area">
94 <Transition name="crumb" mode="out-in">
95 <span :key="currentNav.key" class="crumb">
96 <span class="crumb-dot" />
97 {{ currentNav.label }}
98 </span>
99 </Transition>
100 </div>
101
102 <!-- 窗口控件 -->
103 <div class="win-actions no-drag">
104 <button
105 class="win-btn"
106 :class="{ 'is-on': pinned }"
107 :title="pinned ? '取消置顶' : '窗口置顶'"
108 @click="handlePin"
109 >
110 <Icon name="pin" :size="15" />
111 </button>
112 <button
113 class="win-btn"
114 :class="{ 'is-on': miniMode }"
115 :title="miniMode ? '退出迷你模式' : '迷你播放器'"
116 @click="handleMini"
117 >
118 <Icon name="miniplayer" :size="15" />
119 </button>
120 <span class="divider" />
121 <button class="win-btn" title="最小化" @click="handleMinimise">
122 <Icon name="minimize-win" :size="15" />
123 </button>
124 <button class="win-btn" :title="maximised ? '还原' : '最大化'" @click="handleMaximise">
125 <Icon :name="maximised ? 'restore-win' : 'maximize-win'" :size="14" />
126 </button>
127 <button class="win-btn is-close" title="关闭" @click="handleClose">
128 <Icon name="close" :size="15" />
129 </button>
130 </div>
131 </header>
132</template>
133
134<style scoped>
135.titlebar {
136 --wails-draggable: drag;
137 display: flex;
138 align-items: center;
139 height: var(--h-titlebar);
140 padding-left: 14px;
141 flex: none;
142 background: var(--surface-1);
143 border-bottom: 1px solid var(--line-1);
144 position: relative;
145 z-index: 30;
146}
147
148/* 顶部一条细腻的主题色渐变,弱化"系统窗口"感 */
149.titlebar::after {
150 content: '';
151 position: absolute;
152 left: 0;
153 right: 0;
154 top: 0;
155 height: 2px;
156 background: linear-gradient(90deg, var(--theme-color), rgba(var(--theme-color-rgb), 0.15) 46%, transparent);
157 opacity: 0.9;
158}
159
160.brand {
161 display: flex;
162 align-items: center;
163 gap: 9px;
164 flex: none;
165}
166
167.mark {
168 display: grid;
169 place-items: center;
170 width: 26px;
171 height: 26px;
172 border-radius: 8px;
173 color: #fff;
174 background: linear-gradient(135deg, var(--theme-color), var(--theme-color-dark));
175 box-shadow: 0 3px 10px rgba(var(--theme-color-rgb), 0.36);
176}
177
178.brand-text {
179 display: flex;
180 align-items: baseline;
181 gap: 6px;
182}
183
184.brand-text b {
185 font-size: 14px;
186 font-weight: 600;
187 letter-spacing: 0.4px;
188 color: var(--text-1);
189}
190
191.brand-text em {
192 font-style: normal;
193 font-size: 10.5px;
194 letter-spacing: 0.6px;
195 color: var(--text-5);
196 text-transform: uppercase;
197}
198
199.drag-area {
200 flex: 1;
201 min-width: 0;
202 height: 100%;
203 display: flex;
204 align-items: center;
205 justify-content: center;
206}
207
208.crumb {
209 display: inline-flex;
210 align-items: center;
211 gap: 6px;
212 font-size: 12.5px;
213 color: var(--text-4);
214 letter-spacing: 0.3px;
215}
216
217.crumb-dot {
218 width: 5px;
219 height: 5px;
220 border-radius: var(--r-full);
221 background: var(--theme-color);
222 box-shadow: 0 0 0 3px rgba(var(--theme-color-rgb), 0.16);
223}
224
225.crumb-enter-active,
226.crumb-leave-active {
227 transition: opacity var(--t-base) var(--ease-out), transform var(--t-base) var(--ease-out);
228}
229
230.crumb-enter-from {
231 opacity: 0;
232 transform: translateY(6px);
233}
234
235.crumb-leave-to {
236 opacity: 0;
237 transform: translateY(-6px);
238}
239
240.win-actions {
241 --wails-draggable: no-drag;
242 display: flex;
243 align-items: center;
244 gap: 2px;
245 padding-right: 6px;
246 flex: none;
247}
248
249.win-btn {
250 display: grid;
251 place-items: center;
252 width: 34px;
253 height: 30px;
254 border-radius: var(--r-sm);
255 color: var(--text-3);
256 transition: background-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
257}
258
259.win-btn:hover {
260 background: var(--surface-3);
261 color: var(--text-1);
262}
263
264.win-btn.is-on {
265 color: var(--theme-color);
266 background: var(--theme-color-soft);
267}
268
269.win-btn.is-close:hover {
270 background: var(--color-danger);
271 color: #fff;
272}
273
274.divider {
275 width: 1px;
276 height: 16px;
277 margin: 0 5px;
278 background: var(--line-2);
279}
280</style>