refactor: 创建统一 API 封装层,前端不再直接调用 invoke

- 新增 src/api.ts,按职责分为 MusicApi/AudioApi/DeviceApi/DownloadApi/AppApi 五个命名空间
- 替换 15 个文件中所有 invoke 调用为 API 层方法
- 后端接口变更只需修改 api.ts 一处,便于后期迭代维护
This commit is contained in:
2026-05-29 22:02:42 +08:00
parent 68f29c8ea8
commit e40f82cc51
19 changed files with 302 additions and 116 deletions

View File

@ -38,7 +38,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'; import { ref, watch, onMounted, onBeforeUnmount } from 'vue';
import { invoke } from '@tauri-apps/api/core';
import { useUserStore } from './stores/user'; import { useUserStore } from './stores/user';
import { useSettingsStore, type CloseAction } from './stores/settings'; import { useSettingsStore, type CloseAction } from './stores/settings';
import { usePlayerStore } from './stores/player'; import { usePlayerStore } from './stores/player';
@ -55,6 +54,7 @@ import { useUpdater } from './composables/useUpdater';
import { getCurrentWindow } from '@tauri-apps/api/window'; import { getCurrentWindow } from '@tauri-apps/api/window';
import { listen } from '@tauri-apps/api/event'; import { listen } from '@tauri-apps/api/event';
import { register, unregister } from '@tauri-apps/plugin-global-shortcut'; import { register, unregister } from '@tauri-apps/plugin-global-shortcut';
import { MusicApi, AudioApi, DeviceApi, AppApi } from './api';
const userStore = useUserStore(); const userStore = useUserStore();
const player = usePlayerStore(); const player = usePlayerStore();
@ -87,9 +87,9 @@ onMounted(async () => {
if (userStore.isLoggedIn) { if (userStore.isLoggedIn) {
player.loadLikedIds(); player.loadLikedIds();
} }
try { await invoke('stop_audio'); } catch { /* 忽略 */ } try { await AudioApi.stopAudio(); } catch { /* 忽略 */ }
try { try {
const jsonStr: string = await invoke('get_login_status'); const jsonStr: string = await MusicApi.getLoginStatus();
const data = JSON.parse(jsonStr); const data = JSON.parse(jsonStr);
if (data.account || data.profile) { if (data.account || data.profile) {
const profile = data.profile || data.account; const profile = data.profile || data.account;
@ -105,7 +105,7 @@ onMounted(async () => {
if (settings.outputDevice) { if (settings.outputDevice) {
try { try {
await invoke('set_output_device', { device: settings.outputDevice }); await DeviceApi.setOutputDevice(settings.outputDevice);
} catch { /* 忽略 */ } } catch { /* 忽略 */ }
} }
}); });
@ -118,7 +118,7 @@ function closeWindow() {
} else if (settings.closeAction === 'minimize') { } else if (settings.closeAction === 'minimize') {
currentWindow.hide(); currentWindow.hide();
} else { } else {
invoke('exit_app'); AppApi.exitApp();
} }
} }
@ -130,7 +130,7 @@ function handleCloseAction(action: CloseAction, remember: boolean) {
if (action === 'minimize') { if (action === 'minimize') {
currentWindow.hide(); currentWindow.hide();
} else { } else {
invoke('exit_app'); AppApi.exitApp();
} }
} }

196
src/api.ts Normal file
View File

@ -0,0 +1,196 @@
import { invoke } from '@tauri-apps/api/core';
export namespace MusicApi {
export async function getLoginStatus(): Promise<string> {
return invoke('get_login_status');
}
export async function logout(): Promise<void> {
return invoke('logout');
}
export async function getQrKey(): Promise<string> {
return invoke('get_qr_key');
}
export async function checkQrStatus(key: string): Promise<string> {
return invoke('check_qr_status', { query: { key } });
}
export async function likelist(uid: number): Promise<string> {
return invoke('likelist', { uid });
}
export async function likeSong(id: number, like: boolean): Promise<void> {
return invoke('like_song', { query: { id, like: like ? 'true' : 'false' } });
}
export async function userPlaylist(uid: number): Promise<string> {
return invoke('user_playlist', { uid });
}
export async function getPlaylistDetail(id: number): Promise<string> {
return invoke('get_playlist_detail', { id });
}
export async function playlistTrackAll(id: number): Promise<string> {
return invoke('playlist_track_all', { query: { id } });
}
export async function playlistSubscribe(id: number, subscribe: boolean): Promise<void> {
return invoke('playlist_subscribe', { query: { id, subscribe } });
}
export async function recommendResource(): Promise<string> {
return invoke('recommend_resource');
}
export async function recommendSongs(): Promise<string> {
return invoke('recommend_songs');
}
export async function getSongDetail(id: string): Promise<string> {
return invoke('get_song_detail', { id });
}
export async function getSongUrl(query: { id: number; level: string; fm_mode?: boolean }): Promise<string> {
return invoke('get_song_url', { query });
}
export async function getLyric(id: number): Promise<string> {
return invoke('get_lyric', { id });
}
export async function searchSuggest(keyword: string): Promise<string> {
return invoke('search_suggest', { query: { keyword } });
}
export async function getHotSearch(): Promise<string> {
return invoke('get_hot_search');
}
export async function cloudsearch(query: { keyword: string; searchType: number; limit: number }): Promise<string> {
return invoke('cloudsearch', { query });
}
export async function albumDetail(id: number): Promise<string> {
return invoke('album_detail', { id });
}
export async function artistDetail(id: number): Promise<string> {
return invoke('artist_detail', { id });
}
export async function artistSongs(query: { id: number; order: string; limit: number; offset: number }): Promise<string> {
return invoke('artist_songs', { query });
}
export async function artistAlbum(id: number, limit: number, offset: number): Promise<string> {
return invoke('artist_album', { id, limit, offset });
}
export async function artistDesc(id: number): Promise<string> {
return invoke('artist_desc', { id });
}
export async function commentHot(query: { type: number; id: number; limit: number; offset: number }): Promise<string> {
return invoke('comment_hot', { query });
}
export async function commentLike(query: { t: number; type: number; id: number; cid: number }): Promise<void> {
return invoke('comment_like', { query });
}
export async function personalFm(): Promise<string> {
return invoke('personal_fm');
}
export async function personalFmMode(query: { mode: string; subMode: string; limit: number }): Promise<string> {
return invoke('personal_fm_mode', { query });
}
export async function fmTrash(id: number, time: number): Promise<void> {
return invoke('fm_trash', { query: { id, time } });
}
export async function scrobble(query: { id: number; sourceid: string; time: number }): Promise<void> {
return invoke('scrobble', { query });
}
}
export namespace AudioApi {
export async function playAudio(url: string): Promise<void> {
return invoke('play_audio', { url });
}
export async function playLocalAudio(path: string): Promise<void> {
return invoke('play_local_audio', { path });
}
export async function pauseAudio(): Promise<void> {
return invoke('pause_audio');
}
export async function resumeAudio(): Promise<void> {
return invoke('resume_audio');
}
export async function stopAudio(): Promise<void> {
return invoke('stop_audio');
}
export async function seekAudio(time: number): Promise<void> {
return invoke('seek_audio', { time });
}
export async function setVolume(vol: number): Promise<void> {
return invoke('set_volume', { vol });
}
export async function getAudioPosition(): Promise<number> {
return invoke('get_audio_position');
}
}
export namespace DeviceApi {
export async function getOutputDevices(): Promise<string[]> {
return invoke('get_output_devices');
}
export async function setOutputDevice(device: string | null): Promise<void> {
return invoke('set_output_device', { device });
}
}
export namespace DownloadApi {
export async function downloadSong(query: {
id: number;
name: string;
artist: string;
album: string | null;
duration: number | null;
coverUrl: string | null;
level: string;
downloadPath: string | null;
}): Promise<void> {
return invoke('download_song', { query });
}
export async function listLocalSongs(downloadPath: string | null): Promise<any[]> {
return invoke('list_local_songs', { downloadPath });
}
export async function deleteLocalSong(query: { id: number; filename: string; downloadPath: string | null }): Promise<void> {
return invoke('delete_local_song', { query });
}
export async function getDefaultDownloadPath(): Promise<string> {
return invoke('get_default_download_path');
}
}
export namespace AppApi {
export function exitApp(): Promise<void> {
return invoke('exit_app');
}
}

View File

@ -42,7 +42,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue' import { ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { invoke } from '@tauri-apps/api/core' import { MusicApi } from '../api'
import IconMessageSquare from '~icons/lucide/message-square' import IconMessageSquare from '~icons/lucide/message-square'
import IconHeart from '~icons/lucide/heart' import IconHeart from '~icons/lucide/heart'
@ -77,13 +77,11 @@ async function fetchComments(reset = false) {
} }
try { try {
const jsonStr: string = await invoke('comment_hot', { const jsonStr: string = await MusicApi.commentHot({
query: { type: props.type,
type: props.type, id: props.id,
id: props.id, limit: pageSize,
limit: pageSize, offset: (pageNo.value - 1) * pageSize
offset: (pageNo.value - 1) * pageSize
}
}) })
const data = JSON.parse(jsonStr) const data = JSON.parse(jsonStr)
const list = data.hotComments || [] const list = data.hotComments || []
@ -116,13 +114,11 @@ async function likeComment(cid: number) {
const liked = !!target.liked const liked = !!target.liked
likingSet.value.add(cid) likingSet.value.add(cid)
try { try {
await invoke('comment_like', { await MusicApi.commentLike({
query: { t: liked ? 0 : 1,
t: liked ? 0 : 1, type: props.type,
type: props.type, id: props.id,
id: props.id, cid
cid
}
}) })
target.liked = !liked target.liked = !liked
target.likedCount += liked ? -1 : 1 target.likedCount += liked ? -1 : 1

View File

@ -216,7 +216,7 @@ import { usePlayerStore, PlayMode } from '../stores/player';
import { useDownload } from '../composables/useDownload'; import { useDownload } from '../composables/useDownload';
import { formatTime } from '../utils/format'; import { formatTime } from '../utils/format';
import { getCoverUrl } from '../utils/song'; import { getCoverUrl } from '../utils/song';
import { invoke } from '@tauri-apps/api/core'; import { AudioApi } from '../api';
import { showToast } from '../composables/useToast'; import { showToast } from '../composables/useToast';
import { listen } from '@tauri-apps/api/event'; import { listen } from '@tauri-apps/api/event';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
@ -292,7 +292,7 @@ function toggleMute() {
} else { } else {
player.volume = prevVolume.value || 100; player.volume = prevVolume.value || 100;
} }
invoke('set_volume', { vol: player.volume / 100 }); AudioApi.setVolume(player.volume / 100);
} }
let onDocMove: ((e: MouseEvent) => void) | null = null; let onDocMove: ((e: MouseEvent) => void) | null = null;
@ -418,7 +418,7 @@ async function handleVolumeChange(e: Event) {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
const val = parseInt(target.value, 10); const val = parseInt(target.value, 10);
player.volume = val; player.volume = val;
await invoke('set_volume', { vol: val / 100 }); await AudioApi.setVolume(val / 100);
} }
const volumeBarBg = computed(() => { const volumeBarBg = computed(() => {

View File

@ -116,9 +116,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, onMounted } from 'vue'; import { ref, watch, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import { invoke } from '@tauri-apps/api/core';
import { useUserStore } from '../stores/user'; import { useUserStore } from '../stores/user';
import { usePlayerStore } from '../stores/player'; import { usePlayerStore } from '../stores/player';
import { MusicApi } from '../api';
import IconHome from '~icons/lucide/home'; import IconHome from '~icons/lucide/home';
import IconSearch from '~icons/lucide/search'; import IconSearch from '~icons/lucide/search';
import IconRadio from '~icons/lucide/radio'; import IconRadio from '~icons/lucide/radio';
@ -142,7 +142,7 @@ const showSubPlaylists = ref(true);
async function loadPlaylists() { async function loadPlaylists() {
if (!userStore.isLoggedIn || !userStore.user) return; if (!userStore.isLoggedIn || !userStore.user) return;
try { try {
const jsonStr: string = await invoke('user_playlist', { uid: userStore.user.userId }); const jsonStr: string = await MusicApi.userPlaylist(userStore.user.userId);
const data = JSON.parse(jsonStr); const data = JSON.parse(jsonStr);
createdPlaylists.value = (data.playlist || []).filter((p: any) => !p.subscribed).slice(1); createdPlaylists.value = (data.playlist || []).filter((p: any) => !p.subscribed).slice(1);
subPlaylists.value = (data.playlist || []).filter((p: any) => p.subscribed); subPlaylists.value = (data.playlist || []).filter((p: any) => p.subscribed);

View File

@ -1,7 +1,7 @@
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { invoke } from '@tauri-apps/api/core';
import { parseLrc, mergeTranslation, getCurrentLyricIndex, LyricLine } from '../utils/lyric'; import { parseLrc, mergeTranslation, getCurrentLyricIndex, LyricLine } from '../utils/lyric';
import { usePlayerStore } from '../stores/player'; import { usePlayerStore } from '../stores/player';
import { MusicApi } from '../api';
export function useLyric() { export function useLyric() {
const player = usePlayerStore(); const player = usePlayerStore();
@ -19,7 +19,7 @@ export function useLyric() {
return; return;
} }
try { try {
const jsonStr: string = await invoke('get_lyric', { id: song.id }); const jsonStr: string = await MusicApi.getLyric(song.id);
const data = JSON.parse(jsonStr); const data = JSON.parse(jsonStr);
const lrc = data?.lrc?.lyric || ''; const lrc = data?.lrc?.lyric || '';
const tLrc = data?.tlyric?.lyric || ''; const tLrc = data?.tlyric?.lyric || '';

View File

@ -1,9 +1,9 @@
import { reactive, watch } from 'vue'; import { reactive, watch } from 'vue';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event'; import { listen } from '@tauri-apps/api/event';
import { useSettingsStore } from '../stores/settings'; import { useSettingsStore } from '../stores/settings';
import { showToast } from '../composables/useToast'; import { showToast } from '../composables/useToast';
import { getCoverUrl, type Song } from '../utils/song'; import { getCoverUrl, type Song } from '../utils/song';
import { DownloadApi } from '../api';
interface DownloadTask { interface DownloadTask {
id: number; id: number;
@ -42,7 +42,7 @@ async function setupDownloadListener() {
async function refreshLocalIds() { async function refreshLocalIds() {
try { try {
const settings = useSettingsStore(); const settings = useSettingsStore();
const list: { id: number }[] = await invoke('list_local_songs', { downloadPath: settings.downloadPath || null }); const list: { id: number }[] = await DownloadApi.listLocalSongs(settings.downloadPath || null);
localSongIds.clear(); localSongIds.clear();
for (const s of list) { for (const s of list) {
localSongIds.add(s.id); localSongIds.add(s.id);
@ -90,17 +90,15 @@ async function downloadSong(song: Song) {
tasks.push({ id: song.id, name: song.name, progress: 0 }); tasks.push({ id: song.id, name: song.name, progress: 0 });
try { try {
await invoke('download_song', { await DownloadApi.downloadSong({
query: { id: song.id,
id: song.id, name: song.name,
name: song.name, artist,
artist, album: albumName,
album: albumName, duration: durationVal,
duration: durationVal, coverUrl,
coverUrl, level: settings.audioQuality,
level: settings.audioQuality, downloadPath: settings.downloadPath || null,
downloadPath: settings.downloadPath || null,
},
}); });
localSongIds.add(song.id); localSongIds.add(song.id);
} catch (e: any) { } catch (e: any) {

View File

@ -1,10 +1,10 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { ref, watch, nextTick } from 'vue'; import { ref, watch, nextTick } from 'vue';
import { invoke } from '@tauri-apps/api/core';
import { normalizeSong, type Song } from '../utils/song'; import { normalizeSong, type Song } from '../utils/song';
import { useSettingsStore } from './settings'; import { useSettingsStore } from './settings';
import { useUserStore } from './user'; import { useUserStore } from './user';
import { showToast } from '../composables/useToast'; import { showToast } from '../composables/useToast';
import { MusicApi, AudioApi } from '../api';
export type PlayMode = 'loop' | 'shuffle' | 'repeat-one'; export type PlayMode = 'loop' | 'shuffle' | 'repeat-one';
export type { Song }; export type { Song };
@ -70,7 +70,7 @@ export const usePlayerStore = defineStore('player', () => {
const userStore = useUserStore(); const userStore = useUserStore();
if (!userStore.isLoggedIn) return; if (!userStore.isLoggedIn) return;
try { try {
const json: string = await invoke('likelist', { uid: userStore.user!.userId }); const json = await MusicApi.likelist(userStore.user!.userId);
const data = JSON.parse(json); const data = JSON.parse(json);
const ids: number[] = data.ids || data.data?.ids || []; const ids: number[] = data.ids || data.data?.ids || [];
likedIds.value = new Set(ids); likedIds.value = new Set(ids);
@ -83,7 +83,7 @@ export const usePlayerStore = defineStore('player', () => {
const wasLiked = likedIds.value.has(songId); const wasLiked = likedIds.value.has(songId);
const newLike = !wasLiked; const newLike = !wasLiked;
try { try {
await invoke('like_song', { query: { id: songId, like: newLike ? 'true' : 'false' } }); await MusicApi.likeSong(songId, newLike);
if (newLike) { if (newLike) {
likedIds.value.add(songId); likedIds.value.add(songId);
} else { } else {
@ -124,12 +124,10 @@ export const usePlayerStore = defineStore('player', () => {
if (lastScrobbleId === song.id && lastScrobbleStartTime > 0) { if (lastScrobbleId === song.id && lastScrobbleStartTime > 0) {
const playedSec = Math.round((Date.now() - lastScrobbleStartTime) / 1000); const playedSec = Math.round((Date.now() - lastScrobbleStartTime) / 1000);
if (playedSec > 5) { if (playedSec > 5) {
invoke('scrobble', { MusicApi.scrobble({
query: { id: song.id,
id: song.id, sourceid: isFmMode.value ? String(song.id) : '',
sourceid: isFmMode.value ? String(song.id) : '', time: playedSec,
time: playedSec,
},
}).catch(() => {}); }).catch(() => {});
} }
} }
@ -158,7 +156,7 @@ export const usePlayerStore = defineStore('player', () => {
async function fmTrash(songId: number) { async function fmTrash(songId: number) {
try { try {
await invoke('fm_trash', { query: { id: songId, time: 25 } }); await MusicApi.fmTrash(songId, 25);
} catch (e) { } catch (e) {
console.error('fm_trash 失败', e); console.error('fm_trash 失败', e);
showToast('减少推荐失败', 'error'); showToast('减少推荐失败', 'error');
@ -169,13 +167,11 @@ export const usePlayerStore = defineStore('player', () => {
async function fetchFmBatch(): Promise<Song[]> { async function fetchFmBatch(): Promise<Song[]> {
const isDefault = fmMode.value === 'DEFAULT' && !fmSubMode.value; const isDefault = fmMode.value === 'DEFAULT' && !fmSubMode.value;
const jsonStr: string = isDefault const jsonStr: string = isDefault
? await invoke('personal_fm') ? await MusicApi.personalFm()
: await invoke('personal_fm_mode', { : await MusicApi.personalFmMode({
query: { mode: fmMode.value,
mode: fmMode.value, subMode: fmSubMode.value,
subMode: fmSubMode.value, limit: 3,
limit: 3,
},
}); });
const data = JSON.parse(jsonStr); const data = JSON.parse(jsonStr);
const raw = data.data || data; const raw = data.data || data;
@ -191,7 +187,7 @@ export const usePlayerStore = defineStore('player', () => {
reportScrobble(); reportScrobble();
if (!song.dt || song.dt === 0) { if (!song.dt || song.dt === 0) {
try { try {
const jsonStr: string = await invoke('get_song_detail', { id: String(song.id) }); const jsonStr = await MusicApi.getSongDetail(String(song.id));
const data = JSON.parse(jsonStr); const data = JSON.parse(jsonStr);
const full = data.songs?.[0]; const full = data.songs?.[0];
if (full) { if (full) {
@ -202,7 +198,7 @@ export const usePlayerStore = defineStore('player', () => {
} catch { /* 忽略 */ } } catch { /* 忽略 */ }
} }
await invoke('stop_audio'); await AudioApi.stopAudio();
queue.value = []; queue.value = [];
currentIndex.value = -1; currentIndex.value = -1;
playing.value = false; playing.value = false;
@ -210,7 +206,7 @@ export const usePlayerStore = defineStore('player', () => {
fmSong.value = song; fmSong.value = song;
currentSong.value = song; currentSong.value = song;
try { try {
const jsonStr: string = await invoke('get_song_url', { query: { id: Number(song.id), level: settings.audioQuality, fm_mode: true } }); const jsonStr = await MusicApi.getSongUrl({ id: Number(song.id), level: settings.audioQuality, fm_mode: true });
const data = JSON.parse(jsonStr); const data = JSON.parse(jsonStr);
const url: string | undefined = data.url; const url: string | undefined = data.url;
if (!url) throw new Error('无播放源'); if (!url) throw new Error('无播放源');
@ -234,7 +230,7 @@ export const usePlayerStore = defineStore('player', () => {
} }
fmVipSkipCount = 0; fmVipSkipCount = 0;
await invoke('play_audio', { url }); await AudioApi.playAudio(url);
await waitForAudioStart(); await waitForAudioStart();
playing.value = true; playing.value = true;
duration.value = (song.dt || 0) / 1000; duration.value = (song.dt || 0) / 1000;
@ -260,7 +256,7 @@ export const usePlayerStore = defineStore('player', () => {
const idx = queue.value.findIndex(s => s.id === song.id); const idx = queue.value.findIndex(s => s.id === song.id);
if (idx !== -1 && idx === currentIndex.value && currentSong.value?.id === song.id) { if (idx !== -1 && idx === currentIndex.value && currentSong.value?.id === song.id) {
if (!playing.value) { if (!playing.value) {
await invoke('resume_audio'); await AudioApi.resumeAudio();
playing.value = true; playing.value = true;
startTick(); startTick();
} }
@ -286,7 +282,7 @@ export const usePlayerStore = defineStore('player', () => {
&& queue.value.every((s, i) => s.id === songs[i].id); && queue.value.every((s, i) => s.id === songs[i].id);
if (sameQueue) { if (sameQueue) {
if (!playing.value) { if (!playing.value) {
await invoke('resume_audio'); await AudioApi.resumeAudio();
playing.value = true; playing.value = true;
startTick(); startTick();
} }
@ -324,16 +320,16 @@ export const usePlayerStore = defineStore('player', () => {
duration.value = (song.dt || 0) / 1000; duration.value = (song.dt || 0) / 1000;
if (song.localPath) { if (song.localPath) {
await invoke('play_local_audio', { path: song.localPath }); await AudioApi.playLocalAudio(song.localPath);
await waitForAudioStart(); await waitForAudioStart();
playing.value = true; playing.value = true;
startTick(); startTick();
addRecent(song); addRecent(song);
emitPlaybackState(); emitPlaybackState();
return; return;
} }
const jsonStr: string = await invoke('get_song_url', { query: { id: Number(song.id), level: settings.audioQuality } }); const jsonStr = await MusicApi.getSongUrl({ id: Number(song.id), level: settings.audioQuality });
const data = JSON.parse(jsonStr); const data = JSON.parse(jsonStr);
const url: string | undefined = data.url; const url: string | undefined = data.url;
@ -355,7 +351,7 @@ export const usePlayerStore = defineStore('player', () => {
return; return;
} }
await invoke('play_audio', { url }); await AudioApi.playAudio(url);
await waitForAudioStart(); await waitForAudioStart();
playing.value = true; playing.value = true;
startTick(); startTick();
@ -385,7 +381,7 @@ export const usePlayerStore = defineStore('player', () => {
if (syncCounter >= 2) { if (syncCounter >= 2) {
syncCounter = 0; syncCounter = 0;
try { try {
const pos = await invoke<number>('get_audio_position'); const pos = await AudioApi.getAudioPosition();
if (pos >= currentTime.value - 0.5) { if (pos >= currentTime.value - 0.5) {
currentTime.value = pos; currentTime.value = pos;
} }
@ -416,17 +412,17 @@ export const usePlayerStore = defineStore('player', () => {
async function toggle() { async function toggle() {
if (playing.value) { if (playing.value) {
await invoke('pause_audio'); await AudioApi.pauseAudio();
playing.value = false; playing.value = false;
} else { } else {
await invoke('resume_audio'); await AudioApi.resumeAudio();
playing.value = true; playing.value = true;
} }
emitPlaybackState(); emitPlaybackState();
} }
async function stop() { async function stop() {
await invoke('stop_audio'); await AudioApi.stopAudio();
playing.value = false; playing.value = false;
currentSong.value = null; currentSong.value = null;
currentTime.value = 0; currentTime.value = 0;
@ -481,7 +477,7 @@ export const usePlayerStore = defineStore('player', () => {
try { try {
currentTime.value = time; currentTime.value = time;
if (onSeekStart) onSeekStart(); if (onSeekStart) onSeekStart();
await invoke('seek_audio', { time }); await AudioApi.seekAudio(time);
startTick(); startTick();
emitPlaybackState(); emitPlaybackState();
} catch (e) { } catch (e) {
@ -492,7 +488,7 @@ export const usePlayerStore = defineStore('player', () => {
async function adjustVolume(delta: number) { async function adjustVolume(delta: number) {
const newVol = Math.max(0, Math.min(100, volume.value + delta)); const newVol = Math.max(0, Math.min(100, volume.value + delta));
volume.value = newVol; volume.value = newVol;
await invoke('set_volume', { vol: newVol / 100 }); await AudioApi.setVolume(newVol / 100);
emitPlaybackState(); emitPlaybackState();
} }
@ -654,7 +650,7 @@ listen<string>('mpris-command', (event) => {
const vol = parseFloat(cmd.slice(10)); const vol = parseFloat(cmd.slice(10));
if (!isNaN(vol)) { if (!isNaN(vol)) {
player.volume = Math.round(vol * 100); player.volume = Math.round(vol * 100);
invoke('set_volume', { vol }).catch(() => {}); AudioApi.setVolume(vol).catch(() => {});
} }
} else if (cmd.startsWith('Seek:')) { } else if (cmd.startsWith('Seek:')) {
const offsetUs = parseInt(cmd.slice(5), 10); const offsetUs = parseInt(cmd.slice(5), 10);

View File

@ -1,6 +1,6 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { ref } from 'vue'; import { ref } from 'vue';
import { invoke } from '@tauri-apps/api/core'; import { MusicApi } from '../api';
export interface UserProfile { export interface UserProfile {
userId: number; userId: number;
@ -21,7 +21,7 @@ export const useUserStore = defineStore('user', () => {
} }
async function logout() { async function logout() {
try { await invoke('logout'); } catch { /* 忽略 */ } try { await MusicApi.logout(); } catch { /* 忽略 */ }
user.value = null; user.value = null;
isLoggedIn.value = false; isLoggedIn.value = false;
localStorage.removeItem('user_profile'); localStorage.removeItem('user_profile');

View File

@ -59,7 +59,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, watch } from 'vue'; import { ref, onMounted, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { invoke } from '@tauri-apps/api/core'; import { MusicApi } from '../api';
import { usePlayerStore } from '../stores/player'; import { usePlayerStore } from '../stores/player';
import { normalizeSong, type Song } from '../utils/song'; import { normalizeSong, type Song } from '../utils/song';
import { formatDate } from '../utils/format'; import { formatDate } from '../utils/format';
@ -79,7 +79,7 @@ async function fetchAlbum(id: number) {
album.value = null; album.value = null;
songs.value = []; songs.value = [];
try { try {
const jsonStr: string = await invoke('album_detail', { id }); const jsonStr: string = await MusicApi.albumDetail(id);
const data = JSON.parse(jsonStr); const data = JSON.parse(jsonStr);
album.value = data.album; album.value = data.album;
songs.value = (data.songs || []).map(normalizeSong); songs.value = (data.songs || []).map(normalizeSong);

View File

@ -83,7 +83,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, watch } from 'vue'; import { ref, onMounted, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { invoke } from '@tauri-apps/api/core'; import { MusicApi } from '../api';
import { usePlayerStore } from '../stores/player'; import { usePlayerStore } from '../stores/player';
import { formatPlayCount, formatDate } from '../utils/format'; import { formatPlayCount, formatDate } from '../utils/format';
import { normalizeSong, type Song } from '../utils/song'; import { normalizeSong, type Song } from '../utils/song';
@ -115,10 +115,10 @@ async function fetchArtist(id: number) {
briefDesc.value = ''; briefDesc.value = '';
try { try {
const [detailStr, songsStr, albumStr, descStr] = await Promise.all([ const [detailStr, songsStr, albumStr, descStr] = await Promise.all([
invoke('artist_detail', { id }) as Promise<string>, MusicApi.artistDetail(id),
invoke('artist_songs', { query: { id, order: 'hot', limit: 50, offset: 0 } }) as Promise<string>, MusicApi.artistSongs({ id, order: 'hot', limit: 50, offset: 0 }),
invoke('artist_album', { id, limit: 30, offset: 0 }) as Promise<string>, MusicApi.artistAlbum(id, 30, 0),
invoke('artist_desc', { id }) as Promise<string>, MusicApi.artistDesc(id),
]); ]);
const detailData = JSON.parse(detailStr); const detailData = JSON.parse(detailStr);
artist.value = detailData.artist; artist.value = detailData.artist;

View File

@ -36,7 +36,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onActivated, watch } from 'vue'; import { ref, onMounted, onActivated, watch } from 'vue';
import { invoke } from '@tauri-apps/api/core'; import { MusicApi } from '../api';
import SongListItem from '../components/SongListItem.vue'; import SongListItem from '../components/SongListItem.vue';
import { usePlayerStore } from '../stores/player'; import { usePlayerStore } from '../stores/player';
import { pageCacheGet, pageCacheSet, pageCacheInvalidate, pageCacheIsStale } from '../composables/usePageCache'; import { pageCacheGet, pageCacheSet, pageCacheInvalidate, pageCacheIsStale } from '../composables/usePageCache';
@ -63,7 +63,7 @@ async function loadData() {
} }
loading.value = true; loading.value = true;
try { try {
const jsonStr: string = await invoke('recommend_songs'); const jsonStr: string = await MusicApi.recommendSongs();
const data = JSON.parse(jsonStr); const data = JSON.parse(jsonStr);
songs.value = (data.data?.dailySongs || []).map(normalizeSong); songs.value = (data.data?.dailySongs || []).map(normalizeSong);
pageCacheSet('dailySongs', songs.value); pageCacheSet('dailySongs', songs.value);

View File

@ -145,7 +145,7 @@ defineOptions({ name: 'DiscoverView' });
import { ref, computed, onMounted, onActivated, watch, nextTick } from 'vue'; import { ref, computed, onMounted, onActivated, watch, nextTick } from 'vue';
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import { invoke } from '@tauri-apps/api/core'; import { MusicApi } from '../api';
import { usePlayerStore } from '../stores/player'; import { usePlayerStore } from '../stores/player';
import SongListItem from '../components/SongListItem.vue'; import SongListItem from '../components/SongListItem.vue';
import { normalizeSong, type Song } from '../utils/song'; import { normalizeSong, type Song } from '../utils/song';
@ -231,7 +231,7 @@ function onInputChange() {
} }
suggestTimer = setTimeout(async () => { suggestTimer = setTimeout(async () => {
try { try {
const jsonStr: string = await invoke('search_suggest', { query: { keyword: keyword.value.trim() } }); const jsonStr: string = await MusicApi.searchSuggest(keyword.value.trim());
const data = JSON.parse(jsonStr); const data = JSON.parse(jsonStr);
const all = data.result?.allMatch || []; const all = data.result?.allMatch || [];
suggestions.value = all.map((m: any) => m.keyword).slice(0, 8); suggestions.value = all.map((m: any) => m.keyword).slice(0, 8);
@ -254,7 +254,7 @@ async function loadHotTags() {
hotTags.value = cached; hotTags.value = cached;
} else { } else {
try { try {
const json = await invoke('get_hot_search'); const json = await MusicApi.getHotSearch();
const data = JSON.parse(json as string); const data = JSON.parse(json as string);
hotTags.value = (data.data || []).slice(0, 12); hotTags.value = (data.data || []).slice(0, 12);
pageCacheSet('discover_hotTags', hotTags.value); pageCacheSet('discover_hotTags', hotTags.value);
@ -317,8 +317,8 @@ async function fetchTabResults(type: number) {
loading.value = true; loading.value = true;
cacheError.value = false; cacheError.value = false;
try { try {
const jsonStr: string = await invoke('cloudsearch', { const jsonStr: string = await MusicApi.cloudsearch({
query: { keyword: lastSearchKeyword.value, searchType: type, limit: 30 } keyword: lastSearchKeyword.value, searchType: type, limit: 30
}); });
const data = JSON.parse(jsonStr); const data = JSON.parse(jsonStr);
const result = data.result || {}; const result = data.result || {};

View File

@ -41,7 +41,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onActivated, watch } from 'vue'; import { ref, onMounted, onActivated, watch } from 'vue';
import { invoke } from '@tauri-apps/api/core'; import { MusicApi } from '../api';
import SongListItem from '../components/SongListItem.vue'; import SongListItem from '../components/SongListItem.vue';
import { usePlayerStore } from '../stores/player'; import { usePlayerStore } from '../stores/player';
import { useUserStore } from '../stores/user'; import { useUserStore } from '../stores/user';
@ -71,7 +71,7 @@ async function loadData() {
} }
loading.value = true; loading.value = true;
try { try {
const playlistJson: string = await invoke('user_playlist', { uid: userStore.user!.userId }); const playlistJson: string = await MusicApi.userPlaylist(userStore.user!.userId);
const playlistData = JSON.parse(playlistJson); const playlistData = JSON.parse(playlistJson);
const created = (playlistData.playlist || []).filter((p: any) => !p.subscribed); const created = (playlistData.playlist || []).filter((p: any) => !p.subscribed);
if (created.length === 0) { if (created.length === 0) {
@ -79,7 +79,7 @@ async function loadData() {
return; return;
} }
const likePlaylistId = created[0].id; const likePlaylistId = created[0].id;
const trackJson: string = await invoke('playlist_track_all', { query: { id: likePlaylistId } }); const trackJson: string = await MusicApi.playlistTrackAll(likePlaylistId);
const trackData = JSON.parse(trackJson); const trackData = JSON.parse(trackJson);
songs.value = (trackData.songs || []).map(normalizeSong); songs.value = (trackData.songs || []).map(normalizeSong);
pageCacheSet('favoriteSongs', songs.value); pageCacheSet('favoriteSongs', songs.value);

View File

@ -104,7 +104,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onActivated, watch } from 'vue'; import { ref, onMounted, onActivated, watch } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { invoke } from '@tauri-apps/api/core'; import { MusicApi } from '../api';
import { useUserStore } from '../stores/user'; import { useUserStore } from '../stores/user';
import { usePlayerStore } from '../stores/player'; import { usePlayerStore } from '../stores/player';
import { pageCacheGet, pageCacheSet, pageCacheInvalidate, pageCacheIsStale } from '../composables/usePageCache'; import { pageCacheGet, pageCacheSet, pageCacheInvalidate, pageCacheIsStale } from '../composables/usePageCache';
@ -169,7 +169,7 @@ async function loadData() {
} }
const results = await Promise.allSettled( const results = await Promise.allSettled(
RANK_IDS.map(id => invoke('get_playlist_detail', { id })) RANK_IDS.map(id => MusicApi.getPlaylistDetail(id))
); );
rankPlaylists.value = results rankPlaylists.value = results
.filter(r => r.status === 'fulfilled') .filter(r => r.status === 'fulfilled')
@ -181,7 +181,7 @@ async function loadData() {
if (userStore.isLoggedIn) { if (userStore.isLoggedIn) {
try { try {
const json = await invoke('recommend_resource'); const json = await MusicApi.recommendResource();
const data = JSON.parse(json as string); const data = JSON.parse(json as string);
recPlaylists.value = data.recommend || []; recPlaylists.value = data.recommend || [];
} catch { /* 忽略 */ } } catch { /* 忽略 */ }

View File

@ -76,7 +76,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, onActivated, onBeforeUnmount, watch } from 'vue'; import { ref, computed, onMounted, onActivated, onBeforeUnmount, watch } from 'vue';
import { invoke } from '@tauri-apps/api/core'; import { MusicApi, DownloadApi } from '../api';
import { usePlayerStore } from '../stores/player'; import { usePlayerStore } from '../stores/player';
import { useDownload } from '../composables/useDownload'; import { useDownload } from '../composables/useDownload';
import { useSettingsStore } from '../stores/settings'; import { useSettingsStore } from '../stores/settings';
@ -129,7 +129,7 @@ async function refresh() {
loading.value = true; loading.value = true;
pageCacheInvalidate('localMusic'); pageCacheInvalidate('localMusic');
try { try {
const list = await invoke<LocalSong[]>('list_local_songs', { downloadPath: settings.downloadPath || null }); const list = await DownloadApi.listLocalSongs(settings.downloadPath || null);
songs.value = list; songs.value = list;
pageCacheSet('localMusic', list); pageCacheSet('localMusic', list);
fetchMissingCovers(); fetchMissingCovers();
@ -145,7 +145,7 @@ async function fetchMissingCovers() {
if (missing.length === 0) return; if (missing.length === 0) return;
const ids = [...new Set(missing.map(s => s.id))]; const ids = [...new Set(missing.map(s => s.id))];
try { try {
const jsonStr: string = await invoke('get_song_detail', { id: JSON.stringify(ids) }); const jsonStr: string = await MusicApi.getSongDetail(JSON.stringify(ids));
const data = JSON.parse(jsonStr); const data = JSON.parse(jsonStr);
const detailMap = new Map<number, string>(); const detailMap = new Map<number, string>();
for (const s of data.songs || []) { for (const s of data.songs || []) {
@ -194,7 +194,7 @@ function confirmDelete(song: LocalSong) {
async function doDelete() { async function doDelete() {
if (!deleteTarget.value) return; if (!deleteTarget.value) return;
try { try {
await invoke('delete_local_song', { query: { id: deleteTarget.value.id, filename: deleteTarget.value.filename, downloadPath: settings.downloadPath || null } }); await DownloadApi.deleteLocalSong({ id: deleteTarget.value.id, filename: deleteTarget.value.filename, downloadPath: settings.downloadPath || null });
songs.value = songs.value.filter(s => s.id !== deleteTarget.value!.id); songs.value = songs.value.filter(s => s.id !== deleteTarget.value!.id);
download.localSongIds.delete(deleteTarget.value.id); download.localSongIds.delete(deleteTarget.value.id);
showToast('已删除', 'success'); showToast('已删除', 'success');

View File

@ -20,7 +20,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'; import { ref, onMounted, onBeforeUnmount } from 'vue';
import { invoke } from '@tauri-apps/api/core'; import { MusicApi } from '../api';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useUserStore } from '../stores/user'; import { useUserStore } from '../stores/user';
import QRCode from 'qrcode'; import QRCode from 'qrcode';
@ -52,7 +52,7 @@ async function refreshQr() {
qrError.value = ''; qrError.value = '';
if (pollTimer) clearInterval(pollTimer); if (pollTimer) clearInterval(pollTimer);
try { try {
qrKey = await invoke('get_qr_key'); qrKey = await MusicApi.getQrKey();
if (!qrKey) { if (!qrKey) {
qrError.value = '未获取到登录密钥'; qrError.value = '未获取到登录密钥';
qrLoading.value = false; qrLoading.value = false;
@ -76,7 +76,7 @@ async function refreshQr() {
function startPolling() { function startPolling() {
pollTimer = setInterval(async () => { pollTimer = setInterval(async () => {
try { try {
const jsonStr: string = await invoke('check_qr_status', { query: { key: qrKey } }); const jsonStr: string = await MusicApi.checkQrStatus(qrKey);
const data = JSON.parse(jsonStr); const data = JSON.parse(jsonStr);
const code = data.code; const code = data.code;
if (code === 800) { if (code === 800) {
@ -104,7 +104,7 @@ function startPolling() {
async function fetchUserProfile() { async function fetchUserProfile() {
try { try {
const profileJson: string = await invoke('get_login_status'); const profileJson: string = await MusicApi.getLoginStatus();
const profile = JSON.parse(profileJson); const profile = JSON.parse(profileJson);
if (profile.profile) { if (profile.profile) {
userStore.setUser({ userStore.setUser({

View File

@ -68,7 +68,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'; import { ref, computed, onMounted, watch } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { invoke } from '@tauri-apps/api/core'; import { MusicApi } from '../api';
import { usePlayerStore } from '../stores/player'; import { usePlayerStore } from '../stores/player';
import { useUserStore } from '../stores/user'; import { useUserStore } from '../stores/user';
import { showToast } from '../composables/useToast'; import { showToast } from '../composables/useToast';
@ -98,7 +98,7 @@ async function fetchPlaylist(id: number) {
playlist.value = null; playlist.value = null;
songs.value = []; songs.value = [];
try { try {
const jsonStr: string = await invoke('get_playlist_detail', { id }); const jsonStr: string = await MusicApi.getPlaylistDetail(id);
const data = JSON.parse(jsonStr); const data = JSON.parse(jsonStr);
playlist.value = data.playlist; playlist.value = data.playlist;
songs.value = (data.playlist.tracks || []).map(normalizeSong); songs.value = (data.playlist.tracks || []).map(normalizeSong);
@ -128,7 +128,7 @@ async function toggleSubscribe() {
if (!playlist.value) return; if (!playlist.value) return;
const newSubscribed = !subscribed.value; const newSubscribed = !subscribed.value;
try { try {
await invoke('playlist_subscribe', { query: { id: Number(playlist.value.id), subscribe: newSubscribed } }); await MusicApi.playlistSubscribe(Number(playlist.value.id), newSubscribed);
subscribed.value = newSubscribed; subscribed.value = newSubscribed;
showToast(subscribed.value ? '已收藏歌单' : '已取消收藏', 'success'); showToast(subscribed.value ? '已收藏歌单' : '已取消收藏', 'success');
} catch { } catch {

View File

@ -258,7 +258,7 @@ import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import { useSettingsStore, qualityLabels, closeActionLabels, defaultShortcuts, themeLabels, themeColors, appearanceLabels, type CloseAction } from '../stores/settings'; import { useSettingsStore, qualityLabels, closeActionLabels, defaultShortcuts, themeLabels, themeColors, appearanceLabels, type CloseAction } from '../stores/settings';
import { useToast } from '../composables/useToast'; import { useToast } from '../composables/useToast';
import { useUpdater } from '../composables/useUpdater'; import { useUpdater } from '../composables/useUpdater';
import { invoke } from '@tauri-apps/api/core'; import { DeviceApi, DownloadApi } from '../api';
import { getVersion } from '@tauri-apps/api/app'; import { getVersion } from '@tauri-apps/api/app';
import { openUrl } from '@tauri-apps/plugin-opener'; import { openUrl } from '@tauri-apps/plugin-opener';
import { open } from '@tauri-apps/plugin-dialog'; import { open } from '@tauri-apps/plugin-dialog';
@ -287,7 +287,7 @@ const selectedDevice = computed({
set: (val: string) => { set: (val: string) => {
const device = val === '' ? null : val; const device = val === '' ? null : val;
settings.setOutputDevice(device); settings.setOutputDevice(device);
invoke('set_output_device', { device }).then(() => { DeviceApi.setOutputDevice(device).then(() => {
showToast(device ? `已切换到: ${device}` : '已切换到系统默认', 'success'); showToast(device ? `已切换到: ${device}` : '已切换到系统默认', 'success');
}).catch((e) => { }).catch((e) => {
console.error('切换设备失败: ', e); console.error('切换设备失败: ', e);
@ -298,7 +298,7 @@ const selectedDevice = computed({
async function loadDevices() { async function loadDevices() {
try { try {
devices.value = await invoke<string[]>('get_output_devices'); devices.value = await DeviceApi.getOutputDevices();
} catch (e) { } catch (e) {
console.error('获取设备失败: ', e); console.error('获取设备失败: ', e);
} }
@ -309,7 +309,7 @@ const defaultDownloadPath = ref('');
onMounted(async () => { onMounted(async () => {
appVersion.value = await getVersion(); appVersion.value = await getVersion();
try { try {
defaultDownloadPath.value = await invoke<string>('get_default_download_path'); defaultDownloadPath.value = await DownloadApi.getDefaultDownloadPath();
} catch { /* 忽略 */ } } catch { /* 忽略 */ }
loadDevices(); loadDevices();
}); });