mirror of
https://github.com/lqtmcstudio/QZMusic_PC.git
synced 2026-06-20 23:35:06 +08:00
feat: QZ Music for Windows(个人学习项目);Vue主界面+设置界面;Router路由;Pinia全局状态管理;封面取色;IPC通信示例
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
18
README.md
Normal file
18
README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
|
||||
## Type Support For `.vue` Imports in TS
|
||||
|
||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
|
||||
|
||||
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
|
||||
|
||||
1. Disable the built-in TypeScript Extension
|
||||
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
||||
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
||||
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
|
||||
67
dist-electron/main.js
Normal file
67
dist-electron/main.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import { ipcMain, BrowserWindow, app, Menu } from "electron";
|
||||
import { createRequire } from "node:module";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import path from "node:path";
|
||||
createRequire(import.meta.url);
|
||||
const __dirname$1 = path.dirname(fileURLToPath(import.meta.url));
|
||||
process.env.APP_ROOT = path.join(__dirname$1, "..");
|
||||
const VITE_DEV_SERVER_URL = process.env["VITE_DEV_SERVER_URL"];
|
||||
const MAIN_DIST = path.join(process.env.APP_ROOT, "dist-electron");
|
||||
const RENDERER_DIST = path.join(process.env.APP_ROOT, "dist");
|
||||
process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT, "public") : RENDERER_DIST;
|
||||
let win;
|
||||
function createWindow() {
|
||||
win = new BrowserWindow({
|
||||
// 🔧 禁用原生标题栏(无边框)
|
||||
frame: false,
|
||||
icon: path.join(process.env.VITE_PUBLIC, "electron-vite.svg"),
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname$1, "preload.mjs")
|
||||
}
|
||||
});
|
||||
win.webContents.on("did-finish-load", () => {
|
||||
win == null ? void 0 : win.webContents.send("main-process-message", (/* @__PURE__ */ new Date()).toLocaleString());
|
||||
});
|
||||
if (VITE_DEV_SERVER_URL) {
|
||||
win.loadURL(VITE_DEV_SERVER_URL);
|
||||
} else {
|
||||
win.loadFile(path.join(RENDERER_DIST, "index.html"));
|
||||
}
|
||||
}
|
||||
ipcMain.on("window-minimize", (event) => {
|
||||
const win2 = BrowserWindow.fromWebContents(event.sender);
|
||||
win2 == null ? void 0 : win2.minimize();
|
||||
});
|
||||
ipcMain.on("window-maximize", () => {
|
||||
if (win == null ? void 0 : win.isMaximized()) {
|
||||
win.unmaximize();
|
||||
} else {
|
||||
win == null ? void 0 : win.maximize();
|
||||
}
|
||||
});
|
||||
ipcMain.on("window-close", () => {
|
||||
win == null ? void 0 : win.close();
|
||||
});
|
||||
ipcMain.handle("window-is-maximized", () => {
|
||||
return (win == null ? void 0 : win.isMaximized()) || false;
|
||||
});
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
win = null;
|
||||
}
|
||||
});
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
app.whenReady().then(() => {
|
||||
Menu.setApplicationMenu(null);
|
||||
createWindow();
|
||||
});
|
||||
export {
|
||||
MAIN_DIST,
|
||||
RENDERER_DIST,
|
||||
VITE_DEV_SERVER_URL
|
||||
};
|
||||
8
dist-electron/preload.mjs
Normal file
8
dist-electron/preload.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
"use strict";
|
||||
const electron = require("electron");
|
||||
electron.contextBridge.exposeInMainWorld("electronAPI", {
|
||||
minimizeWindow: () => electron.ipcRenderer.send("window-minimize"),
|
||||
maximizeWindow: () => electron.ipcRenderer.send("window-maximize"),
|
||||
closeWindow: () => electron.ipcRenderer.send("window-close"),
|
||||
isMaximized: () => electron.ipcRenderer.invoke("window-is-maximized")
|
||||
});
|
||||
43
electron-builder.json5
Normal file
43
electron-builder.json5
Normal file
@@ -0,0 +1,43 @@
|
||||
// @see - https://www.electron.build/configuration/configuration
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json",
|
||||
"appId": "YourAppID",
|
||||
"asar": true,
|
||||
"productName": "YourAppName",
|
||||
"directories": {
|
||||
"output": "release/${version}"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"dist-electron"
|
||||
],
|
||||
"mac": {
|
||||
"target": [
|
||||
"dmg"
|
||||
],
|
||||
"artifactName": "${productName}-Mac-${version}-Installer.${ext}"
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
{
|
||||
"target": "nsis",
|
||||
"arch": [
|
||||
"x64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"artifactName": "${productName}-Windows-${version}-Setup.${ext}"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"perMachine": false,
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"deleteAppDataOnUninstall": false
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
"AppImage"
|
||||
],
|
||||
"artifactName": "${productName}-Linux-${version}.${ext}"
|
||||
}
|
||||
}
|
||||
27
electron/electron-env.d.ts
vendored
Normal file
27
electron/electron-env.d.ts
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
/// <reference types="vite-plugin-electron/electron-env" />
|
||||
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
/**
|
||||
* The built directory structure
|
||||
*
|
||||
* ```tree
|
||||
* ├─┬─┬ dist
|
||||
* │ │ └── index.html
|
||||
* │ │
|
||||
* │ ├─┬ dist-electron
|
||||
* │ │ ├── main.js
|
||||
* │ │ └── preload.js
|
||||
* │
|
||||
* ```
|
||||
*/
|
||||
APP_ROOT: string
|
||||
/** /dist/ or /public/ */
|
||||
VITE_PUBLIC: string
|
||||
}
|
||||
}
|
||||
|
||||
// Used in Renderer process, expose in `preload.ts`
|
||||
interface Window {
|
||||
ipcRenderer: import('electron').IpcRenderer
|
||||
}
|
||||
92
electron/main.ts
Normal file
92
electron/main.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { app, BrowserWindow, Menu, ipcMain } from 'electron'
|
||||
import { createRequire } from 'node:module'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'node:path'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
// The built directory structure
|
||||
//
|
||||
// ├─┬─┬ dist
|
||||
// │ │ └── index.html
|
||||
// │ │
|
||||
// │ ├─┬ dist-electron
|
||||
// │ │ ├── main.js
|
||||
// │ │ └── preload.mjs
|
||||
// │
|
||||
process.env.APP_ROOT = path.join(__dirname, '..')
|
||||
|
||||
// 🚧 Use ['ENV_NAME'] avoid vite:define plugin - Vite@2.x
|
||||
export const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL']
|
||||
export const MAIN_DIST = path.join(process.env.APP_ROOT, 'dist-electron')
|
||||
export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist')
|
||||
|
||||
process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT, 'public') : RENDERER_DIST
|
||||
|
||||
let win: BrowserWindow | null
|
||||
|
||||
function createWindow() {
|
||||
win = new BrowserWindow({
|
||||
// 🔧 禁用原生标题栏(无边框)
|
||||
frame: false,
|
||||
|
||||
icon: path.join(process.env.VITE_PUBLIC, 'electron-vite.svg'),
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.mjs'),
|
||||
},
|
||||
})
|
||||
|
||||
// Test active push message to Renderer-process.
|
||||
win.webContents.on('did-finish-load', () => {
|
||||
win?.webContents.send('main-process-message', new Date().toLocaleString())
|
||||
})
|
||||
|
||||
if (VITE_DEV_SERVER_URL) {
|
||||
win.loadURL(VITE_DEV_SERVER_URL)
|
||||
} else {
|
||||
win.loadFile(path.join(RENDERER_DIST, 'index.html'))
|
||||
}
|
||||
}
|
||||
|
||||
ipcMain.on('window-minimize', (event) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
win?.minimize()
|
||||
})
|
||||
|
||||
ipcMain.on('window-maximize', () => {
|
||||
if (win?.isMaximized()) {
|
||||
win.unmaximize()
|
||||
} else {
|
||||
win?.maximize()
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on('window-close', () => {
|
||||
win?.close()
|
||||
})
|
||||
|
||||
ipcMain.handle('window-is-maximized', () => {
|
||||
return win?.isMaximized() || false
|
||||
})
|
||||
|
||||
// Quit when all windows are closed, except on macOS.
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
win = null
|
||||
}
|
||||
})
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
app.whenReady().then(() => {
|
||||
Menu.setApplicationMenu(null)
|
||||
|
||||
createWindow()
|
||||
})
|
||||
8
electron/preload.ts
Normal file
8
electron/preload.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { contextBridge, ipcRenderer } from 'electron'
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
minimizeWindow: () => ipcRenderer.send('window-minimize'),
|
||||
maximizeWindow: () => ipcRenderer.send('window-maximize'),
|
||||
closeWindow: () => ipcRenderer.send('window-close'),
|
||||
isMaximized: () => ipcRenderer.invoke('window-is-maximized')
|
||||
})
|
||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + Vue + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
7336
package-lock.json
generated
Normal file
7336
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
package.json
Normal file
41
package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "qzmusic",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build && electron-builder",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@applemusic-like-lyrics/core": "^0.2.0",
|
||||
"@applemusic-like-lyrics/vue": "^0.2.0",
|
||||
"@iconify/vue": "^5.0.0",
|
||||
"@pixi/app": "^7.4.3",
|
||||
"@pixi/core": "^7.4.3",
|
||||
"@pixi/display": "^7.4.3",
|
||||
"@pixi/filter-blur": "^7.4.3",
|
||||
"@pixi/filter-bulge-pinch": "^5.1.1",
|
||||
"@pixi/filter-color-matrix": "^7.4.3",
|
||||
"@pixi/sprite": "^7.4.3",
|
||||
"element-plus": "^2.13.0",
|
||||
"jss": "^10.10.0",
|
||||
"jss-preset-default": "^10.10.0",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"electron": "^30.0.1",
|
||||
"electron-builder": "^24.13.3",
|
||||
"sass-embedded": "^1.97.1",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.1.6",
|
||||
"vite-plugin-electron": "^0.28.6",
|
||||
"vite-plugin-electron-renderer": "^0.14.5",
|
||||
"vue-tsc": "^2.0.26"
|
||||
},
|
||||
"main": "dist-electron/main.js"
|
||||
}
|
||||
34
public/electron-vite.animate.svg
Normal file
34
public/electron-vite.animate.svg
Normal file
@@ -0,0 +1,34 @@
|
||||
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M63.9202 127.84C99.2223 127.84 127.84 99.2223 127.84 63.9202C127.84 28.6181 99.2223 0 63.9202 0C28.6181 0 0 28.6181 0 63.9202C0 99.2223 28.6181 127.84 63.9202 127.84Z" fill="url(#paint0_linear_103_2)"/>
|
||||
<g id="not-lightning" clip-path="url(#clip0_103_2)">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
attributeType="XML"
|
||||
type="rotate"
|
||||
from="0 64 64"
|
||||
to="360 64 64"
|
||||
dur="20s"
|
||||
repeatCount="indefinite"/>
|
||||
<path d="M51.3954 39.5028C52.3733 39.6812 53.3108 39.033 53.4892 38.055C53.6676 37.0771 53.0194 36.1396 52.0414 35.9612L51.3954 39.5028ZM28.9393 60.9358C29.4332 61.7985 30.5329 62.0976 31.3957 61.6037C32.2585 61.1098 32.5575 60.0101 32.0636 59.1473L28.9393 60.9358ZM37.6935 66.7457C37.025 66.01 35.8866 65.9554 35.1508 66.6239C34.415 67.2924 34.3605 68.4308 35.029 69.1666L37.6935 66.7457ZM96.9206 89.515C97.7416 88.9544 97.9526 87.8344 97.3919 87.0135C96.8313 86.1925 95.7113 85.9815 94.8904 86.5422L96.9206 89.515ZM52.0414 35.9612C46.4712 34.9451 41.2848 34.8966 36.9738 35.9376C32.6548 36.9806 29.0841 39.1576 27.0559 42.6762L30.1748 44.4741C31.5693 42.0549 34.1448 40.3243 37.8188 39.4371C41.5009 38.5479 46.1547 38.5468 51.3954 39.5028L52.0414 35.9612ZM27.0559 42.6762C24.043 47.9029 25.2781 54.5399 28.9393 60.9358L32.0636 59.1473C28.6579 53.1977 28.1088 48.0581 30.1748 44.4741L27.0559 42.6762ZM35.029 69.1666C39.6385 74.24 45.7158 79.1355 52.8478 83.2597L54.6499 80.1432C47.8081 76.1868 42.0298 71.5185 37.6935 66.7457L35.029 69.1666ZM52.8478 83.2597C61.344 88.1726 70.0465 91.2445 77.7351 92.3608C85.359 93.4677 92.2744 92.6881 96.9206 89.515L94.8904 86.5422C91.3255 88.9767 85.4902 89.849 78.2524 88.7982C71.0793 87.7567 62.809 84.8612 54.6499 80.1432L52.8478 83.2597ZM105.359 84.9077C105.359 81.4337 102.546 78.6127 99.071 78.6127V82.2127C100.553 82.2127 101.759 83.4166 101.759 84.9077H105.359ZM99.071 78.6127C95.5956 78.6127 92.7831 81.4337 92.7831 84.9077H96.3831C96.3831 83.4166 97.5892 82.2127 99.071 82.2127V78.6127ZM92.7831 84.9077C92.7831 88.3817 95.5956 91.2027 99.071 91.2027V87.6027C97.5892 87.6027 96.3831 86.3988 96.3831 84.9077H92.7831ZM99.071 91.2027C102.546 91.2027 105.359 88.3817 105.359 84.9077H101.759C101.759 86.3988 100.553 87.6027 99.071 87.6027V91.2027Z" fill="#A2ECFB"/>
|
||||
<path d="M91.4873 65.382C90.8456 66.1412 90.9409 67.2769 91.7002 67.9186C92.4594 68.5603 93.5951 68.465 94.2368 67.7058L91.4873 65.382ZM84.507 35.2412C83.513 35.2282 82.6967 36.0236 82.6838 37.0176C82.6708 38.0116 83.4661 38.8279 84.4602 38.8409L84.507 35.2412ZM74.9407 39.8801C75.9127 39.6716 76.5315 38.7145 76.323 37.7425C76.1144 36.7706 75.1573 36.1517 74.1854 36.3603L74.9407 39.8801ZM25.5491 80.9047C25.6932 81.8883 26.6074 82.5688 27.5911 82.4247C28.5747 82.2806 29.2552 81.3664 29.1111 80.3828L25.5491 80.9047ZM94.2368 67.7058C97.8838 63.3907 100.505 58.927 101.752 54.678C103.001 50.4213 102.9 46.2472 100.876 42.7365L97.7574 44.5344C99.1494 46.9491 99.3603 50.0419 98.2974 53.6644C97.2323 57.2945 94.9184 61.3223 91.4873 65.382L94.2368 67.7058ZM100.876 42.7365C97.9119 37.5938 91.7082 35.335 84.507 35.2412L84.4602 38.8409C91.1328 38.9278 95.7262 41.0106 97.7574 44.5344L100.876 42.7365ZM74.1854 36.3603C67.4362 37.8086 60.0878 40.648 52.8826 44.8146L54.6847 47.931C61.5972 43.9338 68.5948 41.2419 74.9407 39.8801L74.1854 36.3603ZM52.8826 44.8146C44.1366 49.872 36.9669 56.0954 32.1491 62.3927C27.3774 68.63 24.7148 75.2115 25.5491 80.9047L29.1111 80.3828C28.4839 76.1026 30.4747 70.5062 35.0084 64.5802C39.496 58.7143 46.2839 52.7889 54.6847 47.931L52.8826 44.8146Z" fill="#A2ECFB"/>
|
||||
<path d="M49.0825 87.2295C48.7478 86.2934 47.7176 85.8059 46.7816 86.1406C45.8455 86.4753 45.358 87.5055 45.6927 88.4416L49.0825 87.2295ZM78.5635 96.4256C79.075 95.5732 78.7988 94.4675 77.9464 93.9559C77.0941 93.4443 75.9884 93.7205 75.4768 94.5729L78.5635 96.4256ZM79.5703 85.1795C79.2738 86.1284 79.8027 87.1379 80.7516 87.4344C81.7004 87.7308 82.71 87.2019 83.0064 86.2531L79.5703 85.1795ZM69.156 22.5301C68.2477 22.1261 67.1838 22.535 66.7799 23.4433C66.3759 24.3517 66.7848 25.4155 67.6931 25.8194L69.156 22.5301ZM45.6927 88.4416C47.5994 93.7741 50.1496 98.2905 53.2032 101.505C56.2623 104.724 59.9279 106.731 63.9835 106.731V103.131C61.1984 103.131 58.4165 101.765 55.8131 99.0249C53.2042 96.279 50.8768 92.2477 49.0825 87.2295L45.6927 88.4416ZM63.9835 106.731C69.8694 106.731 74.8921 102.542 78.5635 96.4256L75.4768 94.5729C72.0781 100.235 68.0122 103.131 63.9835 103.131V106.731ZM83.0064 86.2531C85.0269 79.7864 86.1832 72.1831 86.1832 64.0673H82.5832C82.5832 71.8536 81.4723 79.0919 79.5703 85.1795L83.0064 86.2531ZM86.1832 64.0673C86.1832 54.1144 84.4439 44.922 81.4961 37.6502C78.5748 30.4436 74.3436 24.8371 69.156 22.5301L67.6931 25.8194C71.6364 27.5731 75.3846 32.1564 78.1598 39.0026C80.9086 45.7836 82.5832 54.507 82.5832 64.0673H86.1832Z" fill="#A2ECFB"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M103.559 84.9077C103.559 82.4252 101.55 80.4127 99.071 80.4127C96.5924 80.4127 94.5831 82.4252 94.5831 84.9077C94.5831 87.3902 96.5924 89.4027 99.071 89.4027C101.55 89.4027 103.559 87.3902 103.559 84.9077Z" stroke="#A2ECFB" stroke-width="3.6" stroke-linecap="round"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.8143 89.4027C31.2929 89.4027 33.3023 87.3902 33.3023 84.9077C33.3023 82.4252 31.2929 80.4127 28.8143 80.4127C26.3357 80.4127 24.3264 82.4252 24.3264 84.9077C24.3264 87.3902 26.3357 89.4027 28.8143 89.4027Z" stroke="#A2ECFB" stroke-width="3.6" stroke-linecap="round"/>
|
||||
<path d="M63.9835 27.6986C66.4621 27.6986 68.4714 25.6861 68.4714 23.2036C68.4714 20.7211 66.4621 18.7086 63.9835 18.7086C61.5049 18.7086 59.4956 20.7211 59.4956 23.2036C59.4956 25.6861 61.5049 27.6986 63.9835 27.6986Z" stroke="#A2ECFB" stroke-width="3.6" stroke-linecap="round"/>
|
||||
</g>
|
||||
<path d="M70.7175 48.0096L56.3133 50.676C56.0766 50.7199 55.9013 50.9094 55.887 51.1369L55.001 65.2742C54.9801 65.6072 55.3038 65.8656 55.6478 65.7907L59.6582 64.9163C60.0334 64.8346 60.3724 65.1468 60.2953 65.5033L59.1038 71.0151C59.0237 71.386 59.3923 71.7032 59.7758 71.5932L62.2528 70.8822C62.6368 70.7721 63.0057 71.0902 62.9245 71.4615L61.031 80.1193C60.9126 80.6608 61.6751 80.9561 61.9931 80.4918L62.2055 80.1817L73.9428 58.053C74.1393 57.6825 73.8004 57.26 73.3696 57.3385L69.2417 58.0912C68.8538 58.1618 68.5237 57.8206 68.6332 57.462L71.3274 48.6385C71.437 48.2794 71.1058 47.9378 70.7175 48.0096Z" fill="url(#paint1_linear_103_2)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_103_2" x1="1.43824" y1="7.91009" x2="56.3296" y2="82.4569" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#41D1FF"/>
|
||||
<stop offset="1" stop-color="#BD34FE"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_103_2" x1="60.3173" y1="48.7336" x2="64.237" y2="77.1962" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFEA83"/>
|
||||
<stop offset="0.0833333" stop-color="#FFDD35"/>
|
||||
<stop offset="1" stop-color="#FFA800"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_103_2">
|
||||
<rect width="128" height="128" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.9 KiB |
26
public/electron-vite.svg
Normal file
26
public/electron-vite.svg
Normal file
@@ -0,0 +1,26 @@
|
||||
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_103_2)">
|
||||
<path d="M63.9202 127.84C99.2223 127.84 127.84 99.2223 127.84 63.9202C127.84 28.6181 99.2223 0 63.9202 0C28.6181 0 0 28.6181 0 63.9202C0 99.2223 28.6181 127.84 63.9202 127.84Z" fill="url(#paint0_linear_103_2)"/>
|
||||
<path d="M51.3954 39.5028C52.3733 39.6812 53.3108 39.033 53.4892 38.055C53.6676 37.0771 53.0194 36.1396 52.0414 35.9612L51.3954 39.5028ZM28.9393 60.9358C29.4332 61.7985 30.5329 62.0976 31.3957 61.6037C32.2585 61.1098 32.5575 60.0101 32.0636 59.1473L28.9393 60.9358ZM37.6935 66.7457C37.025 66.01 35.8866 65.9554 35.1508 66.6239C34.415 67.2924 34.3605 68.4308 35.029 69.1666L37.6935 66.7457ZM96.9206 89.515C97.7416 88.9544 97.9526 87.8344 97.3919 87.0135C96.8313 86.1925 95.7113 85.9815 94.8904 86.5422L96.9206 89.515ZM52.0414 35.9612C46.4712 34.9451 41.2848 34.8966 36.9738 35.9376C32.6548 36.9806 29.0841 39.1576 27.0559 42.6762L30.1748 44.4741C31.5693 42.0549 34.1448 40.3243 37.8188 39.4371C41.5009 38.5479 46.1547 38.5468 51.3954 39.5028L52.0414 35.9612ZM27.0559 42.6762C24.043 47.9029 25.2781 54.5399 28.9393 60.9358L32.0636 59.1473C28.6579 53.1977 28.1088 48.0581 30.1748 44.4741L27.0559 42.6762ZM35.029 69.1666C39.6385 74.24 45.7158 79.1355 52.8478 83.2597L54.6499 80.1432C47.8081 76.1868 42.0298 71.5185 37.6935 66.7457L35.029 69.1666ZM52.8478 83.2597C61.344 88.1726 70.0465 91.2445 77.7351 92.3608C85.359 93.4677 92.2744 92.6881 96.9206 89.515L94.8904 86.5422C91.3255 88.9767 85.4902 89.849 78.2524 88.7982C71.0793 87.7567 62.809 84.8612 54.6499 80.1432L52.8478 83.2597ZM105.359 84.9077C105.359 81.4337 102.546 78.6127 99.071 78.6127V82.2127C100.553 82.2127 101.759 83.4166 101.759 84.9077H105.359ZM99.071 78.6127C95.5956 78.6127 92.7831 81.4337 92.7831 84.9077H96.3831C96.3831 83.4166 97.5892 82.2127 99.071 82.2127V78.6127ZM92.7831 84.9077C92.7831 88.3817 95.5956 91.2027 99.071 91.2027V87.6027C97.5892 87.6027 96.3831 86.3988 96.3831 84.9077H92.7831ZM99.071 91.2027C102.546 91.2027 105.359 88.3817 105.359 84.9077H101.759C101.759 86.3988 100.553 87.6027 99.071 87.6027V91.2027Z" fill="#A2ECFB"/>
|
||||
<path d="M91.4873 65.382C90.8456 66.1412 90.9409 67.2769 91.7002 67.9186C92.4594 68.5603 93.5951 68.465 94.2368 67.7058L91.4873 65.382ZM84.507 35.2412C83.513 35.2282 82.6967 36.0236 82.6838 37.0176C82.6708 38.0116 83.4661 38.8279 84.4602 38.8409L84.507 35.2412ZM74.9407 39.8801C75.9127 39.6716 76.5315 38.7145 76.323 37.7425C76.1144 36.7706 75.1573 36.1517 74.1854 36.3603L74.9407 39.8801ZM25.5491 80.9047C25.6932 81.8883 26.6074 82.5688 27.5911 82.4247C28.5747 82.2806 29.2552 81.3664 29.1111 80.3828L25.5491 80.9047ZM94.2368 67.7058C97.8838 63.3907 100.505 58.927 101.752 54.678C103.001 50.4213 102.9 46.2472 100.876 42.7365L97.7574 44.5344C99.1494 46.9491 99.3603 50.0419 98.2974 53.6644C97.2323 57.2945 94.9184 61.3223 91.4873 65.382L94.2368 67.7058ZM100.876 42.7365C97.9119 37.5938 91.7082 35.335 84.507 35.2412L84.4602 38.8409C91.1328 38.9278 95.7262 41.0106 97.7574 44.5344L100.876 42.7365ZM74.1854 36.3603C67.4362 37.8086 60.0878 40.648 52.8826 44.8146L54.6847 47.931C61.5972 43.9338 68.5948 41.2419 74.9407 39.8801L74.1854 36.3603ZM52.8826 44.8146C44.1366 49.872 36.9669 56.0954 32.1491 62.3927C27.3774 68.63 24.7148 75.2115 25.5491 80.9047L29.1111 80.3828C28.4839 76.1026 30.4747 70.5062 35.0084 64.5802C39.496 58.7143 46.2839 52.7889 54.6847 47.931L52.8826 44.8146Z" fill="#A2ECFB"/>
|
||||
<path d="M49.0825 87.2295C48.7478 86.2934 47.7176 85.8059 46.7816 86.1406C45.8455 86.4753 45.358 87.5055 45.6927 88.4416L49.0825 87.2295ZM78.5635 96.4256C79.075 95.5732 78.7988 94.4675 77.9464 93.9559C77.0941 93.4443 75.9884 93.7205 75.4768 94.5729L78.5635 96.4256ZM79.5703 85.1795C79.2738 86.1284 79.8027 87.1379 80.7516 87.4344C81.7004 87.7308 82.71 87.2019 83.0064 86.2531L79.5703 85.1795ZM69.156 22.5301C68.2477 22.1261 67.1838 22.535 66.7799 23.4433C66.3759 24.3517 66.7848 25.4155 67.6931 25.8194L69.156 22.5301ZM45.6927 88.4416C47.5994 93.7741 50.1496 98.2905 53.2032 101.505C56.2623 104.724 59.9279 106.731 63.9835 106.731V103.131C61.1984 103.131 58.4165 101.765 55.8131 99.0249C53.2042 96.279 50.8768 92.2477 49.0825 87.2295L45.6927 88.4416ZM63.9835 106.731C69.8694 106.731 74.8921 102.542 78.5635 96.4256L75.4768 94.5729C72.0781 100.235 68.0122 103.131 63.9835 103.131V106.731ZM83.0064 86.2531C85.0269 79.7864 86.1832 72.1831 86.1832 64.0673H82.5832C82.5832 71.8536 81.4723 79.0919 79.5703 85.1795L83.0064 86.2531ZM86.1832 64.0673C86.1832 54.1144 84.4439 44.922 81.4961 37.6502C78.5748 30.4436 74.3436 24.8371 69.156 22.5301L67.6931 25.8194C71.6364 27.5731 75.3846 32.1564 78.1598 39.0026C80.9086 45.7836 82.5832 54.507 82.5832 64.0673H86.1832Z" fill="#A2ECFB"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M103.559 84.9077C103.559 82.4252 101.55 80.4127 99.071 80.4127C96.5924 80.4127 94.5831 82.4252 94.5831 84.9077C94.5831 87.3902 96.5924 89.4027 99.071 89.4027C101.55 89.4027 103.559 87.3902 103.559 84.9077Z" stroke="#A2ECFB" stroke-width="3.6" stroke-linecap="round"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.8143 89.4027C31.2929 89.4027 33.3023 87.3902 33.3023 84.9077C33.3023 82.4252 31.2929 80.4127 28.8143 80.4127C26.3357 80.4127 24.3264 82.4252 24.3264 84.9077C24.3264 87.3902 26.3357 89.4027 28.8143 89.4027Z" stroke="#A2ECFB" stroke-width="3.6" stroke-linecap="round"/>
|
||||
<path d="M63.9835 27.6986C66.4621 27.6986 68.4714 25.6861 68.4714 23.2036C68.4714 20.7211 66.4621 18.7086 63.9835 18.7086C61.5049 18.7086 59.4956 20.7211 59.4956 23.2036C59.4956 25.6861 61.5049 27.6986 63.9835 27.6986Z" stroke="#A2ECFB" stroke-width="3.6" stroke-linecap="round"/>
|
||||
<path d="M70.7175 48.0096L56.3133 50.676C56.0766 50.7199 55.9013 50.9094 55.887 51.1369L55.001 65.2742C54.9801 65.6072 55.3038 65.8656 55.6478 65.7907L59.6582 64.9163C60.0334 64.8346 60.3724 65.1468 60.2953 65.5033L59.1038 71.0151C59.0237 71.386 59.3923 71.7032 59.7758 71.5932L62.2528 70.8822C62.6368 70.7721 63.0057 71.0902 62.9245 71.4615L61.031 80.1193C60.9126 80.6608 61.6751 80.9561 61.9931 80.4918L62.2055 80.1817L73.9428 58.053C74.1393 57.6825 73.8004 57.26 73.3696 57.3385L69.2417 58.0912C68.8538 58.1618 68.5237 57.8206 68.6332 57.462L71.3274 48.6385C71.437 48.2794 71.1058 47.9378 70.7175 48.0096Z" fill="url(#paint1_linear_103_2)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_103_2" x1="1.43824" y1="7.91009" x2="56.3296" y2="82.4569" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#41D1FF"/>
|
||||
<stop offset="1" stop-color="#BD34FE"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_103_2" x1="60.3173" y1="48.7336" x2="64.237" y2="77.1962" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFEA83"/>
|
||||
<stop offset="0.0833333" stop-color="#FFDD35"/>
|
||||
<stop offset="1" stop-color="#FFA800"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_103_2">
|
||||
<rect width="128" height="128" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.7 KiB |
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
185
src/App.vue
Normal file
185
src/App.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<div class="app-container" :class="{ 'dark-mode': store.darkMode }">
|
||||
<SettingsOverlay />
|
||||
<SideBar />
|
||||
|
||||
<main class="main-content">
|
||||
<header class="header-bar">
|
||||
<div class="header-left"></div>
|
||||
<WindowControls />
|
||||
</header>
|
||||
|
||||
<div class="content-scrollable">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="footer-player">
|
||||
<PlayerBar />
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { usePlayerStore } from './stores/playerStore';
|
||||
import SideBar from './components/layout/SideBar.vue';
|
||||
import WindowControls from './components/layout/WindowControls.vue';
|
||||
import PlayerBar from './components/layout/PlayerBar.vue';
|
||||
import SettingsOverlay from "./components/layout/SettingsOverlay.vue";
|
||||
|
||||
const store = usePlayerStore();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* 主题变量系统 */
|
||||
:root {
|
||||
/* 亮色主题变量 */
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f6f8fa;
|
||||
--bg-tertiary: #f9fafb;
|
||||
--text-primary: #333333;
|
||||
--text-secondary: #666666;
|
||||
--text-tertiary: #888888;
|
||||
--border-color: #e0e0e0;
|
||||
--border-light: #f0f0f0;
|
||||
--sidebar-bg: #f6f8fa;
|
||||
--sidebar-border: #e0e0e0;
|
||||
--header-bg: #ffffff;
|
||||
--content-bg: #ffffff;
|
||||
--player-bg: #ffffff;
|
||||
--card-bg: #ffffff;
|
||||
--overlay-bg: rgba(0, 0, 0, 0.4);
|
||||
--settings-modal-bg: white;
|
||||
--settings-sidebar-bg: #f9fafb;
|
||||
--settings-sidebar-border: #eee;
|
||||
--nav-item-hover: #eee;
|
||||
--nav-item-active: #333;
|
||||
--nav-item-active-text: white;
|
||||
--close-btn-color: #999;
|
||||
--close-btn-hover: #333;
|
||||
--dummy-option-border: #f0f0f0;
|
||||
--logo-placeholder-bg: linear-gradient(45deg, #ff6b6b, #4ecdc4);
|
||||
}
|
||||
|
||||
/* 深色主题变量 */
|
||||
.dark-mode {
|
||||
--bg-primary: #1a1a1a;
|
||||
--bg-secondary: #2d2d2d;
|
||||
--bg-tertiary: #333333;
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #b0b0b0;
|
||||
--text-tertiary: #808080;
|
||||
--border-color: #404040;
|
||||
--border-light: #3a3a3a;
|
||||
--sidebar-bg: #2d2d2d;
|
||||
--sidebar-border: #404040;
|
||||
--header-bg: #1a1a1a;
|
||||
--content-bg: #1a1a1a;
|
||||
--player-bg: #1a1a1a;
|
||||
--card-bg: #2d2d2d;
|
||||
--overlay-bg: rgba(0, 0, 0, 0.7);
|
||||
--settings-modal-bg: #2d2d2d;
|
||||
--settings-sidebar-bg: #333333;
|
||||
--settings-sidebar-border: #404040;
|
||||
--nav-item-hover: #404040;
|
||||
--nav-item-active: #6366f1;
|
||||
--nav-item-active-text: white;
|
||||
--close-btn-color: #808080;
|
||||
--close-btn-hover: #ffffff;
|
||||
--dummy-option-border: #3a3a3a;
|
||||
--logo-placeholder-bg: linear-gradient(45deg, #6366f1, #8b5cf6);
|
||||
}
|
||||
|
||||
/* 全局重置 */
|
||||
html, body, #app {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
overflow: hidden; /* 防止浏览器默认滚动 */
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
transition: background-color 0.6s ease, color 0.6s ease;
|
||||
}
|
||||
|
||||
/* 布局结构 */
|
||||
.app-container {
|
||||
display: grid;
|
||||
grid-template-columns: 220px 1fr; /* 侧边栏宽度 | 主内容 */
|
||||
grid-template-rows: 1fr 80px; /* 主体高度 | 播放器高度 */
|
||||
transition: background-color 0.6s ease;
|
||||
}
|
||||
|
||||
/* 布局结构样式 */
|
||||
.app-container {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
|
||||
/* 侧边栏跨越第一行 */
|
||||
.sidebar {
|
||||
grid-column: 1 / 2;
|
||||
grid-row: 1 / 2;
|
||||
}
|
||||
|
||||
/* 主内容区 */
|
||||
.main-content {
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 1 / 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
background: var(--content-bg);
|
||||
|
||||
/* 顶部拖拽区 */
|
||||
.header-bar {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
-webkit-app-region: drag; /* Electron 拖拽 */
|
||||
background: var(--header-bg);
|
||||
}
|
||||
|
||||
/* 路由视图滚动区 */
|
||||
.content-scrollable {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px 40px;
|
||||
background: var(--content-bg);
|
||||
|
||||
/* 隐藏滚动条但保留功能 */
|
||||
&::-webkit-scrollbar { width: 6px; }
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--border-color);
|
||||
border-radius: 3px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 底部播放器跨越两列 */
|
||||
.footer-player {
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 2 / 3;
|
||||
z-index: 200;
|
||||
background: var(--player-bg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 路由动画 */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
276
src/components/layout/PlayerBar.vue
Normal file
276
src/components/layout/PlayerBar.vue
Normal file
@@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<div class="player-bar" :style="gradientStyle">
|
||||
<div class="glass-overlay"></div>
|
||||
|
||||
<!-- 新增:顶部进度条,使用专辑图提取的最深色 -->
|
||||
<div class="top-progress">
|
||||
<div class="progress-fill" :style="{ width: store.progressPercentage + '%', backgroundColor: store.progressColor }"></div>
|
||||
</div>
|
||||
|
||||
<div class="bar-content">
|
||||
<div class="side-container left">
|
||||
<div class="track-info">
|
||||
<div class="album-cover">
|
||||
<img :src="store.currentSong.cover" @load="store.extractColors" alt="Album Pic"/>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<div class="song-title">{{ store.currentSong.title }}</div>
|
||||
<div class="artist-name">{{ store.currentSong.artist }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="center-container">
|
||||
<div class="controls-wrapper">
|
||||
<button class="ctrl-btn sm">
|
||||
<Icon icon="lucide:skip-back" width="22" />
|
||||
</button>
|
||||
<button class="ctrl-btn lg play-btn" @click="store.togglePlay">
|
||||
<Icon :icon="store.isPlaying ? 'lucide:pause' : 'lucide:play'" width="28" fill="currentColor" />
|
||||
</button>
|
||||
<button class="ctrl-btn sm">
|
||||
<Icon icon="lucide:skip-forward" width="22" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="side-container right">
|
||||
<div class="extra-controls">
|
||||
<!-- 美化后的时间显示,放在右侧最左侧 -->
|
||||
<div class="time-display">
|
||||
{{ formatTime(store.currentTime) }} / {{ formatTime(store.currentSong.duration) }}
|
||||
</div>
|
||||
|
||||
<Icon icon="lucide:repeat" width="20" class="icon-btn" />
|
||||
<div class="volume-box">
|
||||
<Icon icon="lucide:volume-2" width="20" />
|
||||
<el-slider v-model="store.volume" size="small" class="custom-slider" />
|
||||
</div>
|
||||
<Icon icon="lucide:list-music" width="20"
|
||||
class="icon-btn"
|
||||
:class="{ active: store.showPlaylist }"
|
||||
@click="store.showPlaylist = !store.showPlaylist" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 保留底部细进度条(鼠标悬停可变粗) -->
|
||||
<div class="bottom-progress">
|
||||
<div class="progress-fill" :style="{ width: store.progressPercentage + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { usePlayerStore } from '../../stores/playerStore';
|
||||
import { Icon } from '@iconify/vue';
|
||||
|
||||
const store = usePlayerStore();
|
||||
|
||||
const gradientStyle = computed(() => {
|
||||
return {
|
||||
background: `linear-gradient(135deg, ${store.themeColors.primary}, ${store.themeColors.secondary})`
|
||||
};
|
||||
});
|
||||
|
||||
function formatTime(val: number) {
|
||||
if (!val || isNaN(val)) return '0:00';
|
||||
const m = Math.floor(val / 60);
|
||||
const s = Math.floor(val % 60);
|
||||
return `${m}:${s < 10 ? '0' + s : s}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.player-bar {
|
||||
height: 80px; // 稍微增高以容纳布局
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: background 0.5s ease; // 颜色切换动画
|
||||
|
||||
.glass-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(255, 255, 255, 0.15); // 稍微提亮
|
||||
backdrop-filter: blur(20px); // 毛玻璃
|
||||
z-index: 1;
|
||||
border-top: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.bar-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 3%; // 使用百分比 padding 适应缩放
|
||||
color: white; // 假设提取的颜色较深,文字用白色,反之需计算反色
|
||||
}
|
||||
|
||||
/* 左右容器,确保中间绝对居中 */
|
||||
.side-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0; // 防止 flex item 溢出
|
||||
|
||||
&.right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
/* 左侧信息 */
|
||||
.track-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.album-cover {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||
flex-shrink: 0;
|
||||
img { width: 100%; height: 100%; object-fit: cover; }
|
||||
}
|
||||
|
||||
.meta {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
.song-title { font-size: 1.1rem; font-weight: 600; text-overflow: ellipsis; overflow: hidden; }
|
||||
.artist-name { font-size: 0.9rem; opacity: 0.8; margin-top: 4px; }
|
||||
}
|
||||
}
|
||||
|
||||
/* 绝对居中容器 */
|
||||
.center-container {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 200px; // 限制宽度
|
||||
|
||||
.controls-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.ctrl-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: rgba(255,255,255,0.9);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
transition: transform 0.2s, color 0.2s;
|
||||
|
||||
&:hover { color: #fff; transform: scale(1.1); }
|
||||
&:active { transform: scale(0.95); }
|
||||
|
||||
&.play-btn {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: rgba(255,255,255,0.2);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
&:hover { background: rgba(255,255,255,0.3); }
|
||||
}
|
||||
}
|
||||
|
||||
/* 右侧图标 */
|
||||
.extra-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
|
||||
.time-display {
|
||||
margin-right: auto;
|
||||
padding-right: 20px; // 与图标保持距离
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.2s;
|
||||
&:hover { opacity: 1; }
|
||||
&.active { color: #ffeb3b; opacity: 1; }
|
||||
}
|
||||
|
||||
.volume-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 底部微型进度条 */
|
||||
.bottom-progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: rgba(255,255,255,0.1);
|
||||
z-index: 3;
|
||||
cursor: pointer;
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: rgba(255,255,255,0.6);
|
||||
transition: width 0.1s linear;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
height: 6px;
|
||||
.progress-fill { background: #fff; }
|
||||
}
|
||||
}
|
||||
/* 顶部进度条 */
|
||||
.top-progress {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
z-index: 4;
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
transition: width 0.2s ease;
|
||||
background-color: white; // fallback
|
||||
}
|
||||
}
|
||||
.time-display {
|
||||
font-family: 'SF Mono', 'Roboto Mono', Menlo, monospace;
|
||||
font-size: 12.5px;
|
||||
font-weight: 500;
|
||||
opacity: 0.9;
|
||||
letter-spacing: 0.5px;
|
||||
min-width: 100px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* 覆盖 Element Slider 样式以适配深色背景 */
|
||||
:deep(.custom-slider) {
|
||||
--el-slider-main-bg-color: rgba(255,255,255,0.8);
|
||||
--el-slider-runway-bg-color: rgba(255,255,255,0.2);
|
||||
.el-slider__bar { background-color: white; }
|
||||
.el-slider__button { border-color: white; width: 12px; height: 12px; }
|
||||
}
|
||||
</style>
|
||||
361
src/components/layout/SettingsOverlay.vue
Normal file
361
src/components/layout/SettingsOverlay.vue
Normal file
@@ -0,0 +1,361 @@
|
||||
<template>
|
||||
<!-- 遮罩层独立动画 -->
|
||||
<transition name="fade-quick">
|
||||
<div v-if="store.showSettings" class="backdrop" @click="store.toggleSettings"></div>
|
||||
</transition>
|
||||
|
||||
<!-- 弹窗动画 -->
|
||||
<transition name="fade-slide">
|
||||
<div v-if="store.showSettings" class="settings-overlay">
|
||||
<div class="settings-modal">
|
||||
<aside class="settings-sidebar">
|
||||
<div class="title">设置</div>
|
||||
<ul class="nav-list">
|
||||
<li v-for="item in menuItems" :key="item.id"
|
||||
:class="{ active: activeTab === item.id }"
|
||||
@click="activeTab = item.id">
|
||||
{{ item.name }}
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<main class="settings-content">
|
||||
<button class="close-btn" @click="store.toggleSettings">
|
||||
<Icon icon="lucide:x" width="24" />
|
||||
</button>
|
||||
|
||||
<transition name="fade" mode="out-in">
|
||||
<!-- 通用设置 -->
|
||||
<div v-if="activeTab === 'general'" class="section-content" key="general">
|
||||
<h2>{{ menuItems.find(i => i.id === activeTab)?.name }}</h2>
|
||||
<div class="dummy-option">
|
||||
<span>深色模式</span>
|
||||
<el-switch v-model="store.darkMode" @change="logSetting('darkMode', store.darkMode)" />
|
||||
</div>
|
||||
<div class="dummy-option">
|
||||
<span>自动播放</span>
|
||||
<el-switch v-model="settings.autoPlay" @change="logSetting('autoPlay', settings.autoPlay)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 播放与音质 -->
|
||||
<div v-else-if="activeTab === 'audio'" class="section-content" key="audio">
|
||||
<h2>{{ menuItems.find(i => i.id === activeTab)?.name }}</h2>
|
||||
<div class="dummy-option">
|
||||
<span>启用高音质 (Hi-Res)</span>
|
||||
<el-switch v-model="settings.hiResAudio" @change="logSetting('hiResAudio', settings.hiResAudio)" />
|
||||
</div>
|
||||
<div class="dummy-option">
|
||||
<span>开启桌面歌词</span>
|
||||
<el-switch v-model="settings.desktopLyrics" @change="logSetting('desktopLyrics', settings.desktopLyrics)" />
|
||||
</div>
|
||||
<div class="dummy-option">
|
||||
<span>音量增强</span>
|
||||
<el-switch v-model="settings.volumeBoost" @change="logSetting('volumeBoost', settings.volumeBoost)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快捷键 -->
|
||||
<div v-else-if="activeTab === 'shortcut'" class="section-content" key="shortcut">
|
||||
<h2>{{ menuItems.find(i => i.id === activeTab)?.name }}</h2>
|
||||
<div class="dummy-option">
|
||||
<span>启用全局快捷键</span>
|
||||
<el-switch v-model="settings.globalShortcuts" @change="logSetting('globalShortcuts', settings.globalShortcuts)" />
|
||||
</div>
|
||||
<div class="dummy-option">
|
||||
<span>媒体键控制</span>
|
||||
<el-switch v-model="settings.mediaKeys" @change="logSetting('mediaKeys', settings.mediaKeys)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下载管理 -->
|
||||
<div v-else-if="activeTab === 'download'" class="section-content" key="download">
|
||||
<h2>{{ menuItems.find(i => i.id === activeTab)?.name }}</h2>
|
||||
<div class="dummy-option">
|
||||
<span>下载完成后通知</span>
|
||||
<el-switch v-model="settings.downloadNotify" @change="logSetting('downloadNotify', settings.downloadNotify)" />
|
||||
</div>
|
||||
<div class="dummy-option">
|
||||
<span>高质量下载</span>
|
||||
<el-switch v-model="settings.hqDownload" @change="logSetting('hqDownload', settings.hqDownload)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 关于页面 -->
|
||||
<div v-else class="about-section" key="about">
|
||||
<div class="logo-animate">M</div>
|
||||
<h3>Muse Player</h3>
|
||||
<p class="version">Version 1.0.0 Beta</p>
|
||||
<div class="desc">
|
||||
Designed for music lovers.<br>
|
||||
Crafted with Vue 3 & Electron.
|
||||
</div>
|
||||
<div class="links">
|
||||
<a href="#">GitHub</a>
|
||||
<a href="#">Website</a>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue';
|
||||
import { usePlayerStore } from '../../stores/playerStore';
|
||||
import { Icon } from '@iconify/vue';
|
||||
|
||||
const store = usePlayerStore();
|
||||
const activeTab = ref('general');
|
||||
|
||||
// 其他设置的本地状态管理
|
||||
const settings = reactive({
|
||||
autoPlay: true,
|
||||
hiResAudio: true,
|
||||
desktopLyrics: false,
|
||||
volumeBoost: false,
|
||||
globalShortcuts: true,
|
||||
mediaKeys: true,
|
||||
downloadNotify: true,
|
||||
hqDownload: true
|
||||
});
|
||||
|
||||
// 切换时打印日志
|
||||
const logSetting = (key: string, value: boolean) => {
|
||||
console.log(`[设置变更] ${key}: ${value ? '开启' : '关闭'}`);
|
||||
// 这里可以添加实际保存设置的逻辑
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{ id: 'general', name: '通用' },
|
||||
{ id: 'audio', name: '播放与音质' },
|
||||
{ id: 'shortcut', name: '快捷键' },
|
||||
{ id: 'download', name: '下载管理' },
|
||||
{ id: 'about', name: '关于' },
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 遮罩层 - 快速淡入淡出 */
|
||||
.fade-quick-enter-active,
|
||||
.fade-quick-leave-active {
|
||||
transition: opacity 0.1s ease;
|
||||
}
|
||||
|
||||
.fade-quick-enter-from,
|
||||
.fade-quick-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 遮罩层样式 */
|
||||
.backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 998;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
/* 弹窗容器 */
|
||||
.settings-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
pointer-events: none; /* 防止点击穿透 */
|
||||
|
||||
.settings-modal {
|
||||
width: 70vw;
|
||||
height: 70vh;
|
||||
max-width: 900px;
|
||||
background: var(--settings-modal-bg);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
pointer-events: auto; /* 恢复弹窗内的点击事件 */
|
||||
}
|
||||
|
||||
.settings-sidebar {
|
||||
width: 200px;
|
||||
background: var(--settings-sidebar-bg);
|
||||
padding: 30px 20px;
|
||||
border-right: 1px solid var(--settings-sidebar-border);
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 30px;
|
||||
padding-left: 10px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.nav-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: var(--nav-item-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--nav-item-active);
|
||||
color: var(--nav-item-active-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
flex: 1;
|
||||
padding: 40px;
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
background: var(--settings-modal-bg);
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--close-btn-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--close-btn-hover);
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 30px;
|
||||
font-size: 20px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.dummy-option {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid var(--dummy-option-border);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 关于页面样式 */
|
||||
.about-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
|
||||
.logo-animate {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: var(--logo-placeholder-bg);
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 40px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.version {
|
||||
color: var(--text-tertiary);
|
||||
margin: 10px 0 30px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.desc {
|
||||
line-height: 1.6;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
|
||||
a {
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: border 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 弹窗进入动画 - 遮罩已经独立 */
|
||||
.fade-slide-enter-active,
|
||||
.fade-slide-leave-active {
|
||||
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.fade-slide-enter-from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(10px);
|
||||
}
|
||||
|
||||
.fade-slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-10px);
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0) rotate(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-10px) rotate(5deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
98
src/components/layout/SideBar.vue
Normal file
98
src/components/layout/SideBar.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<aside class="sidebar">
|
||||
<div class="brand">
|
||||
<div class="logo-placeholder">M</div>
|
||||
<span class="app-name">Muse Player</span>
|
||||
</div>
|
||||
|
||||
<nav class="menu">
|
||||
<router-link to="/" class="menu-item" active-class="active">
|
||||
<Icon icon="lucide:home" width="20" />
|
||||
<span>主页</span>
|
||||
</router-link>
|
||||
<router-link to="/local" class="menu-item" active-class="active">
|
||||
<Icon icon="lucide:hard-drive" width="20" />
|
||||
<span>本地音乐</span>
|
||||
</router-link>
|
||||
<router-link to="/playlist" class="menu-item" active-class="active">
|
||||
<Icon icon="lucide:list-music" width="20" />
|
||||
<span>我的歌单</span>
|
||||
</router-link>
|
||||
</nav>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue';
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.sidebar {
|
||||
width: 220px;
|
||||
height: 100%;
|
||||
background-color: var(--sidebar-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px 12px;
|
||||
border-right: 1px solid var(--sidebar-border);
|
||||
// 针对 Electron 的拖拽区域避免
|
||||
-webkit-app-region: drag;
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 40px;
|
||||
padding-left: 12px;
|
||||
|
||||
.logo-placeholder {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: linear-gradient(135deg, #ff6b6b, #556270);
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
-webkit-app-region: no-drag; // 菜单项可点击,不可拖拽
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 16px;
|
||||
border-radius: 10px;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 14px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--nav-item-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
88
src/components/layout/WindowControls.vue
Normal file
88
src/components/layout/WindowControls.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div class="window-controls">
|
||||
<div class="settings-btn" @click="store.toggleSettings">
|
||||
<Icon icon="lucide:settings" width="22" />
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="traffic-lights">
|
||||
<div class="light minimize" @click="handleMinimize"></div>
|
||||
<div class="light maximize" @click="handleMaximize"></div>
|
||||
<div class="light close" @click="handleClose"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { usePlayerStore } from '../../stores/playerStore'
|
||||
|
||||
const store = usePlayerStore()
|
||||
|
||||
// ✅ 调用主进程暴露的 API
|
||||
const handleClose = () => {
|
||||
window.electronAPI.closeWindow()
|
||||
}
|
||||
|
||||
const handleMinimize = () => {
|
||||
window.electronAPI.minimizeWindow()
|
||||
}
|
||||
|
||||
const handleMaximize = async () => {
|
||||
// 可选:切换图标(比如最大化/还原)
|
||||
await window.electronAPI.maximizeWindow()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.window-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 24px;
|
||||
height: 100%;
|
||||
gap: 24px; // 设置和红绿灯之间的距离
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
.settings-btn {
|
||||
cursor: pointer;
|
||||
color: var(--text-tertiary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-primary);
|
||||
transform: rotate(45deg); // 简单的交互动画
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
.traffic-lights {
|
||||
display: flex;
|
||||
gap: 10px; // 按钮间距拉大
|
||||
|
||||
.light {
|
||||
width: 15px; // 放大按钮 (原12px)
|
||||
height: 15px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: transform 0.1s, opacity 0.2s;
|
||||
|
||||
&:hover { opacity: 0.8; }
|
||||
&:active { transform: scale(0.9); }
|
||||
|
||||
/* 调整了顺序: 最小化-最大化-关闭,符合一般习惯,也可按需调整 */
|
||||
&.close { background-color: #ff5f56; }
|
||||
&.minimize { background-color: #ffbd2e; }
|
||||
&.maximize { background-color: #27c93f; }
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
25
src/main.ts
Normal file
25
src/main.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
// src/main.ts
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
|
||||
import App from './App.vue'
|
||||
import HomeView from './views/HomeView.vue'
|
||||
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
|
||||
const pinia = createPinia()
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: HomeView },
|
||||
// 可扩展其他路由
|
||||
]
|
||||
})
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(ElementPlus)
|
||||
app.mount('#app')
|
||||
168
src/stores/playerStore.ts
Normal file
168
src/stores/playerStore.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
|
||||
export const usePlayerStore = defineStore('player', () => {
|
||||
const currentSong = ref({
|
||||
id: 1,
|
||||
title: "Example Track",
|
||||
artist: "AI Artist",
|
||||
cover: "http://p2.music.126.net/W3VMsSEjTdvhz7h3a0oxTg==/17782401556325576.jpg?param=130y130",
|
||||
url: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3",
|
||||
duration: 0
|
||||
});
|
||||
|
||||
const isPlaying = ref(false);
|
||||
const currentTime = ref(0);
|
||||
const volume = ref(80);
|
||||
const showPlaylist = ref(false);
|
||||
const showSettings = ref(false);
|
||||
const darkMode = ref(false);
|
||||
const themeColors = ref({ primary: '#6366f1', secondary: '#a855f7' });
|
||||
|
||||
// 新增:用于顶部进度条的最深色
|
||||
const progressColor = ref('#ffffff');
|
||||
|
||||
const audio = new Audio(currentSong.value.url);
|
||||
audio.volume = volume.value / 100;
|
||||
|
||||
// 颜色提取函数(轻量版,不依赖外部库)
|
||||
function extractColors() {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'Anonymous';
|
||||
img.src = currentSong.value.cover + '?t=' + Date.now(); // 避免缓存
|
||||
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
// 缩小图片以提高性能
|
||||
const scale = 0.1;
|
||||
canvas.width = Math.floor(img.width * scale);
|
||||
canvas.height = Math.floor(img.height * scale);
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
|
||||
// 收集所有像素颜色
|
||||
const colors: { r: number; g: number; b: number; brightness: number }[] = [];
|
||||
|
||||
for (let i = 0; i < imageData.length; i += 4) {
|
||||
const r = imageData[i];
|
||||
const g = imageData[i + 1];
|
||||
const b = imageData[i + 2];
|
||||
const brightness = (r + g + b) / 3;
|
||||
|
||||
colors.push({ r, g, b, brightness });
|
||||
}
|
||||
|
||||
// 计算颜色频率(简单实现)
|
||||
const colorFrequency: Record<string, number> = {};
|
||||
colors.forEach(color => {
|
||||
// 将颜色量化为 16 位色,减少颜色数量
|
||||
const key = `${Math.floor(color.r / 16)}${Math.floor(color.g / 16)}${Math.floor(color.b / 16)}`;
|
||||
colorFrequency[key] = (colorFrequency[key] || 0) + 1;
|
||||
});
|
||||
|
||||
// 转换回 RGB 并按频率排序
|
||||
const sortedColors = Object.entries(colorFrequency)
|
||||
.map(([key, count]) => {
|
||||
const r = parseInt(key[0], 16) * 16;
|
||||
const g = parseInt(key[1], 16) * 16;
|
||||
const b = parseInt(key[2], 16) * 16;
|
||||
return { r, g, b, count };
|
||||
})
|
||||
.sort((a, b) => b.count - a.count);
|
||||
|
||||
// 提取主色调和辅助色调
|
||||
if (sortedColors.length > 0) {
|
||||
// 主色调:频率最高的颜色
|
||||
const primary = sortedColors[0];
|
||||
const primaryColor = `rgb(${primary.r}, ${primary.g}, ${primary.b})`;
|
||||
|
||||
// 辅助色调:选择与主色调亮度差异较大的颜色
|
||||
let secondary = sortedColors[1] || sortedColors[0];
|
||||
let maxBrightnessDiff = Math.abs(primary.brightness - secondary.brightness);
|
||||
|
||||
// 在频率较高的颜色中寻找最合适的辅助色
|
||||
for (let i = 1; i < Math.min(10, sortedColors.length); i++) {
|
||||
const currentBrightness = (sortedColors[i].r + sortedColors[i].g + sortedColors[i].b) / 3;
|
||||
const brightnessDiff = Math.abs(primary.brightness - currentBrightness);
|
||||
|
||||
if (brightnessDiff > maxBrightnessDiff) {
|
||||
maxBrightnessDiff = brightnessDiff;
|
||||
secondary = sortedColors[i];
|
||||
}
|
||||
}
|
||||
|
||||
const secondaryColor = `rgb(${secondary.r}, ${secondary.g}, ${secondary.b})`;
|
||||
|
||||
// 更新主题颜色
|
||||
themeColors.value = {
|
||||
primary: primaryColor,
|
||||
secondary: secondaryColor
|
||||
};
|
||||
|
||||
// 提取进度条颜色(使用主色调的深色版本)
|
||||
const darkPrimary = {
|
||||
r: Math.floor(primary.r * 0.7),
|
||||
g: Math.floor(primary.g * 0.7),
|
||||
b: Math.floor(primary.b * 0.7)
|
||||
};
|
||||
progressColor.value = `rgb(${darkPrimary.r}, ${darkPrimary.g}, ${darkPrimary.b})`;
|
||||
} else {
|
||||
// 默认颜色
|
||||
themeColors.value = { primary: '#6366f1', secondary: '#a855f7' };
|
||||
progressColor.value = '#ffffff';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 监听歌曲切换时重新提取颜色
|
||||
watch(() => currentSong.value.cover, () => {
|
||||
extractColors();
|
||||
console.log("!!")
|
||||
}, { immediate: true });
|
||||
|
||||
audio.addEventListener('loadedmetadata', () => {
|
||||
currentSong.value.duration = audio.duration;
|
||||
});
|
||||
|
||||
audio.addEventListener('timeupdate', () => {
|
||||
currentTime.value = audio.currentTime;
|
||||
});
|
||||
|
||||
audio.addEventListener('ended', () => {
|
||||
isPlaying.value = false;
|
||||
currentTime.value = 0;
|
||||
});
|
||||
|
||||
function togglePlay() {
|
||||
if (isPlaying.value) {
|
||||
audio.pause();
|
||||
} else {
|
||||
audio.play().catch(e => console.warn("播放失败,需用户交互:", e));
|
||||
}
|
||||
isPlaying.value = !isPlaying.value;
|
||||
}
|
||||
|
||||
function seek(time: number) {
|
||||
audio.currentTime = time;
|
||||
}
|
||||
function toggleSettings() { console.log("!!");showSettings.value = !showSettings.value; }
|
||||
|
||||
watch(volume, (newVol) => {
|
||||
audio.volume = newVol / 100;
|
||||
});
|
||||
|
||||
const progressPercentage = computed(() =>
|
||||
currentSong.value.duration ? (currentTime.value / currentSong.value.duration) * 100 : 0
|
||||
);
|
||||
|
||||
return {
|
||||
currentSong, isPlaying, currentTime, volume, showPlaylist,
|
||||
themeColors, progressPercentage, progressColor,
|
||||
togglePlay, seek, extractColors,
|
||||
showSettings, toggleSettings,
|
||||
darkMode
|
||||
};
|
||||
});
|
||||
378
src/views/HomeView.vue
Normal file
378
src/views/HomeView.vue
Normal file
@@ -0,0 +1,378 @@
|
||||
<template>
|
||||
<div class="home-view">
|
||||
<section class="section-container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">推荐歌单</h2>
|
||||
<div class="more">查看全部 <Icon icon="lucide:chevron-right" /></div>
|
||||
</div>
|
||||
|
||||
<div class="playlist-grid">
|
||||
<div
|
||||
v-for="item in playlistData"
|
||||
:key="item.id"
|
||||
class="playlist-card"
|
||||
@click="handlePlayListClick(item)"
|
||||
>
|
||||
<div class="cover-wrapper">
|
||||
<img :src="item.img" :alt="item.name" loading="lazy" />
|
||||
|
||||
<div class="play-count">
|
||||
<Icon icon="lucide:play" width="12" />
|
||||
<span>{{ item.play_count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="playlist-info">
|
||||
<div class="playlist-name">{{ item.name }}</div>
|
||||
<div class="playlist-author">{{ item.author }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { Icon } from '@iconify/vue';
|
||||
|
||||
// 注入你提供的 JSON 数据
|
||||
const playlistData = ref([
|
||||
|
||||
{
|
||||
|
||||
"play_count": "4.7万",
|
||||
|
||||
"id": "17387580241",
|
||||
|
||||
"author": "张大佛爷张大佛爷",
|
||||
|
||||
"name": "巴西Funk:来首精神氮泵助力燃脂",
|
||||
|
||||
"time": "2025-11-02",
|
||||
|
||||
"img": "http://p2.music.126.net/YkpQqGGlHR4DD8BjGTQe8Q==/109951172228935288.jpg",
|
||||
|
||||
"total": 30,
|
||||
|
||||
"desc": "巴西放克(Funk brasileiro),通常在巴西被称为\"Funk\",是一种受嘻哈音乐(Hip Hop)影响的音乐风 格,起源于里约热内卢的贫民区(也称为贫民窟)。里约放克(Funk carioca)最初源自电音放克(Electro)和迈阿密贝 斯(Miami Bass),之后逐渐发展出独特的风格,并成为巴西低收入青少年中最受欢迎的音乐类型之一。\n\n听说巴西Funk和健身很搭噢!",
|
||||
|
||||
"source": "wy"
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"play_count": "3.6万",
|
||||
|
||||
"id": "14435495551",
|
||||
|
||||
"author": "凌晨一点的莱茵猫",
|
||||
|
||||
"name": "ACG治愈 | 回忆复刻 重温动漫里的温情时刻",
|
||||
|
||||
"time": "2025-10-18",
|
||||
|
||||
"img": "http://p2.music.126.net/uPWibcRbc7u3vDD44gXA0A==/109951172205320822.jpg",
|
||||
|
||||
"total": 99,
|
||||
|
||||
"desc": "不是所有治愈都需要大段对白\n一段旋律就够了\n\n可以是主角低谷时的背景音\n也可以是圆满结局的收尾曲\n\n每一段都裹着当时的情绪\n现在听依旧能暖到心里最软的地方",
|
||||
|
||||
"source": "wy"
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"play_count": "3万",
|
||||
|
||||
"id": "17438630520",
|
||||
|
||||
"author": "露露在发呆",
|
||||
|
||||
"name": "跑步 •鬼灭之刃超燃和风主题歌",
|
||||
|
||||
"time": "2025-11-15",
|
||||
|
||||
"img": "http://p2.music.126.net/DuKyLyYhKd2YhWsRYkd9DQ==/109951172287476927.jpg",
|
||||
|
||||
"total": 26,
|
||||
|
||||
"desc": "鬼灭之刃即将迎来无限城终极大决战,精选历代TV番、剧场版主题曲与超人气配乐插曲,与动画同样出彩的是其独特美妙的日式和风电子流行曲风,给你今日份的运动计划注入来自鬼灭之刃的二次元能量吧,适配有氧、无氧运动 全适配,高能KPOP祝你今天也能量满满燃爆卡路里!",
|
||||
|
||||
"source": "wy"
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"play_count": "1.7万",
|
||||
|
||||
"id": "17430413151",
|
||||
|
||||
"author": "心语馆",
|
||||
|
||||
"name": "【高质量轻音】享受宁静 沉浸自然",
|
||||
|
||||
"time": "2025-11-13",
|
||||
|
||||
"img": "http://p2.music.126.net/El8lldCevbObAYAcBz0kvg==/109951172275960287.jpg",
|
||||
|
||||
"total": 72,
|
||||
|
||||
"desc": "当城市喧嚣渐远\n让音符化作自然的信使\n\n钢琴的澄澈如林间晨露\n弦乐的温柔似晚风拂叶\n\n虫鸣与流水声悄然交织\n每一段旋律都在勾勒远山\n\n晴空与旷野的轮廓\n无需刻意追寻\n\n只需闭上眼\n便能在旋律里触摸自然 的呼吸\n\n让身心沉潜于这份不被打扰的宁静\n重拾内心的松弛与澄澈……",
|
||||
|
||||
"source": "wy"
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"play_count": "4681",
|
||||
|
||||
"id": "17440686473",
|
||||
|
||||
"author": "與鯨島",
|
||||
|
||||
"name": "刘宇|我生如刀锋 自当斩荆棘",
|
||||
|
||||
"time": "2025-11-15",
|
||||
|
||||
"img": "http://p2.music.126.net/l6q13AT-n47NICqDGf90lw==/109951172291475298.jpg",
|
||||
|
||||
"total": 50,
|
||||
|
||||
"desc": "他说:\n“我接受来自四面八方的欢呼鼓舞,也接受不明缘由的流言与我接受来自四面八方的欢呼鼓舞,也 接受不明缘由的流言与误解。我低谷过,但没人能熄灭我跳动的火焰。眼前不变的是光明和希望,再尖锐的噪音也无法打 断已经响起的旋律,仍在舞蹈,就是我的态度。”\n\n“我很早之前就说过,恶评它其实不会伤害到我,哪怕我说的一句话 ,它没有错误,有些人都可以在TA的立场上找出这句话的错误,我觉得这个不太需要去考虑的,因为你做什么事情问心无 愧最重要。”\n\n“我觉得你们也在发光,是因为你们的光汇聚在一起,我才能这么明亮。”\n\n“天赋固然重要,但我想要 才更重要。”\n\n“愿你我皆被温柔以待,身处泥泞也嗅得大地芳香。”\n\n“我想告诉你们的是,是你们的光唤醒了我心中 的那颗种子,我知道有一天,这颗种子,可以成为为你们遮挡伤害的那棵大树,请相信我。”\n\n“谢谢您曾对我说,要去 做那个扇风的人,要让全世界知道这把扇子扇出来的风,叫中国风。”\n\n“国风推广这条路,其实我的初心一直不变,依 旧在前行,也理解我作为公众人物应该负担起更多的责误解。我低谷过,但没人能熄灭我跳动的火焰。眼前不变的是光明 和希望,再尖锐的噪音也无法打断已经响起的旋律,仍在舞蹈,就是我的态度。”\n\n“我很早之前就说过,恶评它其实不 会伤害到我,哪怕我说的一句话,它没有错误,有些人都可以在TA的立场上找出这句话的错误,我觉得这个不太需要去考 虑的,因为你做什么事情问心无愧最重要。”\n\n“我觉得你们也在发光,是因为你们的光汇聚在一起,我才能这么明亮。”\n\n“天赋固然重要,但我想要才更重要。”\n\n“愿你我皆被温柔以待,身处泥泞也嗅得大地芳香。”\n\n“我想告诉你们的是,是你们的光唤醒了我心中的那颗种子,我知道有一天,这颗种子,可以成为为你们遮挡伤害的那棵大树,请相信我。”\n\n“谢谢您曾对我说,要去做那个扇风的人,要让全世界知道这把扇子扇出来的风,叫中国风。”\n\n“国风推广这条路,其实我的初心一直不变,依旧在前行,也理解我作为公众人物应该负担起更多的责任、传播的重任。希望未来不负期望, 能号召更多的年轻的朋友关注和热爱国风、传统文化。”\n\n“把冠军定在道路上,而不是目标上。”\n\n“你们带着特别热 情,特别想要跟你传达他们的爱的时候,你就觉得,这点累算什么。”\n\n“太在意恶评,是对好评的不负责。”\n\n",
|
||||
|
||||
"source": "wy"
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"play_count": "2.1万",
|
||||
|
||||
"id": "17412466554",
|
||||
|
||||
"author": "走马川行北海边",
|
||||
|
||||
"name": "网上很火韩语歌曲|宿命感拉满氛围神曲",
|
||||
|
||||
"time": "2025-11-08",
|
||||
|
||||
"img": "http://p2.music.126.net/Rf9Nx1eefoGbqGhqq84SBw==/109951172254139596.jpg",
|
||||
|
||||
"total": 72,
|
||||
|
||||
"desc": "前奏一响,便是跨越时空的宿命羁绊。这些火爆全网的韩语神曲,藏着韩剧里初雪拔剑的悸动、跨越百年的深情,也藏着爱而不得的怅惘与命中注定的奔赴。空灵声线交织着缱绻旋律,氛围感直接拉满,每一段旋律都像在诉说: 有些人,遇见即是宿命,哪怕兜兜转转,也终会被命运牵引。戴上耳机,沉浸式坠入这场关于宿命的浪漫与遗憾。",
|
||||
|
||||
"source": "wy"
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"play_count": "1.2万",
|
||||
|
||||
"id": "17426483259",
|
||||
|
||||
"author": "心语馆",
|
||||
|
||||
"name": "【欧美旋律控】前奏封神 耳熟能详欧美神曲",
|
||||
|
||||
"time": "2025-11-12",
|
||||
|
||||
"img": "http://p2.music.126.net/4EiMzo9LAGbNjwF79ecuZg==/109951172271263928.jpg",
|
||||
|
||||
"total": 67,
|
||||
|
||||
"desc": "这份歌单,是资深乐迷私藏的欧美旋律宝库。从格莱美获奖金曲,到小众音乐人的惊艳之作,每一首都经过时间与口碑的双重检验。\n\n有Adele用灵魂嗓音唱出的《Rolling in the Deep》,那强烈鼓点和凄美钢琴交织,让失恋 的痛楚与坚强直击人心;也有Coldplay用《Viva la Vida》带来的摇滚盛宴,壮丽弦乐搭配激昂节奏,展现帝国兴衰。这 里既有流行、摇滚、民谣等主流风格,也不乏R&B、电子乐等小众类型,满足不同音乐偏好。\n\n无论你是在午后闲暇、深夜独处,还是在通勤路上,这些旋律都能为你带来极致的听觉享受,让你沉浸在欧美音乐的独特魅力中,一听就上瘾,循 环到停不下来 。",
|
||||
|
||||
"source": "wy"
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"play_count": "4198",
|
||||
|
||||
"id": "17514849470",
|
||||
|
||||
"author": "心语馆",
|
||||
|
||||
"name": "【高质量轻音】清新宁静 明亮自然",
|
||||
|
||||
"time": "2025-12-04",
|
||||
|
||||
"img": "http://p2.music.126.net/v0jiaItY2fD3LuQVrdt_Cg==/109951172384635472.jpg",
|
||||
|
||||
"total": 71,
|
||||
|
||||
"desc": "蝉鸣躲进树荫,落叶在石阶上打了个转。木吉他的弦音裹着松针的气息,手鼓轻得像松鼠踩过腐叶。不必追赶时间,只需闭眼,让音符替你接住林间漏下的每一缕暖阳。",
|
||||
|
||||
"source": "wy"
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"play_count": "1万",
|
||||
|
||||
"id": "14407617761",
|
||||
|
||||
"author": "露露在发呆",
|
||||
|
||||
"name": "华语R&B:像一只蝴蝶飞过废墟",
|
||||
|
||||
"time": "2025-10-11",
|
||||
|
||||
"img": "http://p2.music.126.net/nYl-Rts9LlhdrGRIx5St-g==/109951172136819703.jpg",
|
||||
|
||||
"total": 58,
|
||||
|
||||
"desc": "我瞒着所有人装作迈过了很多坎,故意显得很开心的样子,事实上只有我知道,阴影就是阴影,有些坎我永远也迈不过去,有些事一时半会释怀,我承认你很好,有些事跟我喜欢你没关系,有些事没在一个频道上,更没办法将两 件事放一起衡量。",
|
||||
|
||||
"source": "wy"
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"play_count": "8188",
|
||||
|
||||
"id": "17511869819",
|
||||
|
||||
"author": "有只黑猫叫灰心",
|
||||
|
||||
"name": "「韩语宿命感」首尔晚风里的心跳",
|
||||
|
||||
"time": "2025-12-04",
|
||||
|
||||
"img": "http://p2.music.126.net/wXRLNUreaGRVcU7CYe20KA==/109951172381558389.jpg",
|
||||
|
||||
"total": 50,
|
||||
|
||||
"desc": "ᥫᩣ当首尔的晚风裹着便利店的暖光掠过耳机线,韩语歌词里那些轻颤的音节,正藏着和你同频的心跳共振。这是专属于宿命感的BGM——每一段旋律都像被按下慢放的初遇镜头:是走廊转角撞进眼底的视线,是咖啡杯沿碰在一起时的轻响,是晚风里没说出口的「恰好我也喜欢你」。耳机里的每一句,都是藏在旋律里的、只属于你的心动序章✨",
|
||||
|
||||
"source": "wy"
|
||||
|
||||
}
|
||||
|
||||
]);
|
||||
|
||||
const handlePlayListClick = (item: any) => {
|
||||
console.log("跳转到歌单详情:", item.id);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.home-view {
|
||||
padding-bottom: 40px;
|
||||
|
||||
.section-container {
|
||||
margin-bottom: 40px;
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.section-title {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
color: #1a1a1a;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.more {
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
&:hover { color: #ff6b6b; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.playlist-grid {
|
||||
display: grid;
|
||||
// 使用 auto-fill 实现响应式列数
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 24px;
|
||||
|
||||
.playlist-card {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
|
||||
&:hover {
|
||||
.cover-wrapper {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
|
||||
|
||||
img { transform: scale(1.05); }
|
||||
}
|
||||
.playlist-name { color: #ff6b6b; }
|
||||
}
|
||||
|
||||
.cover-wrapper {
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
background: #f0f0f0;
|
||||
margin-bottom: 12px;
|
||||
transition: all 0.4s ease;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.6s ease;
|
||||
}
|
||||
|
||||
.play-count {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(8px);
|
||||
color: white;
|
||||
padding: 4px 10px;
|
||||
border-radius: 20px;
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.playlist-info {
|
||||
.playlist-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
// 两行文本截断
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.playlist-author {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
25
tsconfig.json
Normal file
25
tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "electron"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
11
tsconfig.node.json
Normal file
11
tsconfig.node.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
29
vite.config.ts
Normal file
29
vite.config.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import path from 'node:path'
|
||||
import electron from 'vite-plugin-electron/simple'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
electron({
|
||||
main: {
|
||||
// Shortcut of `build.lib.entry`.
|
||||
entry: 'electron/main.ts',
|
||||
},
|
||||
preload: {
|
||||
// Shortcut of `build.rollupOptions.input`.
|
||||
// Preload scripts may contain Web assets, so use the `build.rollupOptions.input` instead `build.lib.entry`.
|
||||
input: path.join(__dirname, 'electron/preload.ts'),
|
||||
},
|
||||
// Ployfill the Electron and Node.js API for Renderer process.
|
||||
// If you want use Node.js in Renderer process, the `nodeIntegration` needs to be enabled in the Main process.
|
||||
// See 👉 https://github.com/electron-vite/vite-plugin-electron-renderer
|
||||
renderer: process.env.NODE_ENV === 'test'
|
||||
// https://github.com/electron-vite/vite-plugin-electron-renderer/issues/78#issuecomment-2053600808
|
||||
? undefined
|
||||
: {},
|
||||
}),
|
||||
],
|
||||
})
|
||||
Reference in New Issue
Block a user