feat(stores): 将轻量状态管理替换为Pinia并添加持久化功能
- 使用Pinia替代原有的reactive store实现状态管理 - 集成pinia-plugin-persistedstate插件实现设置和搜索历史的持久化 - 新增searchHistory store用于存储搜索历史记录 - 更新README文档中的状态管理相关说明
30
README.md
|
|
@ -1,4 +1,3 @@
|
|||
<<<<<<< HEAD
|
||||
# YwYMusic 🎵
|
||||
|
||||
一款基于 **Tauri 2 + Vue 3** 的轻量在线音乐播放器。前端负责界面与播放,音乐数据、音频直链解析、扫码登录等能力由独立的音乐聚合后端提供。
|
||||
|
|
@ -34,7 +33,7 @@
|
|||
| 路由 | Vue Router(Hash 模式,兼容 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/ # Tauri(Rust)壳与打包配置
|
||||
│ ├─ 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", ¶ms).await?;
|
||||
|
||||
// 显式请求 METING_API_URL
|
||||
state.get_at(ApiEndpoint::Meting, "/api", ¶ms).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`。
|
||||
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 916 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 974 B After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 903 B After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.9 KiB |
|
|
@ -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>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#fff</color>
|
||||
</resources>
|
||||
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 704 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 227 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -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", ¶ms).await.map_err(|e| e.to_string())?;
|
||||
Ok(ApiResponse::success(data))
|
||||
}
|
||||
|
|
@ -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", ¶ms)
|
||||
.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", ¶ms)
|
||||
.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", ¶ms)
|
||||
.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", ¶ms)
|
||||
.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", ¶ms)
|
||||
.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", ¶ms)
|
||||
.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", ¶ms)
|
||||
.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(¶ms)
|
||||
.send()
|
||||
let params = vec![("id".to_string(), id), ("source".to_string(), source)];
|
||||
let data = http
|
||||
.get_at(ApiEndpoint::GoMusic, "/api/v1/music/url", ¶ms)
|
||||
.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(¶ms)
|
||||
// .send()
|
||||
// .await
|
||||
// .map_err(|e| e.to_string())?;
|
||||
// let data = resp.json().await.map_err(|e| e.to_string())?;
|
||||
// Ok(ApiResponse::success(data))
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -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", ¶ms)
|
||||
.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", ¶ms)
|
||||
.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", ¶ms)
|
||||
.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", ¶ms)
|
||||
.await
|
||||
.map_err(|e| e.to_string());
|
||||
Ok(ApiResponse::success(data?))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 916 KiB |
|
After Width: | Height: | Size: 658 KiB |
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
// 满页:推测还有下一页
|
||||
|
|
|
|||
|
|
@ -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 代理播放
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||