仰望星辰工作室

starmusic-pc

starmusic-pc/ frontend/src/views/AboutView.vue 13.8 KB · 567 行 原始文件
1<script setup>
2/**
3 * AboutView — 关于
4 *
5 * 品牌介绍 + 星空主题装饰卡 + 隐藏彩蛋(连点版本号 7 次触发「星辰跳一跳」)。
6 * 页面刻意使用深色星空风格,与全局浅色主题区分,作为「惊喜」时刻。
7 */
8import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
9import Icon from '../components/Icon.vue'
10import StarJumpGame from '../components/StarJumpGame.vue'
11import { getAppVersion } from '../common/update.js'
12import { currentTheme } from '../composables/useTheme.js'
13import { toast, toastOk } from '../composables/useFeedback.js'
14import { openExternal } from '../uni/native.js'
15
16const version = ref(getAppVersion())
17const showGame = ref(false)
18const unlocked = ref([])
19
20const MESSAGES = ['每一颗星星,都是一个未说出口的愿望', '愿你像星辰一样,即使微弱,也始终发光', '仰望星辰的人,终会成为别人的光', '夏日之瓜 · 仰望星辰工作室 · 感谢你发现这里']
21const THRESHOLDS = [3, 6, 10, 15]
22
23// ============ 星空 ============
24
25const stars = Array.from({ length: 36 }, (_, i) => ({
26 id: i,
27 x: Math.random() * 100,
28 y: Math.random() * 100,
29 size: 1 + Math.random() * 2.4,
30 delay: Math.random() * 4,
31 duration: 2.4 + Math.random() * 3.2,
32}))
33
34// ============ 隐藏入口:连点 7 次 ============
35
36const TAP_TARGET = 7
37const taps = ref(0)
38let resetTimer = null
39
40const handleVersionTap = () => {
41 taps.value += 1
42 if (resetTimer) clearTimeout(resetTimer)
43 resetTimer = setTimeout(() => {
44 taps.value = 0
45 }, 5000)
46
47 if (taps.value >= TAP_TARGET) {
48 taps.value = 0
49 if (resetTimer) clearTimeout(resetTimer)
50 showGame.value = true
51 toastOk('发现隐藏彩蛋:星辰跳一跳')
52 return
53 }
54 if (taps.value >= 3) {
55 toast(`再点 ${TAP_TARGET - taps.value} 次试试`, 'none', 1400)
56 }
57}
58
59const onGameScore = (score) => {
60 const list = []
61 THRESHOLDS.forEach((t, i) => {
62 if (score >= t) list.push(MESSAGES[i])
63 })
64 unlocked.value = list
65}
66
67onBeforeUnmount(() => {
68 if (resetTimer) clearTimeout(resetTimer)
69})
70
71const openHomepage = async () => {
72 const ok = await openExternal('https://starmusic.fangqihang.cn/')
73 if (!ok) toast('链接已复制到剪贴板', 'none', 2000)
74}
75
76const stack = [
77 { icon: 'play', label: '播放内核', value: 'HTMLAudio + 事件总线' },
78 { icon: 'database', label: '音源协议', value: 'LX Music 兼容沙箱' },
79 { icon: 'palette', label: '界面', value: 'Vue 3 + 自绘设计系统' },
80 { icon: 'shield', label: '外壳', value: 'Wails / WebView2' },
81]
82</script>
83
84<template>
85 <div class="view-about view-scroll scroll-y">
86 <div class="sky-deco">
87 <span
88 v-for="s in stars"
89 :key="s.id"
90 class="star"
91 :style="{
92 left: `${s.x}%`,
93 top: `${s.y}%`,
94 width: `${s.size}px`,
95 height: `${s.size}px`,
96 animationDelay: `${s.delay}s`,
97 animationDuration: `${s.duration}s`,
98 }"
99 />
100 </div>
101
102 <div class="inner">
103 <!-- ============ 品牌 ============ -->
104 <section class="hero a-fade-up">
105 <div class="logo">
106 <span class="logo-mark">
107 <Icon name="music2" :size="34" :width="2" />
108 </span>
109 <span class="logo-ring" />
110 </div>
111 <h1>星音乐</h1>
112 <p class="hero-sub">star-music · 夏日之瓜 · 仰望星辰工作室出品</p>
113 <div class="hero-tags">
114 <span class="tag">多平台聚合</span>
115 <span class="tag">自定义音源</span>
116 <span class="tag">云同步歌单</span>
117 <span class="tag">节拍可视化</span>
118 </div>
119 </section>
120
121 <!-- ============ 星空寄语卡 ============ -->
122 <section class="star-card a-fade-up" style="--delay: 80ms">
123 <div class="card-sky">
124 <span class="moon" />
125 <span class="shooting" />
126 <span class="shooting s2" />
127 <span
128 v-for="s in stars.slice(0, 22)"
129 :key="`c${s.id}`"
130 class="star"
131 :style="{
132 left: `${s.x}%`,
133 top: `${s.y}%`,
134 width: `${s.size}px`,
135 height: `${s.size}px`,
136 animationDelay: `${s.delay}s`,
137 animationDuration: `${s.duration}s`,
138 }"
139 />
140 </div>
141 <div class="card-content">
142 <p class="card-title">愿每一次仰望,都有星光回应</p>
143 <p class="card-sub">
144 星音乐把「找歌 → 试听 → 收藏 → 播放」这一条链路做成了一个安静的小工具。
145 它不提供任何音乐文件,只把你自己导入的音源与官方公开接口连起来。
146 </p>
147 </div>
148 </section>
149
150 <!-- ============ 已解锁寄语 ============ -->
151 <TransitionGroup name="unlock" tag="section" class="unlock-list">
152 <div v-for="(text, i) in unlocked" :key="text" class="unlock-item" :style="{ '--delay': `${i * 70}ms` }">
153 <Icon name="sparkle" :size="14" />
154 <span>{{ text }}</span>
155 </div>
156 </TransitionGroup>
157
158 <!-- ============ 技术栈 ============ -->
159 <section class="stack a-fade-up" style="--delay: 160ms">
160 <div v-for="item in stack" :key="item.label" class="stack-item">
161 <span class="stack-icon"><Icon :name="item.icon" :size="15" /></span>
162 <span class="stack-label">{{ item.label }}</span>
163 <span class="stack-value">{{ item.value }}</span>
164 </div>
165 </section>
166
167 <!-- ============ 责任声明 ============ -->
168 <section class="notice a-fade-up" style="--delay: 220ms">
169 <p class="notice-title">
170 <Icon name="info" :size="14" />
171 责任声明
172 </p>
173 <p class="notice-text">
174 本项目仅用于技术学习与交流,不提供任何音乐文件的存储与分发。所有播放数据均来自你自行导入的第三方音源,
175 版权归原作者及版权方所有。请勿用于商业用途,并在法律允许的范围内使用。
176 </p>
177 </section>
178
179 <!-- ============ 页脚(彩蛋入口) ============ -->
180 <footer class="foot a-fade-up" style="--delay: 280ms">
181 <button class="ver-btn press" title="嗯?多点几下试试" @click="handleVersionTap">
182 <Icon name="star4" :size="13" />
183 <span>v{{ version }}</span>
184 <em v-if="taps > 0 && taps < TAP_TARGET">{{ taps }}/{{ TAP_TARGET }}</em>
185 </button>
186 <span class="sep">·</span>
187 <span>{{ currentTheme.name }}</span>
188 <span class="sep">·</span>
189 <button class="link-btn" @click="openHomepage">
190 官网
191 <Icon name="external" :size="11" />
192 </button>
193 <span class="sep">·</span>
194 <span>© 2026 仰望星辰工作室</span>
195 </footer>
196 </div>
197
198 <!-- ============ 彩蛋游戏 ============ -->
199 <Transition name="game">
200 <div v-if="showGame" class="game-mask" @click.self="showGame = false">
201 <StarJumpGame
202 :color="currentTheme.color"
203 class="game-panel"
204 @score="onGameScore"
205 @close="showGame = false"
206 />
207 </div>
208 </Transition>
209 </div>
210</template>
211
212<style scoped>
213.view-about {
214 position: relative;
215 height: 100%;
216 min-height: 0;
217 padding: 26px 32px 40px;
218 background: radial-gradient(120% 80% at 20% 0%, #1a2740 0%, #121c2c 42%, #0d141f 100%);
219 overflow-x: hidden;
220}
221
222.sky-deco {
223 position: absolute;
224 inset: 0;
225 pointer-events: none;
226 opacity: 0.7;
227}
228
229.star {
230 position: absolute;
231 border-radius: var(--r-full);
232 background: #fff;
233 animation: kTwinkle 3s var(--ease-in-out) infinite;
234}
235
236.inner {
237 position: relative;
238 z-index: 2;
239 max-width: 820px;
240 margin: 0 auto;
241 display: flex;
242 flex-direction: column;
243 gap: 16px;
244}
245
246/* ---------- 品牌 ---------- */
247
248.hero {
249 display: flex;
250 flex-direction: column;
251 align-items: center;
252 text-align: center;
253 padding: 14px 0 6px;
254}
255
256.logo {
257 position: relative;
258 display: grid;
259 place-items: center;
260 width: 84px;
261 height: 84px;
262 margin-bottom: 18px;
263}
264
265.logo-mark {
266 display: grid;
267 place-items: center;
268 width: 84px;
269 height: 84px;
270 border-radius: 26px;
271 color: #fff;
272 background: linear-gradient(140deg, var(--theme-color), var(--theme-color-dark));
273 box-shadow: 0 16px 40px rgba(var(--theme-color-rgb), 0.42);
274 animation: kBreath 5s var(--ease-in-out) infinite;
275}
276
277.logo-ring {
278 position: absolute;
279 inset: -12px;
280 border-radius: 38px;
281 border: 1.5px solid rgba(var(--theme-color-rgb), 0.4);
282 animation: kPulseRing 3.6s var(--ease-out) infinite;
283}
284
285.hero h1 {
286 font-size: 30px;
287 font-weight: 700;
288 letter-spacing: 7px;
289 text-indent: 7px;
290 color: #fff;
291}
292
293.hero-sub {
294 margin-top: 9px;
295 font-size: 12.5px;
296 letter-spacing: 1.4px;
297 color: rgba(226, 236, 250, 0.6);
298}
299
300.hero-tags {
301 display: flex;
302 flex-wrap: wrap;
303 justify-content: center;
304 gap: 7px;
305 margin-top: 16px;
306}
307
308.tag {
309 padding: 4px 11px;
310 border-radius: var(--r-full);
311 font-size: 11.5px;
312 color: rgba(226, 236, 250, 0.82);
313 background: rgba(255, 255, 255, 0.08);
314 border: 1px solid rgba(255, 255, 255, 0.1);
315}
316
317/* ---------- 寄语卡 ---------- */
318
319.star-card {
320 position: relative;
321 border-radius: var(--r-xl);
322 overflow: hidden;
323 border: 1px solid rgba(255, 255, 255, 0.12);
324 background: rgba(255, 255, 255, 0.05);
325 backdrop-filter: blur(20px) saturate(150%);
326 -webkit-backdrop-filter: blur(20px) saturate(150%);
327 box-shadow: 0 24px 60px rgba(0, 0, 0, 0.36);
328}
329
330.card-sky {
331 position: absolute;
332 inset: 0;
333 pointer-events: none;
334}
335
336.moon {
337 position: absolute;
338 top: 22px;
339 right: 30px;
340 width: 42px;
341 height: 42px;
342 border-radius: var(--r-full);
343 background: radial-gradient(circle at 34% 34%, #fff, #dfe9ff 60%, #b9c8e6);
344 box-shadow: 0 0 34px rgba(220, 232, 255, 0.6);
345}
346
347.shooting {
348 position: absolute;
349 top: 24%;
350 right: 12%;
351 width: 120px;
352 height: 1.5px;
353 border-radius: var(--r-full);
354 background: linear-gradient(90deg, rgba(255, 255, 255, 0.95), transparent);
355 animation: kShooting 6s var(--ease-out) infinite;
356 animation-delay: 1.4s;
357}
358
359.shooting.s2 {
360 top: 54%;
361 right: 30%;
362 width: 80px;
363 animation-delay: 3.8s;
364 opacity: 0.7;
365}
366
367.card-content {
368 position: relative;
369 z-index: 2;
370 padding: 30px 28px;
371}
372
373.card-title {
374 font-size: 18px;
375 font-weight: 600;
376 letter-spacing: 1.4px;
377 color: #fff;
378}
379
380.card-sub {
381 margin-top: 10px;
382 font-size: 12.5px;
383 line-height: 1.9;
384 color: rgba(226, 236, 250, 0.66);
385 max-width: 560px;
386}
387
388/* ---------- 解锁寄语 ---------- */
389
390.unlock-list {
391 display: flex;
392 flex-direction: column;
393 gap: 8px;
394}
395
396.unlock-item {
397 display: flex;
398 align-items: center;
399 gap: 9px;
400 padding: 11px 15px;
401 border-radius: var(--r-md);
402 font-size: 12.5px;
403 letter-spacing: 0.6px;
404 color: rgba(255, 255, 255, 0.9);
405 background: linear-gradient(100deg, rgba(var(--theme-color-rgb), 0.24), rgba(255, 255, 255, 0.04));
406 border: 1px solid rgba(var(--theme-color-rgb), 0.3);
407 animation: kFadeUp var(--t-slow) var(--ease-out) both;
408 animation-delay: var(--delay, 0ms);
409}
410
411.unlock-enter-active {
412 transition: opacity var(--t-slow) var(--ease-out), transform var(--t-slow) var(--ease-overshoot);
413}
414
415.unlock-enter-from {
416 opacity: 0;
417 transform: translateY(12px);
418}
419
420/* ---------- 技术栈 ---------- */
421
422.stack {
423 display: grid;
424 grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
425 gap: 10px;
426}
427
428.stack-item {
429 display: flex;
430 flex-direction: column;
431 gap: 6px;
432 padding: 14px 16px;
433 border-radius: var(--r-lg);
434 background: rgba(255, 255, 255, 0.05);
435 border: 1px solid rgba(255, 255, 255, 0.09);
436}
437
438.stack-icon {
439 display: grid;
440 place-items: center;
441 width: 28px;
442 height: 28px;
443 border-radius: var(--r-sm);
444 color: var(--theme-color);
445 background: rgba(var(--theme-color-rgb), 0.16);
446}
447
448.stack-label {
449 font-size: 11px;
450 letter-spacing: 0.6px;
451 color: rgba(226, 236, 250, 0.5);
452}
453
454.stack-value {
455 font-size: 12.5px;
456 color: rgba(255, 255, 255, 0.9);
457}
458
459/* ---------- 声明 ---------- */
460
461.notice {
462 padding: 16px 18px;
463 border-radius: var(--r-lg);
464 background: rgba(255, 255, 255, 0.04);
465 border: 1px solid rgba(255, 255, 255, 0.08);
466}
467
468.notice-title {
469 display: flex;
470 align-items: center;
471 gap: 7px;
472 font-size: 12.5px;
473 font-weight: 600;
474 color: rgba(255, 255, 255, 0.86);
475 margin-bottom: 7px;
476}
477
478.notice-text {
479 font-size: 11.5px;
480 line-height: 1.9;
481 color: rgba(226, 236, 250, 0.58);
482}
483
484/* ---------- 页脚 ---------- */
485
486.foot {
487 display: flex;
488 align-items: center;
489 justify-content: center;
490 flex-wrap: wrap;
491 gap: 8px;
492 padding: 10px 0 4px;
493 font-size: 11.5px;
494 color: rgba(226, 236, 250, 0.5);
495}
496
497.ver-btn {
498 display: inline-flex;
499 align-items: center;
500 gap: 6px;
501 height: 24px;
502 padding: 0 10px;
503 border-radius: var(--r-full);
504 color: rgba(226, 236, 250, 0.72);
505 font-size: 11.5px;
506 background: rgba(255, 255, 255, 0.07);
507 transition: background-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
508}
509
510.ver-btn:hover {
511 background: rgba(var(--theme-color-rgb), 0.24);
512 color: #fff;
513}
514
515.ver-btn em {
516 font-style: normal;
517 color: var(--theme-color);
518 font-weight: 600;
519}
520
521.link-btn {
522 display: inline-flex;
523 align-items: center;
524 gap: 4px;
525 color: rgba(226, 236, 250, 0.7);
526 font-size: 11.5px;
527 transition: color var(--t-fast) var(--ease-out);
528}
529
530.link-btn:hover {
531 color: var(--theme-color);
532}
533
534.sep {
535 opacity: 0.4;
536}
537
538/* ---------- 游戏覆盖层 ---------- */
539
540.game-mask {
541 position: fixed;
542 inset: 0;
543 z-index: var(--z-overlay);
544 display: grid;
545 place-items: center;
546 padding: 32px;
547 background: rgba(8, 12, 20, 0.66);
548 backdrop-filter: blur(6px);
549 -webkit-backdrop-filter: blur(6px);
550 animation: kMaskIn var(--t-base) ease both;
551}
552
553.game-panel {
554 width: min(760px, 100%);
555 animation: kPopIn var(--t-slow) var(--ease-overshoot) both;
556}
557
558.game-enter-active,
559.game-leave-active {
560 transition: opacity var(--t-base) var(--ease-out);
561}
562
563.game-enter-from,
564.game-leave-to {
565 opacity: 0;
566}
567</style>