Skip to content

Commit 7239f84

Browse files
authored
Merge pull request #779 from imsyy/dev-cy
✨ feat: 音质鉴权
2 parents 7fb0d14 + 13b199c commit 7239f84

29 files changed

Lines changed: 1528 additions & 25 deletions

File tree

components.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ declare module 'vue' {
106106
NH3: typeof import('naive-ui')['NH3']
107107
NIcon: typeof import('naive-ui')['NIcon']
108108
NImage: typeof import('naive-ui')['NImage']
109+
NImageGroup: typeof import('naive-ui')['NImageGroup']
109110
NInput: typeof import('naive-ui')['NInput']
110111
NInputGroup: typeof import('naive-ui')['NInputGroup']
111112
NInputNumber: typeof import('naive-ui')['NInputNumber']

electron/main/windows/main-window.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,20 @@ class MainWindow {
4848
}
4949
return { action: "deny" };
5050
});
51+
52+
// 拦截页面导航
53+
this.win.webContents.on("will-navigate", (event, url) => {
54+
// 检查是否为图片文件(通过扩展名简单判断)
55+
const imageExtensions = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ".ico"];
56+
const isImage = imageExtensions.some((ext) => url.toLowerCase().split("?")[0].endsWith(ext));
57+
58+
// 如果是图片,阻止导航并下载
59+
if (isImage) {
60+
event.preventDefault();
61+
this.win?.webContents.downloadURL(url);
62+
}
63+
});
64+
5165
// 窗口显示时
5266
this.win?.on("show", () => {
5367
this.win?.webContents.send("lyricsScroll");

src/api/song.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,48 @@ export const songChorus = (id: number) => {
159159
params: { id },
160160
});
161161
};
162+
163+
/**
164+
* 歌曲百科 - 简要信息
165+
* @param {number} id - 歌曲 id
166+
*/
167+
export const songWikiSummary = (id: number) => {
168+
return request({
169+
url: "/song/wiki/summary",
170+
params: { id },
171+
});
172+
};
173+
174+
/**
175+
* 乐谱列表
176+
* @description 通过歌曲 id 获取该歌曲下的乐谱列表
177+
*/
178+
export const songSheetList = (id: number) => {
179+
return request({
180+
url: "/sheet/list",
181+
params: { id },
182+
});
183+
};
184+
185+
/**
186+
* 乐谱内容预览
187+
* @description 通过乐谱 id 获取乐谱的完整内容
188+
*/
189+
export const songSheetPreview = (id: number) => {
190+
return request({
191+
url: "/sheet/preview",
192+
params: { id },
193+
});
194+
};
195+
196+
/**
197+
* 回忆坐标信息
198+
* @description 获取当前歌曲的回忆坐标信息(同手机 APP 百科页的回忆坐标功能)
199+
* @param id 歌曲 ID
200+
*/
201+
export const songFirstListenInfo = (id: number) => {
202+
return request({
203+
url: "/music/first/listen/info",
204+
params: { id },
205+
});
206+
};

src/api/user.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,12 @@ export const dailySignin = (type: 0 | 1 = 0) => {
138138
},
139139
});
140140
};
141+
// 获取用户 VIP 信息
142+
export const userVipInfo = () => {
143+
return request({
144+
url: "/vip/info",
145+
params: {
146+
timestamp: Date.now(),
147+
},
148+
});
149+
};

src/components/Card/SongCard.vue

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,17 @@
9999
>
100100
MV
101101
</n-tag>
102+
<!-- 脏标 -->
103+
<n-tag
104+
v-if="settingStore.showSongExplicitTag && (song.mark && (song.mark & EXPLICIT_CONTENT_MARK))"
105+
:bordered="false"
106+
class="explicit"
107+
type="error"
108+
round
109+
title="Explicit Content"
110+
>
111+
E
112+
</n-tag>
102113
<!-- 歌手 -->
103114
<div v-if="Array.isArray(song.artists)" class="artists">
104115
<n-text
@@ -181,6 +192,7 @@ import { usePlayerController } from "@/core/player/PlayerController";
181192
import { isElectron } from "@/utils/env";
182193
import { useBlobURLManager } from "@/core/resource/BlobURLManager";
183194
import { useMobile } from "@/composables/useMobile";
195+
import { EXPLICIT_CONTENT_MARK } from "@/utils/meta";
184196
185197
const props = defineProps<{
186198
// 歌曲

src/components/Layout/User.vue

Lines changed: 133 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<template>
22
<n-popover
33
:show="userMenuShow"
4-
style="padding: 12px; max-width: 160px"
4+
style="padding: 12px; max-width: 240px"
55
trigger="manual"
66
@clickoutside="userMenuShow = false"
77
>
@@ -22,8 +22,8 @@
2222
<SvgIcon name="Person" :depth="3" size="26" />
2323
</n-avatar>
2424
</div>
25-
<n-flex v-if="isDesktop" class="user-data" size="small">
26-
<n-text class="name">
25+
<n-flex v-if="isDesktop" :wrap="false" class="user-data" size="small">
26+
<n-text class="name text-hidden">
2727
{{ dataStore.userLoginStatus ? dataStore.userData.name || "未知用户名" : "未登录" }}
2828
</n-text>
2929
<!-- VIP -->
@@ -70,6 +70,29 @@
7070
<n-text :depth="3">部分功能暂不可用</n-text>
7171
</n-flex>
7272
<n-divider />
73+
<!-- 多账号 -->
74+
<div class="account-list" v-if="dataStore.userLoginStatus && dataStore.loginType !== 'uid'">
75+
<n-text class="subtitle" :depth="3">切换账号</n-text>
76+
<div
77+
v-for="account in otherAccounts"
78+
:key="account.userId"
79+
class="account-item"
80+
@click="handleSwitchAccount(account.userId)"
81+
>
82+
<n-avatar :src="account.avatarUrl" round size="small" />
83+
<div class="account-name text-hidden">{{ account.name }}</div>
84+
<div class="delete-btn" @click.stop="handleRemoveAccount(account.userId)">
85+
<SvgIcon name="Close" />
86+
</div>
87+
</div>
88+
<n-button class="add-account" ghost block @click="handleAddAccount">
89+
<template #icon>
90+
<SvgIcon name="Add" />
91+
</template>
92+
添加账号
93+
</n-button>
94+
</div>
95+
<n-divider v-if="dataStore.userLoginStatus" />
7396
<!-- 退出登录 -->
7497
<n-button :focusable="false" class="logout" strong secondary round @click="isLogout">
7598
<template #icon>
@@ -86,11 +109,14 @@ import { useDataStore } from "@/stores";
86109
import { openUserLogin } from "@/utils/modal";
87110
import { getLoginState } from "@/api/login";
88111
import {
112+
updateUserData,
113+
updateSpecialUserData,
89114
toLogout,
90115
isLogin,
91116
refreshLoginData,
92-
updateUserData,
93-
updateSpecialUserData,
117+
saveCurrentAccount,
118+
switchAccount,
119+
removeAccount,
94120
} from "@/utils/auth";
95121
import { useMobile } from "@/composables/useMobile";
96122
@@ -158,6 +184,55 @@ const checkLoginStatus = async () => {
158184
}
159185
};
160186
187+
// 其他账号列表(排除当前登录的)
188+
const otherAccounts = computed(() => {
189+
return dataStore.userList.filter((u) => u.userId !== dataStore.userData.userId);
190+
});
191+
192+
// 切换账号
193+
const handleSwitchAccount = async (userId: number) => {
194+
userMenuShow.value = false;
195+
await switchAccount(userId);
196+
};
197+
198+
// 移除账号
199+
const handleRemoveAccount = (userId: number) => {
200+
removeAccount(userId);
201+
};
202+
203+
// 添加新账号 (保存当前 -> 开启强制登录 -> 成功后自动切换)
204+
const handleAddAccount = async () => {
205+
// 限制账号数量
206+
if (dataStore.userList.length >= 3) {
207+
window.$message.warning("最多只能保留 3 个账号");
208+
return;
209+
}
210+
211+
// 1先保存当前账号状态 (快照)
212+
saveCurrentAccount();
213+
214+
userMenuShow.value = false;
215+
216+
// 打开登录框 (强制模式, 不登出当前用户以保持 cookies 直到新登录成功, 禁用 UID 登录)
217+
openUserLogin(
218+
false,
219+
true,
220+
async () => {
221+
// 登录成功回调
222+
// 此时新 cookies 已设置,store 已更新
223+
window.$message.loading("正在更新数据...");
224+
try {
225+
await updateUserData();
226+
window.$message.success("登录成功");
227+
// router.push("/");
228+
} catch (error) {
229+
console.error("Login update failed", error);
230+
}
231+
},
232+
true,
233+
);
234+
};
235+
161236
// 退出登录
162237
const isLogout = () => {
163238
if (!isLogin()) {
@@ -169,7 +244,11 @@ const isLogout = () => {
169244
content: "确认退出当前用户登录?",
170245
positiveText: "确认登出",
171246
negativeText: "取消",
172-
onPositiveClick: () => toLogout(),
247+
onPositiveClick: () => {
248+
// 退出时保存当前账号,方便下次登录
249+
saveCurrentAccount();
250+
toLogout();
251+
},
173252
});
174253
};
175254
@@ -233,6 +312,7 @@ onBeforeMount(() => {
233312
.user-info {
234313
.nickname {
235314
font-weight: bold;
315+
max-width: 220px;
236316
}
237317
.n-tag {
238318
height: 18px;
@@ -253,9 +333,56 @@ onBeforeMount(() => {
253333
.n-text {
254334
font-size: 12px;
255335
font-weight: normal;
336+
margin-top: 4px;
256337
}
257338
}
258339
}
340+
.account-list {
341+
.subtitle {
342+
font-size: 12px;
343+
text-align: center;
344+
margin-bottom: 8px;
345+
display: block;
346+
}
347+
.account-item {
348+
display: flex;
349+
align-items: center;
350+
padding: 6px 8px;
351+
margin-bottom: 8px;
352+
border-radius: 8px;
353+
cursor: pointer;
354+
transition: background-color 0.2s;
355+
position: relative;
356+
.account-name {
357+
margin-left: 8px;
358+
font-size: 13px;
359+
flex: 1;
360+
}
361+
.delete-btn {
362+
opacity: 0;
363+
padding: 4px;
364+
border-radius: 4px;
365+
display: flex;
366+
align-items: center;
367+
transition:
368+
background-color 0.2s,
369+
color 0.2s;
370+
&:hover {
371+
background-color: rgba(var(--primary), 0.1);
372+
color: var(--primary-color);
373+
}
374+
}
375+
&:hover {
376+
background-color: rgba(var(--primary), 0.08);
377+
.delete-btn {
378+
opacity: 1;
379+
}
380+
}
381+
}
382+
.add-account {
383+
border-radius: 8px;
384+
}
385+
}
259386
.n-divider {
260387
margin: 12px 0;
261388
}

src/components/Modal/DownloadModal.vue

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,14 @@
7373
import type { SongType, SongLevelType } from "@/types/main";
7474
import { useSettingStore } from "@/stores";
7575
import { songLevelData, getSongLevelsData, AI_AUDIO_LEVELS } from "@/utils/meta";
76+
import { filterAuthorizedQualityOptions } from "@/utils/auth";
7677
import { formatFileSize } from "@/utils/helper";
7778
import { openSetting } from "@/utils/modal";
7879
import { isElectron } from "@/utils/env";
7980
import { songDetail } from "@/api/song";
8081
import { formatSongsList } from "@/utils/format";
8182
import { pick } from "lodash-es";
8283
import { useDownloadManager } from "@/core/resource/DownloadManager";
83-
import SongDataCard from "@/components/Card/SongDataCard.vue";
8484
8585
const props = defineProps<{
8686
songs?: SongType[];
@@ -105,7 +105,7 @@ const downloadPath = computed(() => settingStore.downloadPath);
105105
106106
// 是否可以下载(需要配置下载目录)
107107
const canDownload = computed(() => {
108-
if (!isElectron) return true; // 非 Electron 环境允许下载
108+
if (!isElectron) return true;
109109
return !!downloadPath.value;
110110
});
111111
@@ -114,6 +114,9 @@ const qualityOptions = computed(() => {
114114
const levels = pick(songLevelData, ["l", "m", "h", "sq", "hr", "je", "sk", "db", "jm"]);
115115
let allData = getSongLevelsData(levels);
116116
117+
// 根据 VIP 状态过滤
118+
allData = filterAuthorizedQualityOptions(allData);
119+
117120
if (settingStore.disableAiAudio) {
118121
allData = allData.filter((item) => {
119122
if (item.level === "dolby") return true;

0 commit comments

Comments
 (0)