diff --git a/.env.example b/.env.example index 049df13..5316429 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,14 @@ # Github Token -VITE_GITHUB_TOKEN=your-github-token +NUXT_PUBLIC_GITHUB_TOKEN=your-github-token # UMAMI API KEY -VITE_UMAMI_API_KEY=your-umami-api-key +NUXT_PUBLIC_UMAMI_API_KEY=your-umami-api-key + +# Wakatime API Key +WAKATIME_API_KEY=your-wakatime-api-key + +# Wakatime API Base(可选) +WAKATIME_API_URL=https://wakatime.com/api/v1 # SMTP 服务器地址 SMTP_HOST=smtp.example.com diff --git a/.gitignore b/.gitignore index 14aaedd..b4cca23 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ build/ .next/ .vercel/ .vite/ +.nuxt/ +.output/ +.edgeone # env & secrets .env diff --git a/README.md b/README.md index 60c463f..6783822 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Cloud Home -一个基于 Vue 的个人主页模板,内置友链申请、网站展示、项目展示、友链随机展示、自定义配置,支持 Vercel 部署与邮件通知。 +一个基于 Nuxt 3 (Vue 3) 的个人主页模板,内置友链申请、网站展示、项目展示、友链随机展示、自定义配置,支持 Vercel 部署与邮件通知。 ## 特性 @@ -11,16 +11,16 @@ ## 技术栈 -- 前端:Vue 3 + HTML + CSS -- 构建:Vite -- 部署:Vercel(静态构建 + Serverless Functions) +- 前端:Nuxt 3(Vue 3)+ HTML + CSS +- 构建 / 运行:Nuxt 3 + Nitro +- 部署:Vercel(Nuxt 构建 + Nitro 函数) ## 致谢 排名不分先后 - [Skill Icons](https://github.com/tandpfun/skill-icons):技能图标库,本项目的技能图标来源。 -- [Netease Mini Player](https://github.com/numakkiyu/NeteaseMiniPlayer):迷你网易云播放器组件,为本项目的音乐播放功能提供支持。 +- [Netease Mini Player](https://github.com/numakkiyu/NeteaseMiniPlayer):迷你网易云播放器组件,为本项目的音乐播放功能提供支持。(本项目使用[本人fork的版本](https://github.com/RhenCloud/NeteaseMiniPlayer)) 感谢以上开源项目原作者与维护者的贡献。 @@ -35,7 +35,7 @@ const siteConfig: SiteConfig = { profile: { name: "Example User", // 你的名字 title: "I'm a software developer.", // 你的简介,可为空 - avatar: "avatar.webp", // 你的头像,可为public目录下的文件或外部链接 + avatar: "/avatar.webp", // 你的头像,可为public目录下的文件或外部链接 bio: "Hello World", // 你的喜欢的一句话,可为空 birthday: "xxxx-xx-xx", // 你的生日,可为空 gender: "", // 你的性别,可为空 @@ -66,7 +66,7 @@ const siteConfig: SiteConfig = { siteMeta: { title: "Example Title", // 网站标题 - icon: "favicon.ico", // 网站图标,可为public目录下的文件或外部链接 + icon: "/favicon.ico", // 网站图标,可为public目录下的文件或外部链接 startDate:"xxxx-xx-xx", // 网站创建日期 }, @@ -90,6 +90,10 @@ const siteConfig: SiteConfig = { autoplay: false, // 是否默认以黑胶唱片状态启动(仅浮动模式) defaultMinimized: true, + // 标签页非激活时是否自动暂停 + autoPause: false, + // Music API 配置 + apiUrls: ["https://www.bilibili.uno/api", "https://meting-api.wangcy.site/api"], }, umami: { @@ -166,16 +170,17 @@ const siteConfig: SiteConfig = { 在 Vercel 控制台或本地 `.env` 配置: -- `VITE_GITHUB_TOKEN`: 具有仓库读取权限的 GitHub Token,用于绕过 GitHub API 速率限制。(可选) -- `UMAMI_API_KEY`: Umami 分析的 API Key。 +- `NUXT_PUBLIC_GITHUB_TOKEN`: 具有仓库读取权限的 GitHub Token,用于绕过 GitHub API 速率限制。(可选) +- `NUXT_PUBLIC_UMAMI_API_KEY`: 可选的 Umami API Key,用于展示访问量统计数据。 - `WAKATIME_API_KEY`: Wakatime API Key,用于获取编码统计数据。 +- `WAKATIME_API_URL`: Wakatime API 地址,覆盖默认 `https://wakatime.com/api/v1`(可选)。 - `SMTP_HOST`: 邮件服务器主机名 - `SMTP_PORT`: 端口(如 465 或 587) - `SMTP_USER`: 发件人邮箱账号 - `SMTP_PASS`: 邮箱授权码或密码 -- `MAIL_FROM`: 发件人地址(通常同 SMTP_USER) +- `SENDER_EMAIL`: 发件人地址(通常同 SMTP_USER) - `ADMIN_EMAIL`: 接收通知的邮箱地址 -- `SMTP_SECURE`:是否启用 SSL/TLS 加密(默认 `true`) +- `SMTP_SECURE`:是否强制启用 SSL/TLS(默认为 `true` 当端口为 465)。 ## 本地开发 @@ -184,7 +189,7 @@ pnpm install pnpm dev ``` -访问 `http://localhost:5173/`。 +访问 `http://localhost:3000/`。 ## 构建 @@ -192,7 +197,7 @@ pnpm dev pnpm build ``` -产物输出到 +产物输出到 Nuxt 的 `.output/` 目录,该目录同时包含静态资源与 Nitro 服务器入口。 ## 部署到 Vercel @@ -201,6 +206,8 @@ pnpm build ## API +雁型的 Nitro 路由位于 `server/api`,依旧暴露同样的 `/api` 前缀。 + - `POST /api/send-mail`:友链申请邮件发送。请求体示例: ```json diff --git a/api/send-mail.ts b/api/send-mail.ts deleted file mode 100644 index 0413a3e..0000000 --- a/api/send-mail.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { VercelRequest, VercelResponse } from "@vercel/node"; -import nodemailer from "nodemailer"; - -export default async function handler(req: VercelRequest, res: VercelResponse) { - // CORS 处理 - // res.setHeader("Access-Control-Allow-Origin", "*"); - // res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS"); - // res.setHeader("Access-Control-Allow-Headers", "Content-Type"); - - // if (req.method === "OPTIONS") { - // return res.status(200).end(); - // } - - // if (req.method === "GET") { - // return res.status(200).json({ status: "ok", message: "use POST to send mail" }); - // } - - // if (req.method !== "POST") { - // return res.status(405).json({ error: "Method Not Allowed" }); - // } - - try { - const data = typeof req.body === "string" ? JSON.parse(req.body) : req.body; - const { name, email, site, desc, avatar } = data || {}; - if (!name || !email || !desc) { - return res.status(400).json({ error: "Missing required fields: name, email, desc" }); - } - - const transporter = nodemailer.createTransport({ - host: process.env.SMTP_HOST, - port: Number(process.env.SMTP_PORT || 465), - secure: Number(process.env.SMTP_PORT || 465) === 465, - auth: { - user: process.env.SMTP_USER, - pass: process.env.SMTP_PASS, - }, - }); - - const info = await transporter.sendMail({ - from: process.env.SENDER_EMAIL, - to: process.env.ADMIN_EMAIL, - subject: "友链申请 / 联系表单", - html: ` -
名称:${name}
-邮箱:${email}
-站点:${site || "未填写"}
-描述:${desc}
-头像:${avatar || "未填写"}
-时间:${new Date().toISOString()}
- `, - }); - - return res.status(200).json({ - message: "Mail sent", - id: info.messageId, - }); - } catch (err) { - return res.status(500).json({ error: "Send mail failed", detail: (err as Error).message }); - } -} diff --git a/api/wakatime.ts b/api/wakatime.ts deleted file mode 100644 index 1a03691..0000000 --- a/api/wakatime.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { VercelRequest, VercelResponse } from "@vercel/node"; - -export default async function handler(req: VercelRequest, res: VercelResponse) { - // CORS 处理 - res.setHeader("Access-Control-Allow-Origin", "*"); - res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS"); - res.setHeader("Access-Control-Allow-Headers", "Content-Type"); - - if (req.method === "OPTIONS") { - return res.status(200).end(); - } - - if (req.method !== "GET") { - return res.status(405).json({ error: "Method Not Allowed" }); - } - - const apiKey = process.env.WAKATIME_API_KEY; - const defaultApiUrl = process.env.WAKATIME_API_URL || "https://wakatime.com/api/v1"; - const apiUrl = (req.query.apiUrl as string) || defaultApiUrl; - if (!apiKey) { - return res.status(500).json({ error: "Wakatime API Key not configured" }); - } - - try { - // 获取一周统计数据 - const weeklyStatsResponse = await fetch(`${apiUrl}/users/current/stats/last_7_days`, { - headers: { - Authorization: `Basic ${Buffer.from(apiKey).toString("base64")}`, - }, - }); - - if (!weeklyStatsResponse.ok) { - throw new Error(`Wakatime API error: ${weeklyStatsResponse.status}`); - } - - const weeklyStatsData = await weeklyStatsResponse.json(); - - // 获取所有时间统计数据 - const allTimeStatsResponse = await fetch(`${apiUrl}/users/current/stats/all_time`, { - headers: { - Authorization: `Basic ${Buffer.from(apiKey).toString("base64")}`, - }, - }); - - let allTimeStatsData = null; - if (allTimeStatsResponse.ok) { - allTimeStatsData = await allTimeStatsResponse.json(); - } - - // 获取当前用户状态 - const statusResponse = await fetch(`${apiUrl}/users/current/status`, { - headers: { - Authorization: `Basic ${Buffer.from(apiKey).toString("base64")}`, - }, - }); - - let statusData = null; - if (statusResponse.ok) { - statusData = await statusResponse.json(); - } - - res.status(200).json({ - weekly: weeklyStatsData.data, - allTime: allTimeStatsData ? allTimeStatsData.data : null, - status: statusData, - }); - } catch (error) { - console.error("Wakatime API error:", error); - res.status(500).json({ error: "Failed to fetch Wakatime data" }); - } -} diff --git a/index.html b/index1.html similarity index 100% rename from index.html rename to index1.html diff --git a/nuxt.config.ts b/nuxt.config.ts new file mode 100644 index 0000000..d5c8c79 --- /dev/null +++ b/nuxt.config.ts @@ -0,0 +1,35 @@ +import { defineNuxtConfig } from "nuxt/config"; +import siteConfig from "./src/config/siteConfig"; + +export default defineNuxtConfig({ + compatibilityDate: "2025-12-12", + srcDir: "src/", + css: ["~/styles.css"], + app: { + head: { + title: siteConfig.siteMeta.title, + link: [{ rel: "icon", href: siteConfig.siteMeta.icon }], + }, + }, + nitro: { + prerender: { + crawlLinks: true, + routes: ["/sitemap.xml", "/rss.xml"], + }, + }, + runtimeConfig: { + smtpHost: process.env.SMTP_HOST ?? "", + smtpPort: Number(process.env.SMTP_PORT ?? 465), + smtpUser: process.env.SMTP_USER ?? "", + smtpPass: process.env.SMTP_PASS ?? "", + senderEmail: process.env.SENDER_EMAIL ?? "", + adminEmail: process.env.ADMIN_EMAIL ?? "", + smtpSecure: process.env.SMTP_SECURE ? process.env.SMTP_SECURE === "true" : undefined, + wakatimeApiKey: process.env.WAKATIME_API_KEY ?? "", + wakatimeApiUrl: process.env.WAKATIME_API_URL ?? "https://wakatime.com/api/v1", + public: { + githubToken: process.env.NUXT_PUBLIC_GITHUB_TOKEN ?? "", + umamiApiKey: process.env.NUXT_PUBLIC_UMAMI_API_KEY ?? "", + }, + }, +}); diff --git a/package.json b/package.json index 86a71df..aeeac6a 100644 --- a/package.json +++ b/package.json @@ -3,27 +3,19 @@ "private": true, "type": "module", "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview" + "dev": "nuxt dev", + "build": "nuxt build", + "generate": "nuxt generate", + "preview": "nuxt preview" }, "dependencies": { "@jaseeey/vue-umami-plugin": "^1.4.0", - "@vercel/node": "^5.5.15", - "cros": "^1.1.0", - "express": "^5.2.1", - "he": "^1.2.0", "nodemailer": "^7.0.11", - "vue": "^3.5.25", - "vue-router": "^4.6.3" + "nuxt": "^4.2.2" }, "devDependencies": { - "@types/express": "^5.0.6", "@types/node": "^24.10.1", "@types/nodemailer": "^7.0.4", - "@vitejs/plugin-vue": "^6.0.2", - "typescript": "^5.9.3", - "vite": "^7.2.6", - "vue-tsc": "^3.1.6" + "typescript": "^5.9.3" } } \ No newline at end of file diff --git a/public/css/netease-mini-player-v2.css b/public/css/netease-mini-player-v2.css new file mode 100644 index 0000000..1945ec1 --- /dev/null +++ b/public/css/netease-mini-player-v2.css @@ -0,0 +1,1340 @@ +/** + * [NMPv2] NeteaseMiniPlayer v2 CSS + * Lightweight Player Component Based on NetEase Cloud Music API + * + * Copyright 2025 BHCN STUDIO & 北海的佰川(ImBHCN[numakkiyu]) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:root { + --primary-bg: #fafafa; + --secondary-bg: #f0f0f0; + --bg-color: #fafafa; + --accent-color: #ff6b35; + --accent-color-2: #7a66ff; + --accent-color-3: #00c2ff; + --accent-gradient: linear-gradient( + 135deg, + var(--accent-color) 0%, + var(--accent-color-2) 50%, + var(--accent-color-3) 100% + ); + --text-primary: #333333; + --text-secondary: #666666; + --text-muted: #999999; + --shadow-light: rgba(255, 255, 255, 0.9); + --shadow-dark: rgba(0, 0, 0, 0.1); + --shadow-inset: inset 3px 3px 6px var(--shadow-dark), inset -3px -3px 6px var(--shadow-light); + --shadow-outset: 6px 6px 12px var(--shadow-dark), -6px -6px 12px var(--shadow-light); + --border-radius: 12px; + --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + --theme-transition: background-color 0.3s ease, color 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease; + + --flow-color-1: rgba(255, 107, 53, 0.32); + --flow-color-2: rgba(86, 151, 227, 0.26); + --flow-color-3: rgba(255, 209, 102, 0.26); + --flow-color-4: rgba(122, 102, 255, 0.28); + --flow-color-5: rgba(0, 194, 255, 0.22); + --flow-opacity: 0.45; + --flow-speed: 10s; + --volume-total-width: 60px; + --playlist-flow-opacity: 0.25; +} + +.netease-mini-player[data-theme="dark"] { + --primary-bg: #1e1e1e; + --secondary-bg: #2a2a2a; + --bg-color: #1e1e1e; + --accent-color: #ff8a50; + --accent-color-2: #6c63ff; + --accent-color-3: #00d4ff; + --accent-gradient: linear-gradient( + 135deg, + var(--accent-color) 0%, + var(--accent-color-2) 50%, + var(--accent-color-3) 100% + ); + --text-primary: #e0e0e0; + --text-secondary: #b0b0b0; + --text-muted: #808080; + --shadow-light: rgba(255, 255, 255, 0.05); + --shadow-dark: rgba(0, 0, 0, 0.4); + --shadow-inset: inset 3px 3px 6px var(--shadow-dark), inset -3px -3px 6px var(--shadow-light); + --shadow-outset: 6px 6px 12px var(--shadow-dark), -6px -6px 12px var(--shadow-light); + + --flow-color-1: rgba(255, 138, 80, 0.28); + --flow-color-2: rgba(86, 151, 227, 0.24); + --flow-color-3: rgba(140, 100, 255, 0.26); + --flow-color-4: rgba(124, 110, 255, 0.24); + --flow-color-5: rgba(0, 212, 255, 0.22); + --flow-opacity: 0.36; + --flow-speed: 12s; +} + +@media (prefers-color-scheme: dark) { + .netease-mini-player[data-theme="auto"] { + --primary-bg: #1e1e1e; + --secondary-bg: #2a2a2a; + --bg-color: #1e1e1e; + --accent-color: #ff8a50; + --accent-color-2: #6c63ff; + --accent-color-3: #00d4ff; + --accent-gradient: linear-gradient( + 135deg, + var(--accent-color) 0%, + var(--accent-color-2) 50%, + var(--accent-color-3) 100% + ); + --text-primary: #e0e0e0; + --text-secondary: #b0b0b0; + --text-muted: #808080; + --shadow-light: rgba(255, 255, 255, 0.05); + --shadow-dark: rgba(0, 0, 0, 0.4); + --shadow-inset: inset 3px 3px 6px var(--shadow-dark), inset -3px -3px 6px var(--shadow-light); + --shadow-outset: 6px 6px 12px var(--shadow-dark), -6px -6px 12px var(--shadow-light); + + --flow-color-1: rgba(255, 138, 80, 0.28); + --flow-color-2: rgba(86, 151, 227, 0.24); + --flow-color-3: rgba(140, 100, 255, 0.26); + --flow-color-4: rgba(124, 110, 255, 0.24); + --flow-color-5: rgba(0, 212, 255, 0.22); + --flow-opacity: 0.36; + --flow-speed: 12s; + } +} +@media (prefers-color-scheme: light) { + .netease-mini-player[data-theme="auto"] { + --primary-bg: #fafafa; + --secondary-bg: #f0f0f0; + --bg-color: #fafafa; + --accent-color: #ff6b35; + --accent-color-2: #7a66ff; + --accent-color-3: #00c2ff; + --accent-gradient: linear-gradient( + 135deg, + var(--accent-color) 0%, + var(--accent-color-2) 50%, + var(--accent-color-3) 100% + ); + --text-primary: #333333; + --text-secondary: #666666; + --text-muted: #999999; + --shadow-light: rgba(255, 255, 255, 0.9); + --shadow-dark: rgba(0, 0, 0, 0.1); + --shadow-inset: inset 3px 3px 6px var(--shadow-dark), inset -3px -3px 6px var(--shadow-light); + --shadow-outset: 6px 6px 12px var(--shadow-dark), -6px -6px 12px var(--shadow-light); + + --flow-color-1: rgba(255, 107, 53, 0.32); + --flow-color-2: rgba(86, 151, 227, 0.26); + --flow-color-3: rgba(255, 209, 102, 0.26); + --flow-color-4: rgba(122, 102, 255, 0.28); + --flow-color-5: rgba(0, 194, 255, 0.22); + --flow-opacity: 0.45; + --flow-speed: 10s; + } +} + +.netease-mini-player { + width: 400px; + height: 120px; + background: var(--bg-color); + border-radius: 16px; + padding: 12px; + box-shadow: var(--shadow-outset); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + position: relative; + overflow: visible; + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: 8px; + transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1), height 0.5s cubic-bezier(0.4, 0, 0.2, 1), + border-radius 0.5s cubic-bezier(0.4, 0, 0.2, 1), padding 0.5s cubic-bezier(0.4, 0, 0.2, 1), + var(--theme-transition); + background: 0.5s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.5s cubic-bezier(0.4, 0, 0.2, 1); +} + +.netease-mini-player::before { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + pointer-events: none; + background: radial-gradient(circle at 15% 20%, var(--flow-color-1) 0%, transparent 60%), + radial-gradient(circle at 80% 25%, var(--flow-color-2) 0%, transparent 60%), + radial-gradient(circle at 30% 85%, var(--flow-color-3) 0%, transparent 60%), + radial-gradient(circle at 10% 75%, var(--flow-color-4) 0%, transparent 55%), + radial-gradient(circle at 85% 80%, var(--flow-color-5) 0%, transparent 55%); + opacity: var(--flow-opacity); + background-size: 220% 220%, 220% 220%, 220% 220%, 220% 220%, 220% 220%; + background-position: 0% 0%, 100% 0%, 0% 100%, 20% 80%, 80% 20%; + filter: saturate(1.02) brightness(1.01); + transform: scale(1); + animation: flow-breathe var(--flow-speed) ease-in-out infinite; + animation-play-state: paused; +} + +.netease-mini-player.player-playing::before { + animation-play-state: running; +} + +@keyframes flow-breathe { + 0% { + background-position: 0% 0%, 100% 0%, 0% 100%, 20% 80%, 80% 20%; + transform: scale(1); + filter: saturate(1.02) brightness(1.01); + } + 50% { + background-position: 100% 50%, 0% 100%, 50% 0%, 35% 65%, 65% 35%; + transform: scale(1.03); + filter: saturate(1.15) brightness(1.06); + } + 100% { + background-position: 0% 100%, 100% 50%, 100% 0%, 20% 80%, 80% 20%; + transform: scale(1); + filter: saturate(1.02) brightness(1.01); + } +} + +.netease-mini-player.minimized { + width: 80px; + height: 80px; + border-radius: 50%; + padding: 0; + background: radial-gradient(120px 120px at 30% 30%, rgba(255, 138, 80, 0.25), transparent 60%), + radial-gradient(120px 120px at 70% 70%, rgba(0, 210, 255, 0.22), transparent 60%), + linear-gradient(135deg, #151515, #1e1e1e); + box-shadow: 4px 4px 12px rgba(0, 0, 0, 0.4), -2px -2px 8px rgba(255, 255, 255, 0.1), + inset 0 0 20px rgba(0, 0, 0, 0.3); +} + +.netease-mini-player.minimized .song-content, +.netease-mini-player.minimized .controls, +.netease-mini-player.minimized .playlist-container { + opacity: 0; + visibility: hidden; + transition: opacity 0.3s ease, visibility 0.3s ease; +} + +.netease-mini-player.minimized .player-bottom { + opacity: 0; + visibility: hidden; + transition: opacity 0.3s ease, visibility 0.3s ease; +} + +.netease-mini-player.minimized .minimize-btn { + opacity: 1 !important; + visibility: visible !important; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 1000; + background: rgba(0, 0, 0, 0.7); + border-radius: 50%; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + transition: all 0.3s ease; +} + +.netease-mini-player.minimized .minimize-btn:hover { + background: rgba(0, 0, 0, 0.9); + transform: translate(-50%, -50%) scale(1.1); +} + +.netease-mini-player .song-content, +.netease-mini-player .controls, +.netease-mini-player .player-bottom { + opacity: 1; + visibility: visible; + transition: opacity 0.3s ease 0.2s, visibility 0.3s ease 0.2s; +} + +.netease-mini-player.minimized .album-cover-container { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + border-radius: 50%; + box-shadow: none; + transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1), height 0.5s cubic-bezier(0.4, 0, 0.2, 1), + top 0.5s cubic-bezier(0.4, 0, 0.2, 1), left 0.5s cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 0.5s cubic-bezier(0.4, 0, 0.2, 1); +} + +.netease-mini-player.minimized .album-cover { + width: 100%; + height: 100%; + border-radius: 50%; + filter: brightness(0.8) contrast(1.1); + transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1), height 0.5s cubic-bezier(0.4, 0, 0.2, 1), + filter 0.5s cubic-bezier(0.4, 0, 0.2, 1); +} + +.netease-mini-player.minimized .vinyl-overlay { + background: radial-gradient( + circle at center, + transparent 15%, + rgba(0, 0, 0, 0.1) 15%, + rgba(0, 0, 0, 0.1) 16%, + transparent 16%, + transparent 25%, + rgba(0, 0, 0, 0.15) 25%, + rgba(0, 0, 0, 0.15) 26%, + transparent 26%, + transparent 35%, + rgba(0, 0, 0, 0.1) 35%, + rgba(0, 0, 0, 0.1) 36%, + transparent 36%, + transparent 45%, + rgba(0, 0, 0, 0.15) 45%, + rgba(0, 0, 0, 0.15) 46%, + transparent 46% + ); + box-shadow: inset 0 0 15px rgba(0, 0, 0, 0.5), inset 0 0 5px rgba(0, 0, 0, 0.8); + transition: background 0.5s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.5s cubic-bezier(0.4, 0, 0.2, 1); +} + +.netease-mini-player.minimized .vinyl-center { + width: 16px; + height: 16px; + background: radial-gradient(circle at 30% 30%, #666 0%, #333 50%, #111 100%); + box-shadow: 0 0 3px rgba(0, 0, 0, 0.8), inset 1px 1px 2px rgba(255, 255, 255, 0.2); + border: 1px solid #000; + transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1), height 0.5s cubic-bezier(0.4, 0, 0.2, 1), + background 0.5s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.5s cubic-bezier(0.4, 0, 0.2, 1), + border 0.5s cubic-bezier(0.4, 0, 0.2, 1); +} + +.netease-mini-player[data-position="top-left"] { + position: fixed; + top: 20px; + left: 20px; + z-index: 1000; +} + +.netease-mini-player[data-position="top-right"] { + position: fixed; + top: 20px; + right: 20px; + z-index: 1000; +} + +.netease-mini-player[data-position="bottom-left"] { + position: fixed; + bottom: 20px; + left: 20px; + z-index: 1000; +} + +.netease-mini-player[data-position="bottom-right"] { + position: fixed; + bottom: 20px; + right: 20px; + z-index: 1000; +} + +.netease-mini-player[data-position="static"] { + position: relative; +} + +.player-main { + display: flex; + align-items: center; + gap: 12px; + flex: 1; +} + +.album-cover-container { + width: 60px; + height: 60px; + border-radius: 50%; + position: relative; + flex-shrink: 0; + background: var(--secondary-bg); + box-shadow: inset 2px 2px 4px var(--shadow-dark), inset -2px -2px 4px var(--shadow-light), + 2px 2px 6px rgba(0, 0, 0, 0.2), -1px -1px 3px rgba(255, 255, 255, 0.1); + overflow: hidden; + cursor: pointer; + border: 2px solid rgba(0, 0, 0, 0.1); +} +.album-cover-container::after { + content: ""; + position: absolute; + inset: -4px; + border-radius: 50%; + pointer-events: none; + background: conic-gradient( + from 0deg, + var(--accent-color), + var(--accent-color-2), + var(--accent-color-3), + var(--accent-color) + ); + opacity: 0.18; + filter: blur(6px) saturate(1.05); +} + +.album-cover { + width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; + transition: var(--transition); + filter: brightness(0.9) contrast(1.1); +} + +.vinyl-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: 50%; + background: radial-gradient( + circle at center, + transparent 12%, + rgba(0, 0, 0, 0.8) 12%, + rgba(0, 0, 0, 0.8) 15%, + transparent 15%, + transparent 20%, + rgba(0, 0, 0, 0.15) 20%, + rgba(0, 0, 0, 0.15) 20.5%, + transparent 20.5%, + transparent 25%, + rgba(0, 0, 0, 0.1) 25%, + rgba(0, 0, 0, 0.1) 25.5%, + transparent 25.5%, + transparent 30%, + rgba(0, 0, 0, 0.08) 30%, + rgba(0, 0, 0, 0.08) 30.5%, + transparent 30.5%, + transparent 35%, + rgba(0, 0, 0, 0.06) 35%, + rgba(0, 0, 0, 0.06) 35.5%, + transparent 35.5%, + transparent 40%, + rgba(0, 0, 0, 0.05) 40%, + rgba(0, 0, 0, 0.05) 40.5%, + transparent 40.5%, + transparent 85%, + rgba(0, 0, 0, 0.1) 85%, + rgba(0, 0, 0, 0.2) 100% + ); + pointer-events: none; + box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.3), inset 0 0 20px rgba(0, 0, 0, 0.1); +} + +.vinyl-center { + position: absolute; + top: 50%; + left: 50%; + width: 12px; + height: 12px; + background: radial-gradient(circle at center, #1a1a1a 0%, #333 50%, #1a1a1a 100%); + border-radius: 50%; + transform: translate(-50%, -50%); + box-shadow: 0 0 4px rgba(0, 0, 0, 0.5), inset 0 0 3px rgba(0, 0, 0, 0.8), inset 0 0 1px rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.1); +} + +@keyframes vinyl-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.album-cover { + animation: vinyl-spin 6s linear infinite; + animation-play-state: paused; +} + +.album-cover.playing { + animation-play-state: running; +} + +.vinyl-overlay { + animation: vinyl-spin 3s linear infinite; + animation-play-state: paused; +} + +.album-cover.playing + .vinyl-overlay { + animation-play-state: running; +} + +.song-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; +} + +.song-info { + display: flex; + flex-direction: column; + gap: 2px; +} + +.song-title { + font-size: 14px; + font-weight: 600; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + line-height: 1.2; + transition: var(--theme-transition); +} + +.song-artist { + font-size: 12px; + color: var(--text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + line-height: 1.2; + transition: var(--theme-transition); +} + +.lyrics-container { + min-height: 20px; + display: flex; + flex-direction: column; + gap: 2px; +} + +.lyrics-container.hidden { + display: none; +} + +.lyric-line { + font-size: 11px; + color: #5697e3; + line-height: 1.4; + transition: var(--transition); + opacity: 0.8; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: 100%; + transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out; +} + +.lyric-line.current { + color: #5697e3; + font-weight: 600; + opacity: 1; + text-shadow: 0 0 8px rgba(86, 151, 227, 0.3); + animation: lyric-glow 2s ease-in-out infinite alternate; + + animation: none; + opacity: 0; + transform: translateY(10px); + animation: lyric-fade-in 0.2s ease-out forwards; +} + +.lyric-line.translation { + font-size: 10px; + color: var(--text-muted); + opacity: 0.7; + margin-top: 1px; + transition: var(--theme-transition); +} + +.lyric-line.translation.current { + color: var(--text-secondary); + opacity: 0.9; + font-weight: 500; + transition: var(--theme-transition); +} + +@keyframes lyric-fade-in { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes lyric-glow { + 0% { + text-shadow: 0 0 8px rgba(86, 151, 227, 0.3); + } + 100% { + text-shadow: 0 0 12px rgba(86, 151, 227, 0.5); + } +} + +.lyric-line.word-lyric { + position: relative; + display: inline-block; + max-width: 100%; + white-space: nowrap; + overflow: hidden; +} + +.lyric-word-line { + position: relative; + display: inline-block; + transition: transform 0.1s ease-out; +} + +.lyric-line.word-lyric-active { + -webkit-mask: linear-gradient(90deg, #000 0%, #000 var(--line-progress, 0%), transparent var(--line-progress, 0%)); + mask: linear-gradient(90deg, #000 0%, #000 var(--line-progress, 0%), transparent var(--line-progress, 0%)); +} + +.netease-mini-player[data-theme="dark"] .lyric-line.word-lyric-active { + text-shadow: 0 0 8px rgba(255, 165, 0, 0.7); +} + +.lyric-line.word-lyric::after { + content: ""; + position: absolute; + bottom: 0; + left: 0; + width: var(--line-progress, 0%); + height: 2px; + background: var(--accent-gradient); + border-radius: 1px; + transition: width 0.1s ease; +} + +.controls { + display: flex; + align-items: center; + gap: 6px; + flex-shrink: 0; +} + +.control-btn { + width: 32px; + height: 32px; + border: none; + background: var(--primary-bg); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: var(--transition); + box-shadow: 2px 2px 4px var(--shadow-dark), -2px -2px 4px var(--shadow-light); + font-size: 12px; + color: var(--text-primary); + position: relative; +} + +.control-btn:hover { + box-shadow: inset 1px 1px 2px var(--shadow-dark), inset -1px -1px 2px var(--shadow-light); + background: var(--secondary-bg); +} + +.play-btn { + width: 30px; + height: 30px; + font-size: 14px; + background: var(--primary-bg); + box-shadow: 2px 2px 4px var(--shadow-dark), -2px -2px 4px var(--shadow-light); +} + +.play-icon, +.pause-icon { + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + color: var(--text-primary); + transition: var(--transition); +} + +.play-btn:hover { + background: var(--secondary-bg); + box-shadow: inset 1px 1px 2px var(--shadow-dark), inset -1px -1px 2px var(--shadow-light); +} + +.control-btn:active { + box-shadow: inset 2px 2px 4px var(--shadow-dark), inset -2px -2px 4px var(--shadow-light); + background: var(--accent-gradient); + color: white; +} + +.control-btn:active span { + color: white; +} + +.play-icon { + margin-left: 2px; +} + +.volume-icon { + font-size: 10px; + color: var(--text-secondary); + margin-right: 2px; + flex-shrink: 0; +} + +.player-bottom { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + width: 100%; +} + +.progress-container { + display: flex; + align-items: center; + gap: 6px; + flex: 1; + max-width: var(--progress-max, 60%); +} + +.netease-mini-player[data-embed="true"] .progress-container { + max-width: calc(100% - 60px); + flex: 1; +} + +.time-display { + font-size: 10px; + color: var(--text-muted); + white-space: nowrap; + min-width: 35px; + text-align: center; +} + +.progress-bar-container { + flex: 1; + height: 4px; + background: var(--secondary-bg); + border-radius: 2px; + position: relative; + cursor: pointer; + box-shadow: inset 1px 1px 2px var(--shadow-dark), inset -1px -1px 2px var(--shadow-light); +} + +.progress-bar { + height: 100%; + background: var(--accent-gradient); + border-radius: 2px; + width: 0%; + transition: width 0.1s ease; + position: relative; +} + +.progress-bar::after { + content: ""; + position: absolute; + right: -2px; + top: 50%; + width: 8px; + height: 8px; + background: var(--accent-color-3); + border-radius: 50%; + transform: translateY(-50%); + box-shadow: 0 0 8px rgba(0, 210, 255, 0.6), 1px 1px 2px var(--shadow-dark), -1px -1px 2px var(--shadow-light); +} + +.bottom-controls { + display: flex; + align-items: center; + gap: 8px; + justify-content: flex-end; + flex-shrink: 0; + min-width: 0; +} + +.volume-container { + display: flex; + align-items: center; + gap: 4px; + flex-shrink: 0; +} + +.volume-slider-container { + display: flex; + align-items: center; + width: 40px; +} + +.volume-slider { + flex: 1; + height: 3px; + background: var(--secondary-bg); + border-radius: 2px; + position: relative; + cursor: pointer; + box-shadow: inset 1px 1px 2px var(--shadow-dark), inset -1px -1px 2px var(--shadow-light); +} + +.volume-bar { + height: 100%; + background: var(--accent-color); + border-radius: 2px; + width: 70%; + transition: width 0.1s ease; +} + +.netease-mini-player svg { + width: 100%; + height: 100%; + fill: currentColor; + display: block; +} + +.control-btn svg { + width: 18px; + height: 18px; +} + +.feature-btn svg { + width: 16px; + height: 16px; +} + +.volume-icon svg { + width: 14px; + height: 14px; +} + +.play-btn svg { + width: 22px; + height: 22px; + margin-left: 2px; +} + +.play-btn .pause-icon svg { + margin-left: 0; +} + +.control-btn, +.feature-btn { + font-size: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.minimize-btn svg { + width: 18px; + height: 18px; +} + +.sr-visually-hidden { + position: absolute !important; + width: 1px !important; + height: 1px !important; + margin: -1px !important; + overflow: hidden !important; + clip: rect(0 0 0 0) !important; + clip-path: inset(50%) !important; + white-space: nowrap !important; + border: 0 !important; + padding: 0 !important; +} + +.netease-mini-player.mobile-env .volume-container { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + overflow: hidden; + clip: rect(0 0 0 0); + clip-path: inset(50%); + white-space: nowrap; + border: 0; + padding: 0; + opacity: 0; + pointer-events: none; +} +.netease-mini-player.mobile-env .progress-container { + --progress-max: calc(60% + var(--volume-total-width, 60px)); +} +.netease-mini-player.mobile-env[data-embed="true"] .progress-container { + max-width: calc(100% - 60px + var(--volume-total-width, 60px)); +} + +@media (hover: none) and (pointer: coarse) { + .netease-mini-player.mobile-env .bottom-controls { + gap: 10px; + } +} + +.feature-btn { + width: 20px; + height: 20px; + border: none; + background: transparent; + cursor: pointer; + color: var(--text-muted); + transition: var(--transition); + font-size: 11px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.feature-btn:hover { + color: var(--accent-color); +} + +.feature-btn.active { + color: var(--accent-color); +} + +.netease-mini-player[data-embed="true"] .list-btn { + display: none !important; +} + +.netease-mini-player[data-embed="true"] .minimize-btn { + display: none !important; +} + +.netease-mini-player[data-position="static"] .minimize-btn { + display: none !important; +} + +.netease-mini-player[data-embed="true"] .prev-btn, +.netease-mini-player[data-embed="true"] .next-btn { + display: none !important; +} + +.netease-mini-player[data-embed="true"] .playlist-container { + display: none !important; +} + +.netease-mini-player-embed .list-btn, +.netease-mini-player-embed .minimize-btn, +.netease-mini-player-embed .prev-btn, +.netease-mini-player-embed .next-btn { + display: none !important; +} + +.netease-mini-player-embed .playlist-container { + display: none !important; +} + +.playlist-container { + position: absolute; + left: 0; + right: 0; + top: 100%; + background: var(--primary-bg); + border-radius: var(--border-radius); + box-shadow: 4px 4px 8px var(--shadow-dark), -4px -4px 8px var(--shadow-light); + max-height: 200px; + overflow: hidden; + z-index: 1001; + opacity: 0; + transform: translateY(-10px); + visibility: hidden; + transition: all 0.3s ease; + margin-top: 8px; +} + +.playlist-container::before { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + pointer-events: none; + background: radial-gradient(circle at 20% 15%, var(--flow-color-1) 0%, transparent 60%), + radial-gradient(circle at 75% 30%, var(--flow-color-2) 0%, transparent 55%), + radial-gradient(circle at 35% 85%, var(--flow-color-3) 0%, transparent 55%), + radial-gradient(circle at 25% 70%, var(--flow-color-4) 0%, transparent 50%), + radial-gradient(circle at 85% 75%, var(--flow-color-5) 0%, transparent 50%); + opacity: var(--playlist-flow-opacity); + background-size: 220% 220%, 220% 220%, 220% 220%, 220% 220%, 220% 220%; + background-position: 0% 0%, 100% 0%, 0% 100%, 20% 80%, 80% 20%; + filter: saturate(1.02) brightness(1.01); + transform: scale(1); + animation: flow-breathe var(--flow-speed) ease-in-out infinite; + animation-play-state: paused; +} + +.netease-mini-player.player-playing .playlist-container::before { + animation-play-state: running; +} + +.playlist-container.expand-up { + top: auto; + bottom: 100%; + margin-top: 0; + margin-bottom: 8px; + transform: translateY(10px); +} + +.playlist-container.expand-up.show { + transform: translateY(0); +} + +.playlist-container.show { + opacity: 1; + transform: translateY(0); + visibility: visible; +} + +.playlist-content { + max-height: 200px; + overflow-y: auto; + &::-webkit-scrollbar { + display: none; + } + scrollbar-width: none; +} + +.playlist-item { + padding: 8px 12px; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); + cursor: pointer; + transition: var(--transition); + display: flex; + align-items: center; + gap: 8px; +} + +.playlist-item:last-child { + border-bottom: none; +} + +.playlist-item:hover { + background: rgba(255, 107, 53, 0.08); + box-shadow: inset 1px 1px 2px var(--shadow-dark), inset -1px -1px 2px var(--shadow-light); + transform: translateX(2px); + transition: all 0.2s ease; +} + +.playlist-item.active { + background: rgba(255, 107, 53, 0.15); + color: var(--accent-color); + font-weight: 600; +} + +.playlist-item-index { + font-size: 10px; + color: var(--text-muted); + min-width: 16px; + text-align: center; +} + +.playlist-item.active .playlist-item-index { + color: var(--accent-color); +} + +.playlist-item-info { + flex: 1; + min-width: 0; +} + +.playlist-item-name { + font-size: 11px; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + line-height: 1.2; +} + +.playlist-item-artist { + font-size: 10px; + color: var(--text-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-top: 1px; +} + +.playlist-item.active .playlist-item-name, +.playlist-item.active .playlist-item-artist { + color: var(--accent-color); +} + +.loading { + display: flex; + align-items: center; + justify-content: center; + color: var(--text-muted); + font-size: 12px; +} + +.loading::after { + content: ""; + width: 12px; + height: 12px; + border: 2px solid var(--secondary-bg); + border-top: 2px solid var(--accent-color); + border-radius: 50%; + animation: spin 1s linear infinite; + margin-left: 8px; +} + +.playlist-item-cover { + width: 36px; + height: 36px; + border-radius: 8px; + object-fit: cover; + flex-shrink: 0; + background: var(--secondary-bg); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.error { + color: #ff4757; + font-size: 12px; + text-align: center; +} + +@media (max-width: 480px) { + .netease-mini-player { + width: 100%; + max-width: 360px; + height: 100px; + padding: 8px; + margin: 0 auto; + } + + .album-cover-container { + width: 50px; + height: 50px; + } + + .song-title { + font-size: 12px; + } + + .song-artist { + font-size: 10px; + } + + .lyric-line { + font-size: 10px; + } + + .control-btn { + width: 28px; + height: 28px; + font-size: 10px; + } + + .play-btn { + width: 32px; + height: 32px; + font-size: 12px; + } + + .time-display { + font-size: 9px; + } + + .feature-btn { + width: 20px; + height: 20px; + font-size: 10px; + } + + .volume-slider-container { + width: 50px; + } +} + +.netease-mini-player.dragging { + cursor: grabbing; + -webkit-user-select: none; + user-select: none; + pointer-events: none; + opacity: 0.8; + transform: scale(0.95); + z-index: 9999; + transition: none; +} + +.netease-mini-player { + opacity: 1; +} + +.netease-mini-player.minimized.idle { + opacity: 0.7; +} + +.netease-mini-player.minimized.fading-out { + animation: player-fade-out var(--opacity-duration-down, 0.6s) + var(--opacity-ease-out, cubic-bezier(0.22, 1, 0.36, 1)) forwards; +} + +.netease-mini-player.minimized.fading-in { + animation: player-fade-in var(--opacity-duration-up, 0.25s) var(--opacity-ease-in, cubic-bezier(0.4, 0, 0.2, 1)) + forwards; +} + +@keyframes player-fade-out { + from { + opacity: 1; + } + to { + opacity: 0.7; + } +} + +@keyframes player-fade-in { + from { + opacity: 0.7; + } + to { + opacity: 1; + } +} + +.netease-mini-player.minimized.docked-right { + transform: translateX(calc(50% + var(--dock-gap, 20px))); +} +.netease-mini-player.minimized.docked-left { + transform: translateX(calc(-50% - var(--dock-gap, 20px))); +} + +.netease-mini-player.minimized.fading-out.docked-right { + animation: player-fade-out var(--opacity-duration-down, 0.6s) + var(--opacity-ease-out, cubic-bezier(0.22, 1, 0.36, 1)) forwards, + player-dock-right var(--dock-duration, 0.45s) var(--dock-ease-out, cubic-bezier(0.18, 0.9, 0.2, 1)) forwards; +} +.netease-mini-player.minimized.fading-out.docked-left { + animation: player-fade-out var(--opacity-duration-down, 0.6s) + var(--opacity-ease-out, cubic-bezier(0.22, 1, 0.36, 1)) forwards, + player-dock-left var(--dock-duration, 0.45s) var(--dock-ease-out, cubic-bezier(0.18, 0.9, 0.2, 1)) forwards; +} + +.netease-mini-player.minimized.popping-right { + animation: player-popout-right var(--popout-duration, 0.28s) var(--popout-ease, cubic-bezier(0.4, 0, 0.2, 1)) + forwards; +} +.netease-mini-player.minimized.popping-left { + animation: player-popout-left var(--popout-duration, 0.28s) var(--popout-ease, cubic-bezier(0.4, 0, 0.2, 1)) + forwards; +} + +@keyframes player-dock-right { + from { + transform: translateX(0); + } + to { + transform: translateX(calc(50% + var(--dock-gap, 20px))); + } +} +@keyframes player-dock-left { + from { + transform: translateX(0); + } + to { + transform: translateX(calc(-50% - var(--dock-gap, 20px))); + } +} +@keyframes player-popout-right { + from { + transform: translateX(calc(50% + var(--dock-gap, 20px))); + } + to { + transform: translateX(0); + } +} +@keyframes player-popout-left { + from { + transform: translateX(calc(-50% - var(--dock-gap, 20px))); + } + to { + transform: translateX(0); + } +} + +.netease-mini-player .feature-btn, +.netease-mini-player .control-btn { + font-size: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.netease-mini-player .bottom-controls .feature-btn, +.netease-mini-player .volume-icon { + color: var(--text-primary); +} + +.netease-mini-player .control-btn svg { + width: 18px; + height: 18px; + display: block; + fill: currentColor; +} + +.netease-mini-player .play-btn svg { + width: 20px; + height: 20px; +} + +.netease-mini-player .play-icon { + margin-left: 0; +} + +.netease-mini-player .bottom-controls .feature-btn svg, +.netease-mini-player .volume-icon svg { + width: 16px; + height: 16px; + display: block; + fill: currentColor; +} + +.netease-mini-player .volume-icon { + display: flex; + align-items: center; + justify-content: center; + width: 14px; + height: 14px; + font-size: 0; +} + +.netease-mini-player .feature-btn { + width: auto; + height: auto; +} + +.netease-mini-player .bottom-controls { + gap: 6px; +} + +.netease-mini-player .volume-icon svg { + width: 18px; + height: 18px; +} + +.netease-mini-player.minimized .minimize-btn { + font-size: 0; +} diff --git a/public/js/netease-mini-player-v2.js b/public/js/netease-mini-player-v2.js index c4efbf3..0b672d3 100644 --- a/public/js/netease-mini-player-v2.js +++ b/public/js/netease-mini-player-v2.js @@ -1,22 +1,29 @@ /** * [NMPv2] NeteaseMiniPlayer v2 JavaScript * Lightweight Player Component Based on NetEase Cloud Music API - * + * * Copyright 2025 BHCN STUDIO & 北海的佰川(ImBHCN[numakkiyu]) - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -(()=>{try{const s=document.currentScript;if(s&&s.src){fetch(s.src,{mode:'cors',credentials:'omit'}).catch(()=>{});}}catch(e){}})(); +(() => { + try { + const s = document.currentScript; + if (s && s.src) { + fetch(s.src, { mode: "cors", credentials: "omit" }).catch(() => {}); + } + } catch (e) {} +})(); const GlobalAudioManager = { currentPlayer: null, setCurrent(player) { @@ -24,7 +31,7 @@ const GlobalAudioManager = { this.currentPlayer.pause(); } this.currentPlayer = player; - } + }, }; const ICONS = { @@ -35,11 +42,11 @@ const ICONS = { volume: ``, lyrics: ``, list: ``, - minimize: ``, - maximize: ``, + minimize: ``, + maximize: ``, loopList: ``, - loopSingle: ``, - shuffle: `` + loopSingle: ``, + shuffle: ``, }; class NeteaseMiniPlayer { constructor(element) { @@ -60,7 +67,7 @@ class NeteaseMiniPlayer { this.showLyrics = this.config.lyric; this.cache = new Map(); this.init(); - this.playMode = 'list'; + this.playMode = "list"; this.shuffleHistory = []; this.idleTimeout = null; this.idleDelay = 5000; @@ -68,40 +75,52 @@ class NeteaseMiniPlayer { } parseConfig() { const element = this.element; - const position = element.dataset.position || 'static'; - const validPositions = ['static', 'top-left', 'top-right', 'bottom-left', 'bottom-right']; - const finalPosition = validPositions.includes(position) ? position : 'static'; - const defaultMinimized = element.dataset.defaultMinimized === 'true'; - - const embedValue = element.getAttribute('data-embed') || element.dataset.embed; - const isEmbed = embedValue === 'true' || embedValue === true; + const position = element.dataset.position || "static"; + const validPositions = ["static", "top-left", "top-right", "bottom-left", "bottom-right"]; + const finalPosition = validPositions.includes(position) ? position : "static"; + const defaultMinimized = element.dataset.defaultMinimized === "true"; - const autoPauseAttr = element.getAttribute('data-auto-pause') ?? element.dataset.autoPause; - const autoPauseDisabled = autoPauseAttr === 'true' || autoPauseAttr === true; + const embedValue = element.getAttribute("data-embed") || element.dataset.embed; + const isEmbed = embedValue === "true" || embedValue === true; + + const autoPauseAttr = element.getAttribute("data-auto-pause") ?? element.dataset.autoPause; + const autoPauseDisabled = autoPauseAttr === "true" || autoPauseAttr === true; + + const apiUrls = JSON.parse(element.dataset.apiUrls) || apiUrls === []; + + // 读取 autoPause 配置 + let autoPause = true; // 默认启用自动暂停 + if (window.__NETEASE_MUSIC_CONFIG__?.autoPause !== undefined) { + autoPause = window.__NETEASE_MUSIC_CONFIG__.autoPause; + } else if (element.dataset.autoPause !== undefined) { + autoPause = element.dataset.autoPause !== "false"; + } return { embed: isEmbed, - autoplay: element.dataset.autoplay === 'true', + autoplay: element.dataset.autoplay === "true", playlistId: element.dataset.playlistId, songId: element.dataset.songId, position: finalPosition, - lyric: element.dataset.lyric !== 'false', - theme: element.dataset.theme || 'auto', - size: element.dataset.size || 'compact', + lyric: element.dataset.lyric !== "false", + theme: element.dataset.theme || "auto", + size: element.dataset.size || "compact", defaultMinimized: defaultMinimized, - autoPauseDisabled: autoPauseDisabled + autoPauseDisabled: autoPauseDisabled, + autoPause: autoPause, + apiUrls: apiUrls, }; } async init() { if (this.config.embed) { - this.element.setAttribute('data-embed', 'true'); + this.element.setAttribute("data-embed", "true"); } - this.element.setAttribute('data-position', this.config.position); - + this.element.setAttribute("data-position", this.config.position); + if (this.config.embed) { - this.element.classList.add('netease-mini-player-embed'); + this.element.classList.add("netease-mini-player-embed"); } - + this.initTheme(); this.createPlayerHTML(); this.applyResponsiveControls?.(); @@ -129,12 +148,12 @@ class NeteaseMiniPlayer { this.play(); } } - if (this.config.defaultMinimized && !this.config.embed && this.config.position !== 'static') { + if (this.config.defaultMinimized && !this.config.embed && this.config.position !== "static") { this.toggleMinimize(); } } catch (error) { - console.error('播放器初始化失败:', error); - this.showError('加载失败,请稍后重试'); + console.error("播放器初始化失败:", error); + this.showError("加载失败,请稍后重试"); } } createPlayerHTML() { @@ -157,12 +176,20 @@ class NeteaseMiniPlayer {语言使用统计 · Languages
名称:${ensureValue(name)}
+邮箱:${ensureValue(email)}
+ +描述:${ensureValue(desc)}
+头像:${ensureValue(avatar)}
+时间:${new Date().toISOString()}
+ `; + + const info = await transporter.sendMail({ + from: senderEmail, + to: adminEmail, + replyTo: email, + subject: `友链申请 / 联系表单 · ${ensureValue(name)}`, + html: htmlMessage, + }); + + return { + message: "Mail sent", + id: info.messageId, + }; +}); diff --git a/src/server/api/wakatime.ts b/src/server/api/wakatime.ts new file mode 100644 index 0000000..9c97922 --- /dev/null +++ b/src/server/api/wakatime.ts @@ -0,0 +1,56 @@ +import { defineEventHandler, getQuery, createError } from "h3"; +import { useRuntimeConfig } from "#imports"; + +export default defineEventHandler(async (event) => { + const res = event.node.res; + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS"); + res.setHeader("Access-Control-Allow-Headers", "Content-Type"); + + if (event.node.req.method === "OPTIONS") { + res.statusCode = 200; + return "ok"; + } + + if (event.node.req.method !== "GET") { + throw createError({ statusCode: 405, statusMessage: "Method Not Allowed" }); + } + + const config = useRuntimeConfig(); + const apiKey = config.wakatimeApiKey; + if (typeof apiKey !== "string") { + throw createError({ statusCode: 500, statusMessage: "Invalid WakaTime API Key configuration" }); + } + + const query = getQuery(event); + const apiUrl = (query.apiUrl as string) || config.wakatimeApiUrl; + + const headers = { + Authorization: `Basic ${Buffer.from(apiKey).toString("base64")}`, + }; + + try { + const [weeklyStatsResponse, allTimeStatsResponse, statusResponse] = await Promise.all([ + fetch(`${apiUrl}/users/current/stats/last_7_days`, { headers }), + fetch(`${apiUrl}/users/current/stats/all_time`, { headers }), + fetch(`${apiUrl}/users/current/status`, { headers }), + ]); + + if (!weeklyStatsResponse.ok) { + throw new Error(`Wakatime API error: ${weeklyStatsResponse.status}`); + } + + const weeklyStatsData = await weeklyStatsResponse.json(); + const allTimeStatsData = allTimeStatsResponse.ok ? await allTimeStatsResponse.json() : null; + const statusData = statusResponse.ok ? await statusResponse.json() : null; + + return { + weekly: weeklyStatsData.data, + allTime: allTimeStatsData ? allTimeStatsData.data : null, + status: statusData, + }; + } catch (error) { + console.error("Wakatime API error:", error); + throw createError({ statusCode: 500, statusMessage: "Failed to fetch Wakatime data" }); + } +}); diff --git a/src/styles.css b/src/styles.css index 8013756..b5e89d4 100644 --- a/src/styles.css +++ b/src/styles.css @@ -15,6 +15,42 @@ body { background: radial-gradient(circle at 20% 20%, #1b2b4b, #0f1629); } +/* Layout shell keeps background and toggle visible even when content is hidden */ + +.app-shell { + position: relative; + min-height: 100vh; + color: #e8eefc; + overflow: hidden; + display: flex; + flex-direction: column; + isolation: isolate; +} + +.background-overlay { + position: fixed; + inset: 0; + pointer-events: none; + z-index: -1; + background-repeat: no-repeat; +} + +.content-stack { + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + min-height: 100vh; +} + +.app-body { + position: relative; + z-index: 1; + flex: 1; + display: flex; + flex-direction: column; +} + a { color: #7cc1ff; text-decoration: none; @@ -106,7 +142,7 @@ p { /* do not force width here — let the player's own minimized/expanded styles control sizing. Only constrain max width as a safety net. */ max-width: calc(100% - 40px) !important; - z-index: 9999 !important; + z-index: 10001 !important; margin: 0 !important; transform: none !important; } @@ -127,7 +163,7 @@ p { bottom: 20px !important; left: 20px !important; right: auto !important; - z-index: 9999 !important; + z-index: 10001 !important; } /* Fix: prevent playlist dropdown from increasing document height @@ -141,7 +177,7 @@ p { width: 290px !important; max-height: 50vh !important; overflow: auto !important; - z-index: 10001 !important; + z-index: 10002 !important; } .netease-mini-player[data-position="top-left"] .playlist-container, @@ -153,7 +189,7 @@ p { width: 290px !important; max-height: 50vh !important; overflow: auto !important; - z-index: 10001 !important; + z-index: 10002 !important; } /* If player is docked to the right, align playlist to right edge */ @@ -189,3 +225,65 @@ p { object-fit: cover !important; border-radius: 50% !important; } + +/* Floating background show/hide toggle (bottom-right) */ +.background-toggle { + position: fixed; + right: 18px; + bottom: 18px; + z-index: 10000; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 10px 14px; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.25); + background: rgba(255, 255, 255, 0.14); + backdrop-filter: blur(10px); + color: #f7fbff; + font-weight: 600; + cursor: pointer; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.28); + transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.18s ease, border-color 0.18s ease; +} + +.background-toggle:hover, +.background-toggle:focus-visible { + background: rgba(124, 193, 255, 0.25); + border-color: rgba(124, 193, 255, 0.65); + box-shadow: 0 14px 36px rgba(124, 193, 255, 0.28); + outline: none; +} + +.background-toggle:active { + transform: translateY(1px) scale(0.99); +} + +.background-toggle .toggle-icon { + font-size: 18px; + line-height: 1; +} + +.background-toggle .toggle-label { + font-size: 14px; + letter-spacing: 0.2px; +} + +.background-toggle.active { + background: linear-gradient(135deg, rgba(124, 193, 255, 0.4), rgba(255, 255, 255, 0.2)); + border-color: rgba(124, 193, 255, 0.8); + color: #0f1629; + box-shadow: 0 16px 42px rgba(124, 193, 255, 0.32); +} + +@media (max-width: 640px) { + .background-toggle { + right: 12px; + bottom: 12px; + padding: 9px 12px; + gap: 5px; + } + .background-toggle .toggle-label { + font-size: 13px; + } +} diff --git a/src/views/About.vue b/src/views/About.vue deleted file mode 100644 index bcd0747..0000000 --- a/src/views/About.vue +++ /dev/null @@ -1,64 +0,0 @@ - -