feat:codeformat

This commit is contained in:
sqj
2025-08-13 22:39:11 +08:00
parent 6a6adeb212
commit 81c0d84fc3
11 changed files with 128 additions and 86 deletions

View File

@@ -3,9 +3,11 @@
一个跨平台的音乐播放器应用,支持多来源音乐数据获取与播放。
## 项目简介
Ceru Music 是基于 Electron 和 Vue 开发的跨平台桌面音乐播放器,支持从多个音乐平台获取歌曲信息并播放。该项目结合了现代前端技术和桌面应用开发,提供了流畅的用户体验和灵活的音乐数据源支持。
## 技术栈
- **Electron**:用于构建跨平台桌面应用
- **Vue 3**:前端框架,提供响应式 UI
- **TypeScript**:增强代码可维护性和类型安全
@@ -14,6 +16,7 @@ Ceru Music 是基于 Electron 和 Vue 开发的跨平台桌面音乐播放器,
- **Meting API**:作为备用音乐数据源
## 主要功能
- 支持从多个音乐平台搜索和播放歌曲
- 获取歌词和专辑信息
- 支持虚拟滚动列表,优化大量数据渲染性能
@@ -23,17 +26,21 @@ Ceru Music 是基于 Electron 和 Vue 开发的跨平台桌面音乐播放器,
## 安装与使用
### 推荐开发环境
- **IDE**: VS Code 或 WebStorm
- **Node.js 版本**: 推荐使用最新稳定版
- **包管理器**: pnpm
### 项目设置
1. 安装依赖:
```bash
pnpm install
```
2. 启动开发服务器:
```bash
pnpm dev
```
@@ -44,12 +51,15 @@ Ceru Music 是基于 Electron 和 Vue 开发的跨平台桌面音乐播放器,
```
### 平台构建指令
- **Windows**:
```bash
pnpm build:win
```
- **macOS**:
```bash
pnpm build:mac
```
@@ -60,17 +70,22 @@ Ceru Music 是基于 Electron 和 Vue 开发的跨平台桌面音乐播放器,
```
## 文档与资源
- [API 接口文档](docs/api.md):详细说明了支持的音乐平台和请求格式。
- [产品设计文档](docs/design.md):涵盖项目架构、核心功能设计和开发规范。
## 开源许可
本项目遵循 MIT 许可协议。详情请参阅 [LICENSE](LICENSE) 文件。
## 贡献指南
欢迎贡献代码和反馈建议!请遵循 [Git 提交规范](docs/design.md#git提交规范) 并确保代码符合项目风格指南。
## 更新日志
请参阅 [更新日志](docs/api.md#更新日志) 了解最新功能和改进。
## 联系方式
如有问题或合作意向,请通过 Gitee 私信联系项目维护者。
如有问题或合作意向,请通过 Gitee 私信联系项目维护者。

View File

@@ -13,35 +13,35 @@
## 请求参数
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| server | string | 否 | netease | 音乐平台 |
| type | string | 否 | search | 请求类型 |
| id | string | 否 | hello | 查询ID或关键词 |
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
| ------ | ------ | ---- | ------- | -------------- |
| server | string | 否 | netease | 音乐平台 |
| type | string | 否 | search | 请求类型 |
| id | string | 否 | hello | 查询ID或关键词 |
### 支持的音乐平台 (server)
| 平台代码 | 平台名称 |
|----------|----------|
| netease | 网易云音乐 |
| tencent | QQ音乐 |
| baidu | 百度音乐 |
| xiami | 虾米音乐 |
| kugou | 酷狗音乐 |
| kuwo | 酷我音乐 |
| 平台代码 | 平台名称 |
| -------- | ---------- |
| netease | 网易云音乐 |
| tencent | QQ音乐 |
| baidu | 百度音乐 |
| xiami | 虾米音乐 |
| kugou | 酷狗音乐 |
| kuwo | 酷我音乐 |
### 支持的请求类型 (type)
| 类型 | 说明 | id参数说明 |
|------|------|------------|
| search | 搜索歌曲 | 搜索关键词 |
| song | 获取歌曲详情 | 歌曲ID |
| album | 获取专辑信息 | 专辑ID |
| artist | 获取歌手信息 | 歌手ID |
| playlist | 获取歌单信息 | 歌单ID |
| lrc | 获取歌词 | 歌曲ID |
| url | 获取播放链接 | 歌曲ID |
| pic | 获取封面图片 | 歌曲/专辑/歌手ID |
| 类型 | 说明 | id参数说明 |
| -------- | ------------ | ---------------- |
| search | 搜索歌曲 | 搜索关键词 |
| song | 获取歌曲详情 | 歌曲ID |
| album | 获取专辑信息 | 专辑ID |
| artist | 获取歌手信息 | 歌手ID |
| playlist | 获取歌单信息 | 歌单ID |
| lrc | 获取歌词 | 歌曲ID |
| url | 获取播放链接 | 歌曲ID |
| pic | 获取封面图片 | 歌曲/专辑/歌手ID |
## 响应格式
@@ -74,6 +74,7 @@ GET /?server=netease&type=search&id=周杰伦
```
**响应示例**:
```json
{
"success": true,
@@ -104,6 +105,7 @@ GET /?server=netease&type=lrc&id=186016
```
**响应示例**:
```json
{
"success": true,
@@ -120,6 +122,7 @@ GET /?server=netease&type=url&id=186016
```
**响应示例**:
```json
{
"success": true,
@@ -160,11 +163,11 @@ GET /?server=netease&type=pic&id=186016
## 错误码说明
| 错误信息 | 说明 |
|----------|------|
| require id. | 缺少必需的id参数 |
| 错误信息 | 说明 |
| ------------------- | ---------------- |
| require id. | 缺少必需的id参数 |
| unsupported server. | 不支持的音乐平台 |
| unsupported type. | 不支持的请求类型 |
| unsupported type. | 不支持的请求类型 |
## 注意事项
@@ -184,10 +187,11 @@ GET /?server=netease&type=pic&id=186016
## 技术实现
本API基于以下技术栈
- **PHP**: 后端语言
- **Meting**: 音乐数据获取库
- **Composer**: 依赖管理
## 更新日志
- **v1.0.0**: 初始版本,支持基础的音乐数据获取功能
- **v1.0.0**: 初始版本,支持基础的音乐数据获取功能

View File

@@ -7,6 +7,7 @@ Ceru Music 是一个基于 Electron + Vue 3 的跨平台桌面音乐播放器,
## 项目架构
### 技术栈
- **前端框架**: Vue 3 + TypeScript + Composition API
- **桌面框架**: Electron (v37.2.3)
- **UI组件库**: TDesign Vue Next (v1.15.2)
@@ -18,17 +19,17 @@ Ceru Music 是一个基于 Electron + Vue 3 的跨平台桌面音乐播放器,
- **Node pnpm 版本**
```bash
PS D:\code\Ceru-Music> node -v
PS D:\code\Ceru-Music> node -v
v22.17.0
PS D:\code\Ceru-Music> pnpm -v
10.14.0
```
-
### 架构设计
```
```asp
Ceru Music
├── 主进程 (Main Process)
│ ├── 应用生命周期管理
@@ -45,6 +46,7 @@ Ceru Music
```
### 目录结构
```
src/
├── main/ # 主进程代码
@@ -67,6 +69,7 @@ src/
## 项目开发使用方式
### 开发环境启动
```bash
# 安装依赖
pnpm install
@@ -82,6 +85,7 @@ pnpm typecheck
```
### 构建打包
```bash
# 构建当前平台
pnpm build
@@ -101,18 +105,21 @@ pnpm build:linux
### 接口1: 网易云音乐原生接口 (主要数据源)
#### 获取音乐信息
- **请求地址**: `https://music.163.com/api/song/detail`
- **请求参数**: `ids=[ID1,ID2,ID3,...]` 音乐ID列表
- **示例**: `https://music.163.com/api/song/detail?ids=[36270426]`
#### 获取音乐直链
- **请求地址**: `https://music.163.com/song/media/outer/url`
- **请求参数**: `id=123` 音乐ID
- **示例**: `https://music.163.com/song/media/outer/url?id=36270426.mp3`
#### 获取歌词
- **请求地址**: `https://music.163.com/api/song/lyric`
- **请求参数**:
- **请求参数**:
- `id=123` 音乐ID
- `lv=-1` 获取歌词
- `yv=-1` 获取逐字歌词
@@ -120,6 +127,7 @@ pnpm build:linux
- **示例**: `https://music.163.com/api/song/lyric?id=36270426&lv=-1&yv=-1&tv=-1`
#### 搜索歌曲
- **请求地址**: `https://music.163.com/api/search/get/web`
- **请求参数**:
- `s` 歌名
@@ -131,6 +139,7 @@ pnpm build:linux
### 接口2: Meting API (备用数据源)
#### 参数说明
- **server**: 数据源
- `netease` 网易云音乐(默认)
- `tencent` QQ音乐
@@ -145,6 +154,7 @@ pnpm build:linux
- **id**: 类型ID封面ID/单曲ID/歌单ID
#### 使用示例
```
https://api.qijieya.cn/meting/?type=url&id=1969519579
https://api.qijieya.cn/meting/?type=song&id=591321
@@ -152,10 +162,12 @@ https://api.qijieya.cn/meting/?type=playlist&id=2619366284
```
### 接口3: 备选接口
- **地址**: https://doc.vkeys.cn/api-doc/
- **说明**: 不建议使用,延迟较高
### 接口4: 自部署接口 (备用)
- **地址**: `https://music.shiqianjiang.cn?id=你是我的风景&server=netease`
- **说明**: 不支持分页,用于获取歌曲源、歌词源等
- **文档**: [API文档](./api.md)
@@ -223,7 +235,7 @@ export const useMusicStore = defineStore('music', {
currentTime: 0,
duration: 0
}),
actions: {
// 播放歌曲
async playSong(song: Song) {
@@ -231,23 +243,26 @@ export const useMusicStore = defineStore('music', {
this.isPlaying = true
this.saveToStorage()
},
// 添加到播放列表
addToPlaylist(songs: Song[]) {
this.playlist.push(...songs)
this.saveToStorage()
},
// 保存到本地存储
saveToStorage() {
localStorage.setItem('music-state', JSON.stringify({
currentSong: this.currentSong,
playlist: this.playlist,
playMode: this.playMode,
volume: this.volume
}))
localStorage.setItem(
'music-state',
JSON.stringify({
currentSong: this.currentSong,
playlist: this.playlist,
playMode: this.playMode,
volume: this.volume
})
)
},
// 从本地存储恢复
loadFromStorage() {
const saved = localStorage.getItem('music-state')
@@ -266,12 +281,7 @@ export const useMusicStore = defineStore('music', {
```vue
<template>
<t-virtual-scroll
:data="songList"
:height="600"
:item-height="60"
:buffer="10"
>
<t-virtual-scroll :data="songList" :height="600" :item-height="60" :buffer="10">
<template #default="{ data: song, index }">
<div class="song-item" @click="playSong(song)">
<div class="song-cover">
@@ -291,15 +301,16 @@ export const useMusicStore = defineStore('music', {
### 本地数据存储设计
#### 播放列表存储
```typescript
// 方案1: LocalStorage (简单方案)
class PlaylistStorage {
private key = 'ceru-playlists'
save(playlists: Playlist[]) {
localStorage.setItem(this.key, JSON.stringify(playlists))
}
load(): Playlist[] {
const data = localStorage.getItem(this.key)
return data ? JSON.parse(data) : []
@@ -309,11 +320,11 @@ class PlaylistStorage {
// 方案2: Node.js 文件存储 (最优方案,支持分享)
class FileStorage {
private filePath = path.join(app.getPath('userData'), 'playlists.json')
async save(playlists: Playlist[]) {
await fs.writeFile(this.filePath, JSON.stringify(playlists, null, 2))
}
async load(): Promise<Playlist[]> {
try {
const data = await fs.readFile(this.filePath, 'utf-8')
@@ -322,12 +333,12 @@ class FileStorage {
return []
}
}
// 导出播放列表
async export(playlist: Playlist, exportPath: string) {
await fs.writeFile(exportPath, JSON.stringify(playlist, null, 2))
}
// 导入播放列表
async import(importPath: string): Promise<Playlist> {
const data = await fs.readFile(importPath, 'utf-8')
@@ -353,12 +364,12 @@ export const useAppStore = defineStore('app', {
autoPlay: false
}
}),
actions: {
checkFirstLaunch() {
const hasLaunched = localStorage.getItem('has-launched')
this.isFirstLaunch = !hasLaunched
if (this.isFirstLaunch) {
// 跳转到欢迎页面
router.push('/welcome')
@@ -368,16 +379,16 @@ export const useAppStore = defineStore('app', {
router.push('/home')
}
},
completeWelcome(preferences?: Partial<UserPreferences>) {
if (preferences) {
Object.assign(this.userPreferences, preferences)
}
this.hasCompletedWelcome = true
localStorage.setItem('has-launched', 'true')
localStorage.setItem('user-preferences', JSON.stringify(this.userPreferences))
router.push('/home')
}
}
@@ -396,7 +407,7 @@ export const useAppStore = defineStore('app', {
<t-step title="基础设置" content="配置您的偏好设置" />
<t-step title="完成设置" content="开始您的音乐之旅" />
</t-steps>
<transition name="slide" mode="out-in">
<component :is="currentStepComponent" @next="nextStep" @skip="skipWelcome" />
</transition>
@@ -428,7 +439,8 @@ function skipWelcome() {
</script>
<style scoped>
.slide-enter-active, .slide-leave-active {
.slide-enter-active,
.slide-leave-active {
transition: all 0.3s ease;
}
.slide-enter-from {
@@ -449,6 +461,7 @@ function skipWelcome() {
## 页面动画设计
### 路由过渡动画
```vue
<template>
<router-view v-slot="{ Component, route }">
@@ -468,8 +481,10 @@ function getTransitionName(route: any) {
<style>
/* 滑动动画 */
.slide-left-enter-active, .slide-left-leave-active,
.slide-right-enter-active, .slide-right-leave-active {
.slide-left-enter-active,
.slide-left-leave-active,
.slide-right-enter-active,
.slide-right-leave-active {
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
@@ -492,10 +507,12 @@ function getTransitionName(route: any) {
}
/* 淡入淡出动画 */
.fade-enter-active, .fade-leave-active {
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from, .fade-leave-to {
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
@@ -504,6 +521,7 @@ function getTransitionName(route: any) {
## 核心组件设计
### 音乐播放器组件
```vue
<template>
<div class="music-player">
@@ -514,7 +532,7 @@ function getTransitionName(route: any) {
<div class="song-artist">{{ currentSong?.artist }}</div>
</div>
</div>
<div class="player-controls">
<t-button variant="text" @click="previousSong">
<t-icon name="skip-previous" />
@@ -526,15 +544,10 @@ function getTransitionName(route: any) {
<t-icon name="skip-next" />
</t-button>
</div>
<div class="player-progress">
<span class="time-current">{{ formatTime(currentTime) }}</span>
<t-slider
v-model="progress"
:max="duration"
@change="seekTo"
class="progress-slider"
/>
<t-slider v-model="progress" :max="duration" @change="seekTo" class="progress-slider" />
<span class="time-duration">{{ formatTime(duration) }}</span>
</div>
</div>
@@ -544,6 +557,7 @@ function getTransitionName(route: any) {
## 开发规范
### 代码规范
- 使用 TypeScript 进行类型检查
- 遵循 ESLint 配置的代码规范
- 使用 Prettier 进行代码格式化
@@ -551,6 +565,7 @@ function getTransitionName(route: any) {
- 文件命名使用 kebab-case
### Git 提交规范
```
feat: 新功能
fix: 修复bug
@@ -562,6 +577,7 @@ chore: 构建过程或辅助工具的变动
```
### 性能优化
- 使用虚拟滚动处理大列表
- 图片懒加载
- 组件按需加载
@@ -581,4 +597,4 @@ chore: 构建过程或辅助工具的变动
---
*本设计文档将随着项目开发进度持续更新和完善。*
_本设计文档将随着项目开发进度持续更新和完善。_

View File

@@ -3,11 +3,12 @@
"version": "1.0.0",
"description": "An Electron application with Vue and TypeScript",
"main": "./out/main/index.js",
"author": "example.com",
"author": "sqj,wldss,star",
"license": "MIT",
"homepage": "https://electron-vite.org",
"scripts": {
"format": "prettier --write .",
"lint": "eslint --cache .",
"lint": "eslint --cache . --fix",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false",
"typecheck": "npm run typecheck:node && npm run typecheck:web",

BIN
resources/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

10
resources/logo.svg Normal file
View File

@@ -0,0 +1,10 @@
<svg width="1200" height="1200" viewBox="0 0 1200 1200" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="1200" height="1200" rx="379" fill="white"/>
<path d="M957.362 204.197C728.535 260.695 763.039 192.264 634.41 175.368C451.817 151.501 504.125 315.925 504.125 315.925L630.545 673.497C591.211 654.805 544.287 643.928 494.188 643.928C353.275 643.928 239 729.467 239 834.964C239 940.567 353.137 1026 494.188 1026C635.1 1026 749.375 940.461 749.375 834.964C749.375 832.218 749.237 829.473 749.099 826.727C749.513 825.988 749.789 825.143 750.065 824.087C757.932 789.449 634.272 348.345 634.272 348.345C634.272 348.345 764.971 401.886 860.89 351.936C971.163 294.699 964.953 202.402 957.362 204.197Z" fill="url(#paint0_linear_4_16)" stroke="#29293A" stroke-opacity="0.23"/>
<defs>
<linearGradient id="paint0_linear_4_16" x1="678.412" y1="-1151.29" x2="796.511" y2="832.071" gradientUnits="userSpaceOnUse">
<stop offset="0.572115" stop-color="#B8F1ED"/>
<stop offset="0.9999" stop-color="#B8F1CC"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,7 +1,8 @@
import { app, shell, BrowserWindow, ipcMain } from 'electron'
import { join } from 'path'
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset'
import icon from '../../resources/logo.png?asset'
import path from 'node:path'
function createWindow(): void {
// Create the browser window.
@@ -11,6 +12,7 @@ function createWindow(): void {
show: false,
autoHideMenuBar: true,
...(process.platform === 'linux' ? { icon } : {}),
icon: path.join(__dirname, '../../resources/logo.png'),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,

View File

@@ -1,6 +1,6 @@
<!doctype html>
<html>
<head>
<head lang="zh-CN">
<meta charset="UTF-8" />
<title>Electron</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->

View File

@@ -1,10 +1,7 @@
<script setup lang="ts">
import Versions from './components/Versions.vue'
// const ipcHandle = (): void => window.electron.ipcRenderer.send('ping')
</script>
<template>
<router-view />
<!-- <Versions />-->
</template>

View File

@@ -1,8 +1,6 @@
:root {
}
*,
*::before,
*::after {

View File

@@ -1,11 +1,10 @@
import { createWebHashHistory, createRouter } from 'vue-router'
const routes = [
{
path: '/',
name: 'welcome',
component: () => import('@renderer/views/welcome/index.vue'),
component: () => import('@renderer/views/welcome/index.vue')
}
]