feat(stores): 将轻量状态管理替换为Pinia并添加持久化功能

- 使用Pinia替代原有的reactive store实现状态管理
- 集成pinia-plugin-persistedstate插件实现设置和搜索历史的持久化
- 新增searchHistory store用于存储搜索历史记录
- 更新README文档中的状态管理相关说明
This commit is contained in:
Yuhang Wu 2026-08-13 18:02:04 +08:00
parent 6252188ee7
commit 094f5f8aec
66 changed files with 381 additions and 49 deletions

View File

@ -1,4 +1,3 @@
<<<<<<< HEAD
# YwYMusic 🎵
一款基于 **Tauri 2 + Vue 3** 的轻量在线音乐播放器。前端负责界面与播放,音乐数据、音频直链解析、扫码登录等能力由独立的音乐聚合后端提供。
@ -34,7 +33,7 @@
| 路由 | Vue RouterHash 模式,兼容 Tauri `file://` 协议) |
| 网络请求 | Axios统一拦截器 + 业务码处理) |
| 二维码 | `qrcode`(本地生成登录二维码) |
| 状态管理 | 轻量 `reactive` store无 Pinia 依赖 |
| 状态管理 | Pinia + pinia-plugin-persistedstate持久化设置与搜索历史 |
| 工程化 | pnpm workspace、unplugin-auto-import、unplugin-vue-components |
## 📁 项目结构
@ -46,13 +45,18 @@ YwYMusic
│ ├─ components/ # 通用组件PlayerBar、SongTable、MusicCard、QrLogin 等)
│ ├─ layouts/ # 主布局:侧边栏导航 + 内容区 + 底部播放条
│ ├─ router/ # 路由定义
│ ├─ stores/ # 轻量状态player播放器、settings音乐源设置
│ ├─ stores/ # Pinia 状态player播放器、settings音乐源设置、searchHistory搜索历史
│ ├─ utils/ # 请求封装、封面防盗链代理、时间/数量格式化
│ ├─ views/ # 页面:发现 / 排行榜 / 我的音乐 / 搜索 / 歌单详情 / 设置
│ ├─ mocks/ # 本地兜底数据(排行榜、新歌、我的音乐)
│ ├─ styles/ # 全局样式
│ └─ types/ # 全局类型与自动生成声明
├─ src-tauri/ # TauriRust壳与打包配置
│ ├─ src/
│ │ ├─ commands/ # Tauri 命令music_url 等)
│ │ ├─ services/ # HTTP 客户端(支持 GO_MUSIC_URL / METING_API_URL 切换)
│ │ └─ models.rs # 数据模型与 API 地址常量
│ └─ Cargo.toml
├─ index.html
└─ package.json
```
@ -92,6 +96,25 @@ pnpm tauri build
VITE_BASE_API_URL = "http://127.0.0.1:8080"
```
Rust 后端同时配置了两个 API 地址常量(`src-tauri/src/models.rs`
```rust
pub const GO_MUSIC_URL: &str = "http://127.0.0.1:8080"; // 音乐聚合后端
pub const METING_API_URL: &str = "http://127.0.0.1:81"; // Meting API
```
`HttpClient` 支持通过 `ApiEndpoint` 枚举在请求时选择目标地址:
```rust
use crate::services::http::{ApiEndpoint, HttpClient};
// 默认请求 GO_MUSIC_URL
state.get("/api/v1/playlist/recommend", &params).await?;
// 显式请求 METING_API_URL
state.get_at(ApiEndpoint::Meting, "/api", &params).await?;
```
请先启动后端服务,否则页面将回退到本地 mock 数据或提示网络异常。
## 🔌 后端接口概览
@ -107,6 +130,7 @@ VITE_BASE_API_URL = "http://127.0.0.1:8080"
| 专辑 | `/api/v1/album/detail` | 专辑详情 |
| 系统 | `/api/v1/system/qr_login/*` | 扫码登录会话与状态轮询 |
| 系统 | `/api/v1/system/cookies` | 平台 Cookies 读取 / 写入 |
| Meting | `/api` | Meting API通过 `music_url` 命令调用) |
> 多源参数使用重复键形式传递,例如 `sources=kugou&sources=netease`

BIN
app-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 916 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 B

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
src-tauri/icons/64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,19 @@
use crate::{
models::ApiResponse,
services::http::{ApiEndpoint, HttpClient},
};
use tauri::State;
use tauri_helper::auto_collect_command;
// 获取专辑详情
#[tauri::command]
#[auto_collect_command]
pub async fn album_detail(
http: State<'_, HttpClient>,
id: String,
source: String,
) -> Result<ApiResponse<serde_json::Value>, String> {
let params = [("id".to_string(), id), ("source".to_string(), source)];
let data = http.get_at(ApiEndpoint::GoMusic, "/api/v1/album/detail", &params).await.map_err(|e| e.to_string())?;
Ok(ApiResponse::success(data))
}

View File

@ -7,26 +7,185 @@ use crate::{
services::http::{ApiEndpoint, HttpClient},
};
// 代理请求并下载封面图
#[tauri::command]
#[auto_collect_command]
pub async fn music_cover(
http: State<'_, HttpClient>,
url: String,
artist: String,
) -> Result<ApiResponse<serde_json::Value>, String> {
let params: Vec<(String, String)> =
vec![("url".to_string(), url), ("artist".to_string(), artist)];
let data = http
.get_at(ApiEndpoint::GoMusic, "/api/v1/music/cover", &params)
.await
.map_err(|e| e.to_string());
Ok(ApiResponse::success(data?))
}
// 探测音频大小与码率
#[tauri::command]
#[auto_collect_command]
pub async fn music_inspect(
http: State<'_, HttpClient>,
id: String,
source: String,
duration: u32,
) -> Result<ApiResponse<serde_json::Value>, String> {
let params = vec![
("id".to_string(), id),
("source".to_string(), source),
("duration".to_string(), duration.to_string()),
];
let data = http
.get_at(ApiEndpoint::GoMusic, "/api/v1/music/inspect", &params)
.await
.map_err(|e| e.to_string());
Ok(ApiResponse::success(data?))
}
// 获取 JSON 格式歌词
#[tauri::command]
#[auto_collect_command]
pub async fn music_lyric(
http: State<'_, HttpClient>,
id: String,
source: String,
) -> Result<ApiResponse<serde_json::Value>, String> {
let params = vec![("id".to_string(), id), ("source".to_string(), source)];
let data = http
.get_at(ApiEndpoint::GoMusic, "/api/v1/music/lyric", &params)
.await
.map_err(|e| e.to_string());
Ok(ApiResponse::success(data?))
}
// 下载 LRC 歌词文件
#[tauri::command]
#[auto_collect_command]
pub async fn music_lrc_file(
http: State<'_, HttpClient>,
id: String,
source: String,
name: String,
artist: String,
) -> Result<ApiResponse<serde_json::Value>, String> {
let params = vec![
("id".to_string(), id),
("source".to_string(), source),
("name".to_string(), name),
("artist".to_string(), artist),
];
let data = http
.get_at(ApiEndpoint::GoMusic, "/api/v1/music/lyric/file", &params)
.await
.map_err(|e| e.to_string());
Ok(ApiResponse::success(data?))
}
// 综合搜索与链接解析
#[tauri::command]
#[auto_collect_command]
pub async fn music_search(
http: State<'_, HttpClient>,
keyword: String,
type_: String,
sources: Vec<String>,
) -> Result<ApiResponse<serde_json::Value>, String> {
let mut params = vec![("q".to_string(), keyword), ("type".to_string(), type_)];
// 追加动态 sources 参数
for source in sources {
params.push(("source".to_string(), source));
}
let data = http
.get_at(ApiEndpoint::GoMusic, "/api/v1/music/search", &params)
.await
.map_err(|e| e.to_string());
Ok(ApiResponse::success(data?))
}
// 串流代理与下载音频
#[tauri::command]
#[auto_collect_command]
pub async fn music_stream(
http: State<'_, HttpClient>,
id: String,
source: String,
name: String,
artist: String,
) -> Result<ApiResponse<serde_json::Value>, String> {
let params = vec![
("id".to_string(), id),
("source".to_string(), source),
("name".to_string(), name),
("artist".to_string(), artist),
];
let data = http
.get_at(ApiEndpoint::GoMusic, "/api/v1/music/stream", &params)
.await
.map_err(|e| e.to_string());
Ok(ApiResponse::success(data?))
}
// 智能切换可用的平替音源
pub async fn music_switch(
http: State<'_, HttpClient>,
name: String,
artist: String,
source: String,
target: String,
duration: u32,
) -> Result<ApiResponse<serde_json::Value>, String> {
let params = vec![
("name".to_string(), name),
("artist".to_string(), artist),
("source".to_string(), source),
("target".to_string(), target),
("duration".to_string(), duration.to_string()),
];
let data = http
.get_at(ApiEndpoint::GoMusic, "/api/v1/music/switch", &params)
.await
.map_err(|e| e.to_string());
Ok(ApiResponse::success(data?))
}
// 获取音频裸直链
#[tauri::command]
#[auto_collect_command]
pub async fn music_url(
state: State<'_, HttpClient>,
http: State<'_, HttpClient>,
id: String,
server: String,
type_: String,
source: String,
) -> Result<ApiResponse<serde_json::Value>, String> {
let client = reqwest::Client::new();
let params = vec![
("id".to_string(), id),
("server".to_string(), server),
("type".to_string(), type_),
];
let resp = client
.get(format!("{}/api", METING_API_URL))
.query(&params)
.send()
let params = vec![("id".to_string(), id), ("source".to_string(), source)];
let data = http
.get_at(ApiEndpoint::GoMusic, "/api/v1/music/url", &params)
.await
.map_err(|e| e.to_string())?;
let data = resp.json().await.map_err(|e| e.to_string())?;
Ok(ApiResponse::success(data))
}
// #[tauri::command]
// #[auto_collect_command]
// pub async fn music_url(
// id: String,
// server: String,
// type_: String,
// ) -> Result<ApiResponse<serde_json::Value>, String> {
// let client = reqwest::Client::new();
// let params = vec![
// ("id".to_string(), id),
// ("server".to_string(), server),
// ("type".to_string(), type_),
// ];
// let resp = client
// .get(format!("{}/api", METING_API_URL))
// .query(&params)
// .send()
// .await
// .map_err(|e| e.to_string())?;
// let data = resp.json().await.map_err(|e| e.to_string())?;
// Ok(ApiResponse::success(data))
// }

View File

@ -3,6 +3,61 @@ use tauri_helper::auto_collect_command;
use crate::{models::ApiResponse, services::http::HttpClient};
#[tauri::command]
#[auto_collect_command]
pub async fn playlist_categories(
state: State<'_, HttpClient>,
sources: Vec<String>,
) -> Result<ApiResponse<serde_json::Value>, String> {
let params: Vec<(String, String)> = sources
.iter()
.map(|source| ("sources".to_string(), source.clone()))
.collect();
let data = state
.get("/api/v1/playlist/categories", &params)
.await
.map_err(|e| e.to_string());
Ok(ApiResponse::success(data?))
}
#[tauri::command]
#[auto_collect_command]
pub async fn playlist_category_list(
state: State<'_, HttpClient>,
category_id: String,
source: String,
page: u32,
limit: u32,
) -> Result<ApiResponse<serde_json::Value>, String> {
let params: Vec<(String, String)> = vec![
("category_id".to_string(), category_id),
("source".to_string(), source),
("page".to_string(), page.to_string()),
("limit".to_string(), limit.to_string()),
];
let data = state
.get("/api/v1/playlist/category", &params)
.await
.map_err(|e| e.to_string());
Ok(ApiResponse::success(data?))
}
#[tauri::command]
#[auto_collect_command]
pub async fn playlist_detail(
state: State<'_, HttpClient>,
id: String,
source: String,
) -> Result<ApiResponse<serde_json::Value>, String> {
let params: Vec<(String, String)> =
vec![("id".to_string(), id), ("source".to_string(), source)];
let data = state
.get("/api/v1/playlist/detail", &params)
.await
.map_err(|e| e.to_string());
Ok(ApiResponse::success(data?))
}
#[tauri::command]
#[auto_collect_command]
pub async fn playlist_recommand(
@ -21,3 +76,23 @@ pub async fn playlist_recommand(
Ok(ApiResponse::success(data?))
}
#[tauri::command]
#[auto_collect_command]
pub async fn playlist_user(
state: State<'_, HttpClient>,
source: String,
page: u32,
limit: u32,
) -> Result<ApiResponse<serde_json::Value>, String> {
let params: Vec<(String, String)> = vec![
("source".to_string(), source),
("page".to_string(), page.to_string()),
("limit".to_string(), limit.to_string()),
];
let data = state
.get("/api/v1/playlist/user", &params)
.await
.map_err(|e| e.to_string());
Ok(ApiResponse::success(data?))
}

View File

@ -1,5 +1,5 @@
import { get } from "@/utils/request"
import Source from "@/types/global"
import Source, { ApiResponse } from "@/types/global"
import { invoke } from "@tauri-apps/api/core"
const AlbumApi = {
/**
@ -10,7 +10,8 @@ const AlbumApi = {
* @returns
*/
detail: (id: string, source: Source) => {
return get<any>(`/api/v1/album/detail?id=${id}&source=${source}`)
// return get<any>(`/api/v1/album/detail?id=${id}&source=${source}`)
return invoke<ApiResponse<any>>("album_detail", { id, source })
},
}

View File

@ -1,4 +1,3 @@
import { get } from "@/utils/request"
import Source, { ApiResponse } from "@/types/global"
import { invoke } from "@tauri-apps/api/core"
@ -72,7 +71,8 @@ const MusicApi = {
* @returns
*/
cover: (url: string, name?: string, artist?: string) => {
return get<any>(`/api/v1/music/cover?url=${url}&name=${name}&artist=${artist}`)
// return get<any>(`/api/v1/music/cover?url=${url}&name=${name}&artist=${artist}`)
return invoke<ApiResponse<any>>("music_cover", { url, name, artist })
},
/**
*
@ -82,7 +82,8 @@ const MusicApi = {
* @param duration ()(kbps)
*/
inspect: (id: string, source: Source, duration?: string) => {
return get<any>(`/api/v1/music/inspect?id=${id}&source=${source}&duration=${duration}`)
// return get<any>(`/api/v1/music/inspect?id=${id}&source=${source}&duration=${duration}`)
return invoke<ApiResponse<any>>("music_inspect", { id, source, duration })
},
/**
* JSON
@ -92,7 +93,8 @@ const MusicApi = {
* @returns
*/
lyric: (id: string, source: Source) => {
return get<any>(`/api/v1/music/lyric?id=${id}&source=${source}`)
// return get<any>(`/api/v1/music/lyric?id=${id}&source=${source}`)
return invoke<ApiResponse<any>>("music_lyric", { id, source })
},
/**
* LRC
@ -104,7 +106,8 @@ const MusicApi = {
* @returns
*/
lyricFile: (id: string, source: Source, name?: string, artist?: string) => {
return get<any>(`/api/v1/music/lyric/file?id=${id}&source=${source}&name=${name}&artist=${artist}`)
// return get<any>(`/api/v1/music/lyric/file?id=${id}&source=${source}&name=${name}&artist=${artist}`)
return invoke<ApiResponse<any>>("music_lyric_file", { id, source, name, artist })
},
/**
*
@ -115,8 +118,9 @@ const MusicApi = {
* @returns
*/
search: (keyword: string, type: string, sources?: Source[]) => {
const sourceParams = (sources ?? []).map((s) => `&sources=${encodeURIComponent(s)}`).join("")
return get<MusicSearchResult>(`/api/v1/music/search?q=${encodeURIComponent(keyword)}&type=${type}${sourceParams}`)
// const sourceParams = (sources ?? []).map((s) => `&sources=${encodeURIComponent(s)}`).join("")
// return get<MusicSearchResult>(`/api/v1/music/search?q=${encodeURIComponent(keyword)}&type=${type}${sourceParams}`)
return invoke<ApiResponse<MusicSearchResult>>("music_search", { keyword, type, sources })
},
/**
*
@ -128,7 +132,8 @@ const MusicApi = {
* @returns
*/
stream: (id: string, source: Source, name?: string, artist?: string) => {
return get<any>(`/api/v1/music/stream?id=${id}&source=${source}&name=${name}&artist=${artist}`)
// return get<any>(`/api/v1/music/stream?id=${id}&source=${source}&name=${name}&artist=${artist}`)
return invoke<ApiResponse<any>>("music_stream", { id, source, name, artist })
},
/**
*
@ -141,7 +146,8 @@ const MusicApi = {
* @returns
*/
switch: (name: string, source: Source, artist?: string, target?: string, duration?: string) => {
return get<any>(`/api/v1/music/switch?name=${name}&source=${source}&artist=${artist}&target=${target}&duration=${duration}`)
// return get<any>(`/api/v1/music/switch?name=${name}&source=${source}&artist=${artist}&target=${target}&duration=${duration}`)
return invoke<ApiResponse<any>>("music_switch", { name, source, artist, target, duration })
},
/**
*
@ -151,7 +157,8 @@ const MusicApi = {
* @returns
*/
url: (id: string, source: Source) => {
return get<MusicUrl>(`/api/v1/music/url?id=${id}&source=${source}`)
// return get<MusicUrl>(`/api/v1/music/url?id=${id}&source=${source}`)
return invoke<ApiResponse<MusicUrl>>("music_url", { id, source })
},
musicUrl: (id: string, source: Source) => {

View File

@ -1,4 +1,3 @@
import { get } from "@/utils/request"
import Source, { type ApiResponse } from "@/types/global"
import { invoke } from "@tauri-apps/api/core"
@ -101,8 +100,9 @@ const PlayListApi = {
* @returns
*/
categoryList: (sources?: Source[]) => {
const sourceParams = (sources ?? []).map((s) => `sources=${encodeURIComponent(s)}`).join("&")
return get<PlayListCategoryRes[]>(`/api/v1/playlist/categories${sourceParams ? `?${sourceParams}` : ""}`)
// const sourceParams = (sources ?? []).map((s) => `sources=${encodeURIComponent(s)}`).join("&")
// return get<PlayListCategoryRes[]>(`/api/v1/playlist/categories${sourceParams ? `?${sourceParams}` : ""}`)
return invoke<ApiResponse<PlayListCategoryRes[]>>("playlist_categories", { sources })
},
/**
*
@ -114,7 +114,8 @@ const PlayListApi = {
* @returns
*/
categoryPlayList: (categoryId: string, source?: Source, page?: number, limit?: number) => {
return get<CategoryPlayListRes>(`/api/v1/playlist/category?source=${source}&category_id=${categoryId}&page=${page}&limit=${limit}`)
// return get<CategoryPlayListRes>(`/api/v1/playlist/category?source=${source}&category_id=${categoryId}&page=${page}&limit=${limit}`)
return invoke<ApiResponse<CategoryPlayListRes>>("playlist_category_list", { categoryId, source, page, limit })
},
/**
*
@ -123,7 +124,8 @@ const PlayListApi = {
* @returns
*/
detail: (source: Source, id: string) => {
return get<PlayListSong[]>(`/api/v1/playlist/detail?source=${source}&id=${id}`)
// return get<PlayListSong[]>(`/api/v1/playlist/detail?source=${source}&id=${id}`)
return invoke<ApiResponse<PlayListSong[]>>("playlist_detail", { source, id })
},
/**

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 916 KiB

BIN
src/assets/logo1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 KiB

View File

@ -2,8 +2,9 @@
<div class="app-shell">
<aside class="sidebar">
<div class="brand">
<span class="brand-icon"><AppIcon name="disc" :size="21" /></span>
<span class="brand-name">YwYMusic</span>
<!-- <span class="brand-icon"><AppIcon name="disc" :size="21" /></span> -->
<img class="brand-logo" src="/src/assets/logo.png" alt="YwYMusic logo" />
<!-- <span class="brand-name">YwYMusic</span> -->
</div>
<nav class="nav" aria-label="主导航">
@ -20,7 +21,19 @@
</RouterLink>
</nav>
<div class="sidebar-footer">v0.1.0 · 轻量在线播放</div>
<nav class="nav-settings" aria-label="设置">
<RouterLink
to="/settings"
title="设置"
class="nav-item"
:class="{ active: isActive('/settings') }"
>
<AppIcon name="settings" :size="18" />
<span>设置</span>
</RouterLink>
</nav>
<!-- <div class="sidebar-footer">v0.1.0 · 轻量在线播放</div> -->
</aside>
<div class="main-area">
@ -46,7 +59,6 @@ const navItems = [
{ path: "/", label: "歌单", icon: "home" },
{ path: "/ranking", label: "排行榜", icon: "trophy" },
{ path: "/library", label: "我的音乐", icon: "music" },
{ path: "/settings", label: "设置", icon: "settings" },
{ path: "/demo", label: "演示", icon: "play" },
] as const
@ -74,7 +86,7 @@ watch(
.sidebar {
display: flex;
flex-direction: column;
width: clamp(200px, 16vw, 232px);
width: clamp(168px, 13vw, 196px);
height: 100vh;
flex-shrink: 0;
padding: 20px 14px 14px;
@ -84,8 +96,9 @@ watch(
.brand {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
gap: 6px;
padding: 0 10px 18px;
}
@ -100,6 +113,14 @@ watch(
box-shadow: 0 6px 14px rgba(34, 197, 94, 0.35);
}
.brand-logo {
display: block;
width: 64px;
height: 64px;
object-fit: contain;
margin: 0 auto;
}
.brand-name {
font-size: 17px;
font-weight: 700;
@ -112,6 +133,15 @@ watch(
gap: 4px;
}
.nav-settings {
display: flex;
flex-direction: column;
gap: 4px;
margin-top: auto;
padding-top: 12px;
border-top: 1px solid var(--app-border);
}
.nav-item {
display: flex;
align-items: center;
@ -236,6 +266,11 @@ watch(
width: 100%;
}
.nav-settings {
width: 100%;
margin-top: auto;
}
.nav-item {
justify-content: center;
padding: 10px 0;

View File

@ -60,7 +60,7 @@ let loadSeq = 0
/** 获取歌单分类 */
const list = async () => {
const res = await PlayListApi.categoryList([settingsStore.defaultSource])
categories.value = res[0].categories
categories.value = res.data[0].categories
if (categories.value.length > 0) {
activeCategory.value = categories.value[1].id
playList(activeCategory.value)
@ -75,7 +75,7 @@ async function loadCategoryPlayList(categoryId: string, p: number) {
const res = await PlayListApi.categoryPlayList(categoryId, settingsStore.defaultSource, p, pageSize)
console.log(res)
if (seq !== loadSeq) return
categoryPlaylists.value = res.playlists
categoryPlaylists.value = res.data.playlists
const n = categoryPlaylists.value.length
if (n === pageSize) {
//

View File

@ -86,7 +86,7 @@ const playlistSource = computed<Source>(
onMounted(async () => {
try {
const res = await PlayListApi.detail(playlistSource.value, props.id)
songs.value = res.map((song) => ({
songs.value = res.data.map((song) => ({
id: song.id,
title: song.name,
artist: song.artist,
@ -113,7 +113,8 @@ function playAll() {
async function playWithUrl(song: Song, queue?: Song[]) {
try {
const source = (song.source ?? playlistSource.value) as Source
const { url } = await MusicApi.url(String(song.id), source)
const res = await MusicApi.url(String(song.id), source)
const url = res.data.url
playerStore.playSong(song, queue, url)
} catch {
// MusicApi.stream

View File

@ -136,12 +136,12 @@ async function doSearch(q: string) {
])
if (seq !== requestSeq) return
if (songRes.status === "fulfilled") {
songResults.value = (songRes.value.songs ?? []).map(toSong)
songResults.value = (songRes.value.data.songs ?? []).map(toSong)
} else {
songResults.value = []
}
if (playlistRes.status === "fulfilled") {
playlistResults.value = (playlistRes.value.playlists ?? []).map(toPlaylist)
playlistResults.value = (playlistRes.value.data.playlists ?? []).map(toPlaylist)
} else {
playlistResults.value = []
}
@ -179,8 +179,8 @@ async function playSearchSong(song: Song) {
try {
// const res = await MusicApi.musicUrl(String(song.id), source)
// const url = "http://127.0.0.1:81" + res.data[0].url
const { url } = await MusicApi.url(String(song.id), source)
// const url = "http://127.0.0.1:81" + res.data[0].url
const res = await MusicApi.url(String(song.id), source)
const url = res.data.url
console.log("url: ", url)
playerStore.playSong(song, songResults.value, url)
} catch {