mirror of
https://github.com/imsyy/SPlayer.git
synced 2025-11-25 03:14:57 +08:00
✨ feat: 新增 每日推荐 - 不感兴趣
This commit is contained in:
@@ -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] - 推荐类型
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取歌曲下载链接
|
||||
|
||||
1
src/assets/icons/HeartBroken.svg
Normal file
1
src/assets/icons/HeartBroken.svg
Normal 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 |
@@ -217,9 +217,6 @@ const changeGlobalTheme = () => {
|
||||
railColor: toRGBA(primaryRGB, 0.2),
|
||||
railColorHover: toRGBA(primaryRGB, 0.3),
|
||||
},
|
||||
Popover: {
|
||||
color: `rgb(${surfaceContainerRGB})`,
|
||||
},
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user