feat: 新增 每日推荐 - 不感兴趣

This commit is contained in:
底层用户
2025-10-24 15:23:51 +08:00
parent 2efc0a5228
commit 6beb9c78e1
11 changed files with 139 additions and 25 deletions

View File

@@ -12,6 +12,16 @@ export const dailyRecommend = (type: "songs" | "resource" = "songs") => {
});
};
/**
* 每日推荐 - 不感兴趣
*/
export const dailyRecommendDislike = (id: number) => {
return request({
url: "/recommend/songs/dislike",
params: { id, timestamp: Date.now() },
});
};
/**
* 推荐内容
* @param {string} [type] - 推荐类型

View File

@@ -67,20 +67,18 @@ export const songLyric = (id: number) => {
// 获取格式TTML的歌词
export const songLyricTTML = async (id: number) => {
const url = `https://amll-ttml-db.stevexmh.net/ncm/${id}`;
const url = `https://amll-ttml-db.stevexmh.net/ncm/${id}`;
try {
const response = await fetch(url);
if (response === null || response.status !== 200) {
console.error(`TTML API请求失败或TTML仓库没有歌词, 将会使用默认歌词`);
return null;
}
const data = await response.text();
return data;
} catch (error) {
console.error('TTML API请求出错:', error);
} catch {
return null;
}
}
};
/**
* 获取歌曲下载链接

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M12.025 20.35q-.35 0-.687-.125t-.613-.375Q8 17.45 6.3 15.812t-2.662-2.874t-1.3-2.263T2 8.5q0-2.3 1.6-3.9T7.5 3q1.65 0 2.9.637t.9 1.838l-.925 3.25q-.125.5.163.888t.787.387H13l-.65 6.35q-.025.2.163.225t.237-.15L14.6 10.3q.15-.5-.15-.9t-.8-.4H12l1.525-4.525Q13.8 3.6 14.675 3.3T16.5 3q2.3 0 3.9 1.6T22 8.5q0 1.1-.4 2.175t-1.388 2.375t-2.65 2.938t-4.212 3.862q-.275.25-.625.375t-.7.125"/></svg>

After

Width:  |  Height:  |  Size: 617 B

View File

@@ -217,9 +217,6 @@ const changeGlobalTheme = () => {
railColor: toRGBA(primaryRGB, 0.2),
railColorHover: toRGBA(primaryRGB, 0.3),
},
Popover: {
color: `rgb(${surfaceContainerRGB})`,
},
};
}
} catch (error) {

View File

@@ -15,7 +15,7 @@
<Transition name="fade" mode="out-in">
<VirtList
ref="listRef"
:key="listData?.[0]?.id"
:key="listKey"
:list="listData"
:minSize="94"
:buffer="2"
@@ -62,7 +62,15 @@
:hiddenSize="hiddenSize"
@dblclick.stop="player.updatePlayList(listData, itemData, playListId)"
@contextmenu.stop="
songListMenuRef?.openDropdown($event, listData, itemData, index, type, playListId)
songListMenuRef?.openDropdown(
$event,
listData,
itemData,
index,
type,
playListId,
isDailyRecommend,
)
"
/>
</template>
@@ -114,7 +122,7 @@ import type { DropdownOption } from "naive-ui";
import type { SongType, SortType } from "@/types/main";
import { useMusicStore, useStatusStore } from "@/stores";
import { VirtList } from "vue-virt-list";
import { cloneDeep, entries, isEmpty } from "lodash-es";
import { entries, isEmpty } from "lodash-es";
import { sortOptions } from "@/utils/meta";
import { renderIcon } from "@/utils/helper";
import SongListMenu from "@/components/Menu/SongListMenu.vue";
@@ -143,11 +151,14 @@ const props = withDefaults(
disabledSort?: boolean;
// 播放歌单 ID
playListId?: number;
// 是否为每日推荐
isDailyRecommend?: boolean;
}>(),
{
type: "song",
loadingText: "努力加载中...",
playListId: 0,
isDailyRecommend: false,
},
);
@@ -179,8 +190,9 @@ const songListMenuRef = ref<InstanceType<typeof SongListMenu> | null>(null);
// 列表数据
const listData = computed<SongType[]>(() => {
const data = cloneDeep(props.data);
if (props.disabledSort) return data;
if (props.disabledSort) return props.data;
// 创建副本用于排序(避免修改原数组)
const data = [...props.data];
// 排序
switch (statusStore.listSort) {
case "titleAZ":
@@ -212,6 +224,16 @@ const listData = computed<SongType[]>(() => {
}
});
// 虚拟列表 key
const listKey = computed(() => {
// 每日推荐
if (props.isDailyRecommend) {
return musicStore.dailySongsData.timestamp || 0;
}
// 其他列表长度(检测增删操作)
return listData.value?.length || 0;
});
// 列表是否具有播放歌曲
const hasPlaySong = computed(() => {
return listData.value.findIndex((item) => item.id === musicStore.playSong.id);

View File

@@ -18,7 +18,7 @@
<script setup lang="ts">
import type { SongType } from "@/types/main";
import { NAlert, type DropdownOption } from "naive-ui";
import { useStatusStore, useLocalStore, useDataStore } from "@/stores";
import { useStatusStore, useLocalStore, useDataStore, useMusicStore } from "@/stores";
import { renderIcon, copyData } from "@/utils/helper";
import { deleteCloudSong, importCloudSong } from "@/api/cloud";
import {
@@ -29,6 +29,8 @@ import {
} from "@/utils/modal";
import { deleteSongs, isLogin } from "@/utils/auth";
import { songUrl } from "@/api/song";
import { dailyRecommendDislike } from "@/api/rec";
import { formatSongsList } from "@/utils/format";
import player from "@/utils/player";
const emit = defineEmits<{ removeSong: [index: number[]] }>();
@@ -37,6 +39,7 @@ const router = useRouter();
const dataStore = useDataStore();
const localStore = useLocalStore();
const statusStore = useStatusStore();
const musicStore = useMusicStore();
// 右键菜单数据
const dropdownX = ref<number>(0);
@@ -52,6 +55,7 @@ const openDropdown = (
index: number,
type: "song" | "radio",
playListId?: number,
isDailyRecommend: boolean = false,
) => {
try {
e.preventDefault();
@@ -110,6 +114,15 @@ const openDropdown = (
key: "line-1",
type: "divider",
},
{
key: "dislike",
label: "不感兴趣",
show: isDailyRecommend && isLoginNormal,
props: {
onClick: () => dislikeSong(song, index),
},
icon: renderIcon("HeartBroken"),
},
{
key: "more",
label: "更多操作",
@@ -322,6 +335,41 @@ const importSongToCloud = async (song: SongType) => {
}
};
// 每日推荐 - 不感兴趣
const dislikeSong = async (song: SongType, index: number) => {
if (!song?.id) return;
const loadingMessage = window.$message.loading("正在不感兴趣...", { duration: 0 });
try {
const result = await dailyRecommendDislike(song.id);
// 关闭 loading
loadingMessage.destroy();
if (result.code === 200) {
// 创建新数组以触发响应式更新
const currentList = [...musicStore.dailySongsData.list];
// 从列表中移除当前歌曲
currentList.splice(index, 1);
// 替换原歌曲
if (result.data) {
const formattedSong = formatSongsList([result.data])[0];
currentList.splice(index, 0, formattedSong);
}
// 更新列表(同时更新 timestamp 触发完整响应式更新)
musicStore.dailySongsData = {
list: currentList,
timestamp: Date.now(),
};
window.$message.success("已标记为不感兴趣");
} else {
window.$message.error("操作失败,请重试");
}
} catch (error) {
// 关闭 loading
loadingMessage.destroy();
window.$message.error("操作失败,请重试");
console.error("不感兴趣操作失败:", error);
}
};
defineExpose({ openDropdown });
</script>

View File

@@ -37,7 +37,7 @@
:max="1"
:step="0.01"
vertical
@update:value="(val) => player.setVolume(val)"
@update:value="(val: number) => player.setVolume(val)"
/>
<n-text class="slider-num">{{ statusStore.playVolumePercent }}%</n-text>
</div>

View File

@@ -46,6 +46,8 @@ interface StatusState {
spectrumsData: number[];
/** 纯净歌词模式 */
pureLyricMode: boolean;
/** 是否使用 TTML 歌词 */
usingTTMLLyric: boolean;
/** 当前播放索引 */
playIndex: number;
/** 歌词播放索引 */
@@ -110,6 +112,7 @@ export const useStatusStore = defineStore("status", {
currentTimeOffsetMap: {},
songCoverTheme: {},
pureLyricMode: false,
usingTTMLLyric: false,
spectrumsData: [],
playIndex: -1,
lyricIndex: -1,

View File

@@ -14,12 +14,14 @@ const getExcludeKeywords = () => {
// 恢复默认
export const resetSongLyric = () => {
const musicStore = useMusicStore();
const statusStore = useStatusStore();
musicStore.songLyric = {
lrcData: [],
lrcAMData: [],
yrcData: [],
yrcAMData: [],
};
statusStore.usingTTMLLyric = false;
};
// 解析歌词数据
@@ -172,21 +174,30 @@ export const alignAMLyrics = (
// 处理本地歌词
export const parseLocalLyric = (lyric: string, format: "lrc" | "ttml") => {
const statusStore = useStatusStore();
if (!lyric) {
resetSongLyric();
return;
}
const musicStore = useMusicStore();
switch (format) {
case "lrc":
parseLocalLyricLrc(lyric, musicStore);
parseLocalLyricLrc(lyric);
statusStore.usingTTMLLyric = false;
break;
case "ttml":
parseLocalLyricAM(lyric, musicStore);
parseLocalLyricAM(lyric);
statusStore.usingTTMLLyric = true;
break;
}
};
const parseLocalLyricLrc = (lyric: string, musicStore: any) => {
/**
* 解析本地LRC歌词
* @param lyric LRC格式的歌词内容
*/
const parseLocalLyricLrc = (lyric: string) => {
const musicStore = useMusicStore();
// 解析
const lrc: LyricLine[] = parseLrc(lyric);
const lrcData: LyricType[] = parseLrcData(lrc);
@@ -220,7 +231,13 @@ const parseLocalLyricLrc = (lyric: string, musicStore: any) => {
yrcAMData: [],
};
};
const parseLocalLyricAM = (lyric: string, musicStore: any) => {
/**
* 解析本地AM歌词
* @param lyric AM格式的歌词内容
*/
const parseLocalLyricAM = (lyric: string) => {
const musicStore = useMusicStore();
const ttml = parseTTML(lyric);
const yrcAMData = parseTTMLToAMLL(ttml);
const yrcData = parseTTMLToYrc(ttml);

View File

@@ -1,4 +1,4 @@
import { useMusicStore, useSettingStore } from "@/stores";
import { useMusicStore, useSettingStore, useStatusStore } from "@/stores";
import { parsedLyricsData, parseTTMLToAMLL, parseTTMLToYrc, resetSongLyric } from "../lyric";
import { songLyric, songLyricTTML } from "@/api/song";
import { parseTTML } from "@applemusic-like-lyrics/lyric";
@@ -10,14 +10,17 @@ import { LyricType } from "@/types/main";
* @param id 歌曲id
*/
export const getLyricData = async (id: number) => {
const musicStore = useMusicStore();
const settingStore = useSettingStore();
const statusStore = useStatusStore();
if (!id) {
statusStore.usingTTMLLyric = false;
resetSongLyric();
return;
}
try {
const musicStore = useMusicStore();
const settingStore = useSettingStore();
// 检测本地歌词覆盖
const getLyric = getLyricFun(settingStore.localLyricPath, id);
const [lyricRes, ttmlContent] = await Promise.all([
@@ -27,7 +30,10 @@ export const getLyricData = async (id: number) => {
parsedLyricsData(lyricRes);
if (ttmlContent) {
const parsedResult = parseTTML(ttmlContent);
if (!parsedResult?.lines?.length) return;
if (!parsedResult?.lines?.length) {
statusStore.usingTTMLLyric = false;
return;
}
const ttmlLyric = parseTTMLToAMLL(parsedResult);
const ttmlYrcLyric = parseTTMLToYrc(parsedResult);
console.log("TTML lyrics:", ttmlLyric, ttmlYrcLyric);
@@ -46,10 +52,16 @@ export const getLyricData = async (id: number) => {
...musicStore.songLyric,
...updates,
};
statusStore.usingTTMLLyric = true;
} else {
statusStore.usingTTMLLyric = false;
}
} else {
statusStore.usingTTMLLyric = false;
}
} catch (error) {
console.error("❌ Error loading lyrics:", error);
statusStore.usingTTMLLyric = false;
resetSongLyric();
}
};

View File

@@ -37,7 +37,13 @@
</n-flex>
</div>
<!-- 列表 -->
<SongList :data="musicStore.dailySongsData.list" :loading="true" height="auto" />
<SongList
:data="musicStore.dailySongsData.list"
:loading="true"
height="auto"
isDailyRecommend
disabledSort
/>
</div>
</template>