From 4ba433850231b71d40dcb5198348749e7bd01b6c Mon Sep 17 00:00:00 2001 From: Yuhang Wu Date: Thu, 13 Aug 2026 15:14:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E9=9F=B3=E4=B9=90?= =?UTF-8?q?=E6=92=AD=E6=94=BE=E5=92=8C=E6=AD=8C=E5=8D=95=E6=8E=A8=E8=8D=90?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增music_url命令用于获取音乐直链 - 实现playlist_recommand命令支持歌单推荐 - 添加HTTP客户端服务用于API请求 - 集成serde_urlencoded支持查询参数编码 - 扩展PlayerBar组件功能并优化UI - 调整窗口尺寸适配新功能界面 --- src-tauri/Cargo.lock | 19 ++ src-tauri/Cargo.toml | 2 +- src-tauri/src/commands/music.rs | 32 ++ src-tauri/src/commands/playlist.rs | 21 +- src-tauri/src/lib.rs | 3 + src-tauri/src/models.rs | 61 ++++ src-tauri/src/services/http.rs | 515 +++++++++++++++++++++++++++++ src-tauri/src/services/mod.rs | 3 +- src-tauri/tauri.conf.json | 4 +- src/api/music.ts | 11 +- src/api/playlist.ts | 12 +- src/components/MusicCard.vue | 28 +- src/components/PlayerBar.vue | 199 +++++------ src/components/SongTable.vue | 33 +- src/layouts/MainLayout.vue | 24 +- src/views/DiscoverView.vue | 140 ++------ src/views/PlayMusic.vue | 7 +- src/views/SearchView.vue | 52 ++- 18 files changed, 847 insertions(+), 319 deletions(-) create mode 100644 src-tauri/src/models.rs create mode 100644 src-tauri/src/services/http.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index a6530c3..24f2613 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3367,6 +3367,7 @@ dependencies = [ "rustls-platform-verifier", "serde", "serde_json", + "serde_urlencoded", "sync_wrapper", "tokio", "tokio-rustls", @@ -3504,6 +3505,12 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -3726,6 +3733,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_with" version = "3.22.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 7b2c23e..9fa1f58 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -26,7 +26,7 @@ tauri = { version = "2", features = [] } tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" -reqwest = { version = "0.13.4", features = ["json"] } +reqwest = { version = "0.13.4", features = ["json", "query"] } tokio = { version = "1.53.1", features = ["full"] } tauri-plugin-store = "2" tauri-helper = "0.2.1" diff --git a/src-tauri/src/commands/music.rs b/src-tauri/src/commands/music.rs index e69de29..5473208 100644 --- a/src-tauri/src/commands/music.rs +++ b/src-tauri/src/commands/music.rs @@ -0,0 +1,32 @@ +use tauri::State; +use tauri_helper::auto_collect_command; + +use crate::models::METING_API_URL; +use crate::{ + models::ApiResponse, + services::http::{ApiEndpoint, HttpClient}, +}; + +#[tauri::command] +#[auto_collect_command] +pub async fn music_url( + state: State<'_, HttpClient>, + id: String, + server: String, + type_: String, +) -> Result, 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)) +} diff --git a/src-tauri/src/commands/playlist.rs b/src-tauri/src/commands/playlist.rs index a148252..01521a2 100644 --- a/src-tauri/src/commands/playlist.rs +++ b/src-tauri/src/commands/playlist.rs @@ -1,8 +1,23 @@ -use reqwest; +use tauri::State; use tauri_helper::auto_collect_command; +use crate::{models::ApiResponse, services::http::HttpClient}; + #[tauri::command] #[auto_collect_command] -pub async fn playlist_recommand() -> Result { - Ok("playlist_recommand".to_string()) +pub async fn playlist_recommand( + state: State<'_, HttpClient>, + sources: Vec, +) -> Result, String> { + let params: Vec<(String, String)> = sources + .iter() + .map(|source| ("sources".to_string(), source.clone())) + .collect(); + + let data = state + .get("/api/v1/playlist/recommend", ¶ms) + .await + .map_err(|e| e.to_string()); + + Ok(ApiResponse::success(data?)) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 25e798b..42b2b1e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,10 +1,12 @@ mod commands; mod services; +mod models; use commands::album::*; use commands::music::*; use commands::playlist::*; use services::db::init_pool; +use services::http::HttpClient; use tauri::Manager; #[cfg_attr(mobile, tauri::mobile_entry_point)] @@ -16,6 +18,7 @@ pub fn run() { init_pool().await.expect("sqlite数据库连接池初始化失败!") }); app.manage(pool); + app.manage(HttpClient::default()); Ok(()) }) .plugin(tauri_plugin_store::Builder::default().build()) diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs new file mode 100644 index 0000000..bd9cf62 --- /dev/null +++ b/src-tauri/src/models.rs @@ -0,0 +1,61 @@ +use serde::{Deserialize, Serialize}; + +/// 音乐聚合后端基础地址 +pub const GO_MUSIC_URL: &str = "http://127.0.0.1:8080"; +pub const METING_API_URL: &str = "http://127.0.0.1:81"; + +#[derive(Debug, Serialize, Deserialize)] +pub struct PageResult { + pub total: i64, + pub list: Vec, + pub page: i32, + pub page_size: i32, +} + +/// Go_Music_Api 标准响应结构:`{ code: number, msg: string, data: T }` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GoMusicApiResponse { + pub code: i64, + #[serde(default)] + pub msg: String, + #[serde(default)] + pub data: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ApiResponse { + success: bool, + code: i32, + msg: String, + data: Option, +} + +impl ApiResponse { + // 成功响应 + pub fn success(data: T) -> Self { + Self { + success: true, + code: 200, + msg: "操作成功".to_string(), + data: Some(data), + } + } + // 成功响应,数据为空 + pub fn success_empty() -> ApiResponse<()> { + ApiResponse { + success: true, + code: 200, + msg: "操作成功".to_string(), + data: None, + } + } + // 错误响应 + pub fn error(code: i32, message: &str) -> Self { + Self { + success: false, + code, + msg: message.to_string(), + data: None, + } + } +} diff --git a/src-tauri/src/services/http.rs b/src-tauri/src/services/http.rs new file mode 100644 index 0000000..e36d949 --- /dev/null +++ b/src-tauri/src/services/http.rs @@ -0,0 +1,515 @@ +//! 基于 reqwest 的 HTTP 请求工具。 +//! +//! 目标是与前端 `src/utils/request.ts` 的行为保持一致: +//! - 统一 base URL 与超时时间 +//! - 自动携带 `Authorization: Bearer ` +//! - 统一解包后端返回结构 `{ code, msg, data }`,`code == 200` 时直接返回 `data` +//! - 同时提供原始字节 / 文本接口,用于封面图、音频流、歌词文件下载等非 JSON 场景 + +use std::fmt; +use std::sync::RwLock; +use std::time::Duration; + +use reqwest::header::{HeaderMap, HeaderValue, ACCEPT}; +use reqwest::{Method, StatusCode}; +use serde::de::DeserializeOwned; +use serde::Serialize; + +use crate::models::GO_MUSIC_URL; +use crate::models::METING_API_URL; + +/// 默认超时时间(秒),与前端 axios 的 `timeout: 15000` 一致。 +pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(15); + +/// 默认 User-Agent。 +pub const DEFAULT_USER_AGENT: &str = "YwYMusic/0.1"; + +/// 业务成功状态码,与前端 `SUCCESS_CODE` 一致。 +const SUCCESS_CODE: i64 = 200; + +/// 请求使用的 API 地址。 +#[derive(Debug, Clone, Copy)] +pub enum ApiEndpoint { + /// 使用 `HttpClient` 配置的地址,默认是 `GO_MUSIC_URL`。 + GoMusic, + /// 使用 `METING_API_URL`。 + Meting, +} + +/// HTTP 请求过程中可能产生的错误。 +#[derive(Debug)] +pub enum ApiError { + /// 创建 `reqwest::Client` 失败。 + Build(reqwest::Error), + /// 请求地址无效。 + InvalidUrl(String), + /// 网络请求失败。 + Request(reqwest::Error), + /// 服务端返回非成功 HTTP 状态码。 + HttpStatus { status: StatusCode, body: String }, + /// 业务码非 200。 + Business { code: i64, msg: String }, + /// 响应体解析失败。 + Decode { body: String, source: serde_json::Error }, + /// 响应不是标准 `{ code, msg, data }` 结构。 + Unexpected { body: String }, +} + +impl fmt::Display for ApiError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ApiError::Build(e) => write!(f, "创建 HTTP 客户端失败: {e}"), + ApiError::InvalidUrl(e) => write!(f, "无效的请求地址: {e}"), + ApiError::Request(e) => write!(f, "网络请求失败: {e}"), + ApiError::HttpStatus { status, body } => { + write!(f, "HTTP {status}: {body}") + } + ApiError::Business { code, msg } => write!(f, "业务错误[{code}]: {msg}"), + ApiError::Decode { body, source } => { + let preview: String = body.chars().take(300).collect(); + write!(f, "响应解析失败: {source}; body: {preview}") + } + ApiError::Unexpected { body } => write!(f, "响应格式异常: {body}"), + } + } +} + +impl std::error::Error for ApiError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + ApiError::Build(e) | ApiError::Request(e) => Some(e), + ApiError::Decode { source, .. } => Some(source), + _ => None, + } + } +} + +impl From for ApiError { + fn from(e: reqwest::Error) -> Self { + ApiError::Request(e) + } +} + +/// 共享的 HTTP 客户端。 +/// +/// `reqwest::Client` 内部维护连接池,建议整个应用只创建一次并通过 Tauri 状态共享。 +pub struct HttpClient { + client: reqwest::Client, + base_url: String, + token: RwLock>, +} + +impl Default for HttpClient { + fn default() -> Self { + Self::builder() + .build() + .expect("默认 HTTP 客户端构建失败") + } +} + +impl HttpClient { + /// 使用指定 base URL 创建客户端。 + pub fn new(base_url: impl Into) -> Result { + Self::builder().base_url(base_url).build() + } + + pub fn builder() -> HttpClientBuilder { + HttpClientBuilder::default() + } + + pub fn base_url(&self) -> &str { + &self.base_url + } + + /// 更新鉴权 token,`None` 表示清除。 + pub fn set_token(&self, token: Option) { + *self.token.write().expect("token 锁已污染") = token; + } + + pub fn token(&self) -> Option { + self.token.read().expect("token 锁已污染").clone() + } + + /// GET 请求并解包标准业务结构。 + pub async fn get( + &self, + path: &str, + query: &[(String, String)], + ) -> Result + where + T: DeserializeOwned, + { + self.get_at(ApiEndpoint::GoMusic, path, query).await + } + + /// GET 请求并解包标准业务结构,可在单次调用时选择 API 地址。 + pub async fn get_at( + &self, + endpoint: ApiEndpoint, + path: &str, + query: &[(String, String)], + ) -> Result + where + T: DeserializeOwned, + { + self.request_at::(endpoint, Method::GET, path, query, None) + .await + } + + /// DELETE 请求并解包标准业务结构。 + pub async fn delete( + &self, + path: &str, + query: &[(String, String)], + ) -> Result + where + T: DeserializeOwned, + { + self.request_at::( + ApiEndpoint::GoMusic, + Method::DELETE, + path, + query, + None, + ) + .await + } + + /// DELETE 请求并解包标准业务结构,可在单次调用时选择 API 地址。 + pub async fn delete_at( + &self, + endpoint: ApiEndpoint, + path: &str, + query: &[(String, String)], + ) -> Result + where + T: DeserializeOwned, + { + self.request_at::(endpoint, Method::DELETE, path, query, None) + .await + } + + /// POST 请求(JSON 请求体)并解包标准业务结构。 + pub async fn post(&self, path: &str, body: &B) -> Result + where + T: DeserializeOwned, + B: Serialize + ?Sized, + { + self.request_at(ApiEndpoint::GoMusic, Method::POST, path, &[], Some(body)) + .await + } + + /// POST 请求(JSON 请求体),可在单次调用时选择 API 地址。 + pub async fn post_at( + &self, + endpoint: ApiEndpoint, + path: &str, + body: &B, + ) -> Result + where + T: DeserializeOwned, + B: Serialize + ?Sized, + { + self.request_at(endpoint, Method::POST, path, &[], Some(body)) + .await + } + + /// POST 请求(无请求体)并解包标准业务结构。 + pub async fn post_empty(&self, path: &str) -> Result + where + T: DeserializeOwned, + { + self.request_at::( + ApiEndpoint::GoMusic, + Method::POST, + path, + &[], + None, + ) + .await + } + + /// POST 请求(无请求体),可在单次调用时选择 API 地址。 + pub async fn post_empty_at(&self, endpoint: ApiEndpoint, path: &str) -> Result + where + T: DeserializeOwned, + { + self.request_at::(endpoint, Method::POST, path, &[], None) + .await + } + + /// PUT 请求(JSON 请求体)并解包标准业务结构。 + pub async fn put(&self, path: &str, body: &B) -> Result + where + T: DeserializeOwned, + B: Serialize + ?Sized, + { + self.request_at(ApiEndpoint::GoMusic, Method::PUT, path, &[], Some(body)) + .await + } + + /// PUT 请求(JSON 请求体),可在单次调用时选择 API 地址。 + pub async fn put_at( + &self, + endpoint: ApiEndpoint, + path: &str, + body: &B, + ) -> Result + where + T: DeserializeOwned, + B: Serialize + ?Sized, + { + self.request_at(endpoint, Method::PUT, path, &[], Some(body)) + .await + } + + /// 获取原始字节(封面图、音频流等非 JSON 场景)。 + pub async fn get_bytes( + &self, + path: &str, + query: &[(String, String)], + ) -> Result, ApiError> { + self.get_bytes_at(ApiEndpoint::GoMusic, path, query).await + } + + /// 获取原始字节,可在单次调用时选择 API 地址。 + pub async fn get_bytes_at( + &self, + endpoint: ApiEndpoint, + path: &str, + query: &[(String, String)], + ) -> Result, ApiError> { + let response = self.build(endpoint, Method::GET, path, query)?.send().await?; + let status = response.status(); + let bytes = response.bytes().await?; + if !status.is_success() { + return Err(ApiError::HttpStatus { + status, + body: String::from_utf8_lossy(&bytes).into_owned(), + }); + } + Ok(bytes.to_vec()) + } + + /// 获取原始文本(歌词文件等非 JSON 场景)。 + pub async fn get_text( + &self, + path: &str, + query: &[(String, String)], + ) -> Result { + self.get_text_at(ApiEndpoint::GoMusic, path, query).await + } + + /// 获取原始文本,可在单次调用时选择 API 地址。 + pub async fn get_text_at( + &self, + endpoint: ApiEndpoint, + path: &str, + query: &[(String, String)], + ) -> Result { + let response = self.build(endpoint, Method::GET, path, query)?.send().await?; + let status = response.status(); + let text = response.text().await?; + if !status.is_success() { + return Err(ApiError::HttpStatus { status, body: text }); + } + Ok(text) + } + + /// 底层通用请求,`body` 为 `None` 时不发送请求体。 + pub async fn request( + &self, + method: Method, + path: &str, + query: &[(String, String)], + body: Option<&B>, + ) -> Result + where + T: DeserializeOwned, + B: Serialize + ?Sized, + { + self.request_at(ApiEndpoint::GoMusic, method, path, query, body) + .await + } + + /// 底层通用请求,可在单次调用时选择 API 地址。 + pub async fn request_at( + &self, + endpoint: ApiEndpoint, + method: Method, + path: &str, + query: &[(String, String)], + body: Option<&B>, + ) -> Result + where + T: DeserializeOwned, + B: Serialize + ?Sized, + { + let mut builder = self.build(endpoint, method, path, query)?; + if let Some(body) = body { + builder = builder.json(body); + } + let response = builder.send().await?; + unwrap_json(response).await + } + + fn build( + &self, + endpoint: ApiEndpoint, + method: Method, + path: &str, + query: &[(String, String)], + ) -> Result { + let url = self.join_url(endpoint, path)?; + let mut builder = self.client.request(method, &url); + + if let Some(token) = self.token() { + builder = builder.bearer_auth(token); + } + if !query.is_empty() { + builder = builder.query(query); + } + + Ok(builder) + } + + fn join_url(&self, endpoint: ApiEndpoint, path: &str) -> Result { + let base_url = match endpoint { + ApiEndpoint::GoMusic => &self.base_url, + ApiEndpoint::Meting => METING_API_URL, + }; + let base = base_url.trim_end_matches('/'); + let url = format!("{base}{path}"); + reqwest::Url::parse(&url).map_err(|e| ApiError::InvalidUrl(e.to_string()))?; + Ok(url) + } +} + +/// `HttpClient` 构建器。 +#[derive(Clone)] +pub struct HttpClientBuilder { + base_url: String, + timeout: Duration, + user_agent: String, + token: Option, + default_headers: HeaderMap, +} + +impl Default for HttpClientBuilder { + fn default() -> Self { + let mut default_headers = HeaderMap::new(); + default_headers.insert( + ACCEPT, + HeaderValue::from_static("application/json, text/plain, */*"), + ); + + Self { + base_url: GO_MUSIC_URL.to_string(), + timeout: DEFAULT_TIMEOUT, + user_agent: DEFAULT_USER_AGENT.to_string(), + token: None, + default_headers, + } + } +} + +impl HttpClientBuilder { + pub fn base_url(mut self, value: impl Into) -> Self { + self.base_url = value.into(); + self + } + + pub fn timeout(mut self, value: Duration) -> Self { + self.timeout = value; + self + } + + pub fn user_agent(mut self, value: impl Into) -> Self { + self.user_agent = value.into(); + self + } + + pub fn token(mut self, value: impl Into) -> Self { + self.token = Some(value.into()); + self + } + + pub fn header( + mut self, + name: reqwest::header::HeaderName, + value: HeaderValue, + ) -> Self { + self.default_headers.insert(name, value); + self + } + + pub fn build(self) -> Result { + let client = reqwest::Client::builder() + .user_agent(&self.user_agent) + .timeout(self.timeout) + .default_headers(self.default_headers) + .build() + .map_err(ApiError::Build)?; + + Ok(HttpClient { + client, + base_url: self.base_url, + token: RwLock::new(self.token), + }) + } +} + +/// 解包标准业务响应结构 `{ code, msg, data }`。 +async fn unwrap_json( + response: reqwest::Response, +) -> Result { + let status = response.status(); + let bytes = response.bytes().await?; + let body = String::from_utf8_lossy(&bytes).to_string(); + + if !status.is_success() { + return Err(ApiError::HttpStatus { status, body }); + } + + let value: serde_json::Value = serde_json::from_slice(&bytes).map_err(|source| { + ApiError::Decode { + body: body.clone(), + source, + } + })?; + + let obj = value + .as_object() + .ok_or_else(|| ApiError::Unexpected { body: body.clone() })?; + + if !obj.contains_key("code") { + return Err(ApiError::Unexpected { body }); + } + + let code = code_of(&value).ok_or_else(|| ApiError::Unexpected { body: body.clone() })?; + + if code == SUCCESS_CODE { + let data = obj + .get("data") + .cloned() + .unwrap_or(serde_json::Value::Null); + return serde_json::from_value(data).map_err(|source| ApiError::Decode { body, source }); + } + + let msg = obj + .get("msg") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + + Err(ApiError::Business { code, msg }) +} + +/// 兼容 `code` 为数字或字符串两种形式。 +fn code_of(value: &serde_json::Value) -> Option { + match value.get("code") { + Some(serde_json::Value::Number(n)) => n.as_i64(), + Some(serde_json::Value::String(s)) => s.parse::().ok(), + _ => None, + } +} diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs index 8c5eabd..ae79150 100644 --- a/src-tauri/src/services/mod.rs +++ b/src-tauri/src/services/mod.rs @@ -1 +1,2 @@ -pub mod db; \ No newline at end of file +pub mod db; +pub mod http; diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 5f8fc4e..b1f812e 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -13,8 +13,8 @@ "windows": [ { "title": "YwYMusic", - "width": 860, - "height": 600 + "width": 1160, + "height": 660 } ], "security": { diff --git a/src/api/music.ts b/src/api/music.ts index 7ff1967..a36360e 100644 --- a/src/api/music.ts +++ b/src/api/music.ts @@ -1,9 +1,14 @@ import { get } from "@/utils/request" -import Source from "@/types/global" +import Source, { ApiResponse } from "@/types/global" +import { invoke } from "@tauri-apps/api/core" /** 音乐直链 */ export interface MusicUrl { + title: string + author: string url: string + pic: string + lrc: string } /** 搜索结果-单曲 */ @@ -148,6 +153,10 @@ const MusicApi = { url: (id: string, source: Source) => { return get(`/api/v1/music/url?id=${id}&source=${source}`) }, + + musicUrl: (id: string, source: Source) => { + return invoke>("music_url", { id, server: source, type: "song" }) + }, } export default MusicApi diff --git a/src/api/playlist.ts b/src/api/playlist.ts index a4d771e..4522042 100644 --- a/src/api/playlist.ts +++ b/src/api/playlist.ts @@ -1,5 +1,6 @@ import { get } from "@/utils/request" -import Source from "@/types/global" +import Source, { type ApiResponse } from "@/types/global" +import { invoke } from "@tauri-apps/api/core" /** 歌单分类 */ interface PlayListCategory { @@ -28,8 +29,8 @@ interface PlayListSummary { description: string source: string link: string - /** 平台相关的附加元数据,字段随 source 变化 */ - extra: { + /** 平台相关的附加元数据,字段随 source 变化(搜索结果可能缺失) */ + extra?: { category_id: string global_specialid: string id: string @@ -132,8 +133,9 @@ const PlayListApi = { * @returns */ recommend: (sources?: Source[]) => { - const sourceParams = (sources ?? []).map((s) => `sources=${encodeURIComponent(s)}`).join("&") - return get(`/api/v1/playlist/recommend${sourceParams ? `?${sourceParams}` : ""}`) + // const sourceParams = (sources ?? []).map((s) => `sources=${encodeURIComponent(s)}`).join("&") + // return get(`/api/v1/playlist/recommend${sourceParams ? `?${sourceParams}` : ""}`) + return invoke>("playlist_recommand", { sources }) }, } diff --git a/src/components/MusicCard.vue b/src/components/MusicCard.vue index f687654..d78d737 100644 --- a/src/components/MusicCard.vue +++ b/src/components/MusicCard.vue @@ -14,41 +14,41 @@ - {{ playlist.playCount }} + {{ formatPlayCount(playlist.play_count) }}

{{ playlist.name }}

-

{{ playlist.desc }}

+

{{ playlist.description }}

diff --git a/src/components/PlayerBar.vue b/src/components/PlayerBar.vue index bec5318..dcca1f8 100644 --- a/src/components/PlayerBar.vue +++ b/src/components/PlayerBar.vue @@ -1,45 +1,23 @@