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

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

View File

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

View File

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

View File

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

View File

@ -41,7 +41,7 @@
<script setup lang="ts">
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 { usePlayerStore } from '../stores/player';
import { useUserStore } from '../stores/user';
@ -71,7 +71,7 @@ async function loadData() {
}
loading.value = true;
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 created = (playlistData.playlist || []).filter((p: any) => !p.subscribed);
if (created.length === 0) {
@ -79,7 +79,7 @@ async function loadData() {
return;
}
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);
songs.value = (trackData.songs || []).map(normalizeSong);
pageCacheSet('favoriteSongs', songs.value);

View File

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

View File

@ -76,7 +76,7 @@
<script setup lang="ts">
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 { useDownload } from '../composables/useDownload';
import { useSettingsStore } from '../stores/settings';
@ -129,7 +129,7 @@ async function refresh() {
loading.value = true;
pageCacheInvalidate('localMusic');
try {
const list = await invoke<LocalSong[]>('list_local_songs', { downloadPath: settings.downloadPath || null });
const list = await DownloadApi.listLocalSongs(settings.downloadPath || null);
songs.value = list;
pageCacheSet('localMusic', list);
fetchMissingCovers();
@ -145,7 +145,7 @@ async function fetchMissingCovers() {
if (missing.length === 0) return;
const ids = [...new Set(missing.map(s => s.id))];
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 detailMap = new Map<number, string>();
for (const s of data.songs || []) {
@ -194,7 +194,7 @@ function confirmDelete(song: LocalSong) {
async function doDelete() {
if (!deleteTarget.value) return;
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);
download.localSongIds.delete(deleteTarget.value.id);
showToast('已删除', 'success');

View File

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

View File

@ -68,7 +68,7 @@
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue';
import { useRoute } from 'vue-router';
import { invoke } from '@tauri-apps/api/core';
import { MusicApi } from '../api';
import { usePlayerStore } from '../stores/player';
import { useUserStore } from '../stores/user';
import { showToast } from '../composables/useToast';
@ -98,7 +98,7 @@ async function fetchPlaylist(id: number) {
playlist.value = null;
songs.value = [];
try {
const jsonStr: string = await invoke('get_playlist_detail', { id });
const jsonStr: string = await MusicApi.getPlaylistDetail(id);
const data = JSON.parse(jsonStr);
playlist.value = data.playlist;
songs.value = (data.playlist.tracks || []).map(normalizeSong);
@ -128,7 +128,7 @@ async function toggleSubscribe() {
if (!playlist.value) return;
const newSubscribed = !subscribed.value;
try {
await invoke('playlist_subscribe', { query: { id: Number(playlist.value.id), subscribe: newSubscribed } });
await MusicApi.playlistSubscribe(Number(playlist.value.id), newSubscribed);
subscribed.value = newSubscribed;
showToast(subscribed.value ? '已收藏歌单' : '已取消收藏', 'success');
} 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 { useToast } from '../composables/useToast';
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 { openUrl } from '@tauri-apps/plugin-opener';
import { open } from '@tauri-apps/plugin-dialog';
@ -287,7 +287,7 @@ const selectedDevice = computed({
set: (val: string) => {
const device = val === '' ? null : val;
settings.setOutputDevice(device);
invoke('set_output_device', { device }).then(() => {
DeviceApi.setOutputDevice(device).then(() => {
showToast(device ? `已切换到: ${device}` : '已切换到系统默认', 'success');
}).catch((e) => {
console.error('切换设备失败: ', e);
@ -298,7 +298,7 @@ const selectedDevice = computed({
async function loadDevices() {
try {
devices.value = await invoke<string[]>('get_output_devices');
devices.value = await DeviceApi.getOutputDevices();
} catch (e) {
console.error('获取设备失败: ', e);
}
@ -309,7 +309,7 @@ const defaultDownloadPath = ref('');
onMounted(async () => {
appVersion.value = await getVersion();
try {
defaultDownloadPath.value = await invoke<string>('get_default_download_path');
defaultDownloadPath.value = await DownloadApi.getDefaultDownloadPath();
} catch { /* 忽略 */ }
loadDevices();
});