From be8e92578f6cb03b6a2e7440f8aa70596ed737dd Mon Sep 17 00:00:00 2001 From: LaoShui <79132480+laoshuikaixue@users.noreply.github.com> Date: Fri, 10 Jul 2026 19:52:44 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(home):=20=E6=B7=BB=E5=8A=A0=E6=96=B0?= =?UTF-8?q?=E6=AD=8C=E5=8F=91=E5=B8=83=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增关注歌手新歌API接口实现 - 添加新歌发布页面组件和路由配置 - 实现新歌发布列表展示和播放功能 - 集成国际化多语言支持 - 更新侧边栏导航菜单项 - 优化组件类型定义文件 --- components.d.ts | 3 +- electron/main/apis/netease/index.ts | 2 + .../modules/artist_new_song_mv_list_v2.ts | 16 ++ .../modules/artist_new_song_playall.ts | 13 ++ electron/main/apis/netease/modules/index.ts | 4 + src/apis/recommend/netease.ts | 121 ++++++++++- src/components/layout/SideBar.vue | 2 + src/components/list/NewReleaseGroup.vue | 191 ++++++++++++++++ src/i18n/locales/en-US.json | 19 ++ src/i18n/locales/zh-CN.json | 19 ++ src/pages/NewReleases.vue | 205 ++++++++++++++++++ src/router/index.ts | 5 + 12 files changed, 598 insertions(+), 2 deletions(-) create mode 100644 electron/main/apis/netease/modules/artist_new_song_mv_list_v2.ts create mode 100644 electron/main/apis/netease/modules/artist_new_song_playall.ts create mode 100644 src/components/list/NewReleaseGroup.vue create mode 100644 src/pages/NewReleases.vue diff --git a/components.d.ts b/components.d.ts index 381335e0d..fc683ad45 100644 --- a/components.d.ts +++ b/components.d.ts @@ -87,7 +87,6 @@ declare module 'vue' { IconLucideCloudUpload: typeof import('~icons/lucide/cloud-upload')['default'] IconLucideCopy: typeof import('~icons/lucide/copy')['default'] IconLucideDatabase: typeof import('~icons/lucide/database')['default'] - IconLucideDisc: typeof import('~icons/lucide/disc')['default'] IconLucideDisc3: typeof import('~icons/lucide/disc3')['default'] IconLucideDownload: typeof import('~icons/lucide/download')['default'] IconLucideEllipsis: typeof import('~icons/lucide/ellipsis')['default'] @@ -135,6 +134,7 @@ declare module 'vue' { IconLucideShuffle: typeof import('~icons/lucide/shuffle')['default'] IconLucideSkipBack: typeof import('~icons/lucide/skip-back')['default'] IconLucideSkipForward: typeof import('~icons/lucide/skip-forward')['default'] + IconLucideSparkles: typeof import('~icons/lucide/sparkles')['default'] IconLucideTextQuote: typeof import('~icons/lucide/text-quote')['default'] IconLucideTrash2: typeof import('~icons/lucide/trash2')['default'] IconLucideTriangleAlert: typeof import('~icons/lucide/triangle-alert')['default'] @@ -168,6 +168,7 @@ declare module 'vue' { NavSearch: typeof import('./src/components/layout/NavSearch.vue')['default'] NavSearchInput: typeof import('./src/components/layout/NavSearchInput.vue')['default'] NavUser: typeof import('./src/components/layout/NavUser.vue')['default'] + NewReleaseGroup: typeof import('./src/components/list/NewReleaseGroup.vue')['default'] NumberFieldDecrement: typeof import('reka-ui')['NumberFieldDecrement'] NumberFieldIncrement: typeof import('reka-ui')['NumberFieldIncrement'] NumberFieldInput: typeof import('reka-ui')['NumberFieldInput'] diff --git a/electron/main/apis/netease/index.ts b/electron/main/apis/netease/index.ts index 9817132ed..5f72a005c 100644 --- a/electron/main/apis/netease/index.ts +++ b/electron/main/apis/netease/index.ts @@ -63,6 +63,8 @@ const NON_CACHEABLE: ReadonlySet = new Set([ "personal_fm", "fm_trash", "recommend_songs", + "artist_new_song_playall", + "artist_new_song_mv_list_v2", ]); /** 国内 IP 前缀池 */ diff --git a/electron/main/apis/netease/modules/artist_new_song_mv_list_v2.ts b/electron/main/apis/netease/modules/artist_new_song_mv_list_v2.ts new file mode 100644 index 000000000..92ade37fa --- /dev/null +++ b/electron/main/apis/netease/modules/artist_new_song_mv_list_v2.ts @@ -0,0 +1,16 @@ +/** 关注歌手的新歌曲发布流(需登录) */ + +import { createOption } from "../core/option"; +import type { NeteaseModule } from "../core/types"; + +const artistNewSongMvListV2: NeteaseModule = (query, request) => { + const data = { + startTimestamp: query.startTimestamp || Date.now(), + sourceType: query.sourceType || 1, + limit: query.limit || 10, + firstRequest: query.firstRequest ?? true, + }; + return request("/api/sub/artist/new/works/song-mv/list/v2", data, createOption(query, "eapi")); +}; + +export default artistNewSongMvListV2; diff --git a/electron/main/apis/netease/modules/artist_new_song_playall.ts b/electron/main/apis/netease/modules/artist_new_song_playall.ts new file mode 100644 index 000000000..654fb0855 --- /dev/null +++ b/electron/main/apis/netease/modules/artist_new_song_playall.ts @@ -0,0 +1,13 @@ +/** + * 所有关注歌手最近的 50 首新歌(需登录) + * + * 响应:`{ code, data: { count, songList: NeteaseSong[] } }` + */ + +import { createOption } from "../core/option"; +import type { NeteaseModule } from "../core/types"; + +const artistNewSongPlayall: NeteaseModule = (query, request) => + request("/api/sub/artist/new/works/song/playall", {}, createOption(query, "eapi")); + +export default artistNewSongPlayall; diff --git a/electron/main/apis/netease/modules/index.ts b/electron/main/apis/netease/modules/index.ts index 78034a4de..f1ee113dd 100644 --- a/electron/main/apis/netease/modules/index.ts +++ b/electron/main/apis/netease/modules/index.ts @@ -75,6 +75,8 @@ import personalized from "./personalized"; import recommend_resource from "./recommend_resource"; import top_artists from "./top_artists"; import album_new from "./album_new"; +import artist_new_song_playall from "./artist_new_song_playall"; +import artist_new_song_mv_list_v2 from "./artist_new_song_mv_list_v2"; // 歌单 / 喜欢 import playlist_detail from "./playlist_detail"; @@ -164,6 +166,8 @@ export const modules: Record = { recommend_resource, top_artists, album_new, + artist_new_song_playall, + artist_new_song_mv_list_v2, playlist_detail, playlist_create, diff --git a/src/apis/recommend/netease.ts b/src/apis/recommend/netease.ts index 20adce7d0..bff4b6444 100644 --- a/src/apis/recommend/netease.ts +++ b/src/apis/recommend/netease.ts @@ -2,7 +2,14 @@ import type { Track } from "@shared/types/player"; import type { CoverItem } from "@/types/artist"; import type { NeteaseSong } from "@/types/netease"; import { netease as neteaseApi } from "@/apis/netease"; -import { songsToTracks, withPicSize, toPlaylist, toArtist, toAlbum } from "@/utils/format/netease"; +import { + ensureOk, + songsToTracks, + withPicSize, + toPlaylist, + toArtist, + toAlbum, +} from "@/utils/format/netease"; import { playlistToCoverItem, artistToCoverItem, albumToCoverItem } from "@/utils/format/coverItem"; /** 每日推荐歌曲(每日 30 首,需登录) */ @@ -11,6 +18,118 @@ export const fetchDailySongs = async (): Promise => { return songsToTracks(body?.data?.dailySongs); }; +interface FollowedArtistPlayAllResponse { + code?: number; + data?: { + count?: number; + songList?: NeteaseSong[]; + }; +} + +/** 播放所有关注歌手最近的 50 首新歌(需登录) */ +export const fetchFollowedArtistPlayAll = async (): Promise => { + const body = ensureOk(await neteaseApi.artist_new_song_playall()); + return songsToTracks(body.data?.songList); +}; + +interface RawNewReleaseTitle { + artistName: string; + artistId: number; + artistUserId?: number; + imgUrl?: string; + resourceId: number; + resourceName: string; + resourcePicUrl?: string; + publishTime: number; +} + +interface RawNewReleaseWork { + sourceType: number; + info?: { + blockTitle?: RawNewReleaseTitle; + blockType?: string; + songLists?: NeteaseSong[]; + publishTime?: number; + albumSongCount?: number; + }; +} + +interface FollowedArtistNewWorksResponse { + code?: number; + data?: { + hasMore?: boolean; + latestVisitTime?: number; + newSongCount?: number; + newWorks?: RawNewReleaseWork[]; + }; +} + +export interface FollowedArtistNewWork { + id: string; + sourceType: number; + blockType: string; + artistId: string; + artistName: string; + artistAvatar?: string; + resourceId: string; + resourceName: string; + resourceCover?: string; + publishTime: number; + albumSongCount: number; + tracks: Track[]; +} + +export interface FollowedArtistNewWorksPage { + works: FollowedArtistNewWork[]; + hasMore: boolean; + newSongCount: number; + nextTimestamp?: number; +} + +/** 关注歌手新歌发布流,按歌手和专辑/单曲分块 */ +export const fetchFollowedArtistNewWorks = async (params: { + startTimestamp: number; + firstRequest: boolean; + limit?: number; +}): Promise => { + const body = ensureOk( + await neteaseApi.artist_new_song_mv_list_v2({ + startTimestamp: params.startTimestamp, + sourceType: 1, + limit: params.limit ?? 10, + firstRequest: params.firstRequest, + }), + ); + const works = (body.data?.newWorks ?? []).flatMap((raw) => { + const title = raw.info?.blockTitle; + if (!title) return []; + const tracks = songsToTracks(raw.info?.songLists); + return [ + { + id: `${raw.sourceType}:${title.resourceId}:${title.publishTime}`, + sourceType: raw.sourceType, + blockType: raw.info?.blockType ?? "song", + artistId: String(title.artistId), + artistName: title.artistName, + artistAvatar: withPicSize(title.imgUrl, 100), + resourceId: String(title.resourceId), + resourceName: title.resourceName, + resourceCover: withPicSize(title.resourcePicUrl) ?? tracks[0]?.cover, + publishTime: raw.info?.publishTime ?? title.publishTime, + albumSongCount: raw.info?.albumSongCount ?? tracks.length, + tracks, + }, + ]; + }); + const lastTimestamp = works.at(-1)?.publishTime; + return { + works, + hasMore: body.data?.hasMore ?? false, + newSongCount: body.data?.newSongCount ?? 0, + nextTimestamp: lastTimestamp === undefined ? undefined : Math.max(0, lastTimestamp - 1), + }; +}; + /** * 心动模式智能播放列表 * @param seedId - 种子歌曲 id diff --git a/src/components/layout/SideBar.vue b/src/components/layout/SideBar.vue index 6c5c68eee..ad142de83 100644 --- a/src/components/layout/SideBar.vue +++ b/src/components/layout/SideBar.vue @@ -21,6 +21,7 @@ import IconLucideStar from "~icons/lucide/star"; import IconLucideHistory from "~icons/lucide/history"; import IconLucideDownload from "~icons/lucide/download"; import IconLucideCloud from "~icons/lucide/cloud"; +import IconLucideSparkles from "~icons/lucide/sparkles"; import IconLucidePlus from "~icons/lucide/plus"; import IconLucideChevronDown from "~icons/lucide/chevron-down"; import IconSpHeartMode from "~icons/sp/heart-mode"; @@ -169,6 +170,7 @@ const menuItems = computed(() => [ ), }, { key: "/favorites", label: t("nav.favorites"), icon: markRaw(IconLucideStar) }, + { key: "/new-releases", label: t("nav.newReleases"), icon: markRaw(IconLucideSparkles) }, { key: "/cloud", label: t("nav.cloud"), icon: markRaw(IconLucideCloud) }, ...(systemSettings.download.enabled ? ([ diff --git a/src/components/list/NewReleaseGroup.vue b/src/components/list/NewReleaseGroup.vue new file mode 100644 index 000000000..6034903c0 --- /dev/null +++ b/src/components/list/NewReleaseGroup.vue @@ -0,0 +1,191 @@ + + + diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index 4d9a70701..d16613a93 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json @@ -189,6 +189,7 @@ "history": "Play History", "download": "Downloads", "favorites": "Favorites", + "newReleases": "New Releases", "cloud": "My Cloud", "streaming": "Streaming", "reload": "Reload", @@ -637,6 +638,24 @@ "empty": "No daily recommendations available", "needLogin": "Sign in to see daily recommendations" }, + "newReleases": { + "title": "New Releases", + "tagline": "New music released in the last three months by artists you follow", + "playLatest": "Play Latest 50", + "refresh": "Refresh releases", + "empty": "No new songs from followed artists", + "noResults": "No matching releases", + "needLogin": "Sign in to view new songs from artists you follow", + "loadFailed": "Failed to load new releases. Please try again later", + "playFailed": "Failed to load the play queue. Please try again later", + "releaseAlbum": "released an album on {date}", + "releaseSong": "released a song on {date}", + "trackCount": "{count} tracks", + "expand": "Show {count} more", + "collapse": "Collapse", + "loadMore": "Load More", + "playRelease": "Play this release" + }, "favorites": { "title": "Favorites", "empty": "Nothing here yet — go explore and save something you like", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 5bf0937ab..94172b2ac 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -177,6 +177,7 @@ "history": "播放历史", "download": "下载管理", "favorites": "我的收藏", + "newReleases": "新歌发布", "cloud": "我的云盘", "streaming": "流媒体", "reload": "热重载", @@ -625,6 +626,24 @@ "empty": "暂无每日推荐内容", "needLogin": "登录后查看每日推荐" }, + "newReleases": { + "title": "新歌发布", + "tagline": "近三个月内,所有关注歌手发布的新歌", + "playLatest": "播放最新50", + "refresh": "刷新新歌", + "empty": "暂无关注歌手的新歌", + "noResults": "没有匹配的新歌发布", + "needLogin": "登录后查看关注歌手的新歌", + "loadFailed": "新歌加载失败,请稍后重试", + "playFailed": "播放列表获取失败,请稍后重试", + "releaseAlbum": "{date} 上线专辑", + "releaseSong": "{date} 上线新歌", + "trackCount": "共 {count} 首", + "expand": "展开其余 {count} 首", + "collapse": "收起", + "loadMore": "加载更多", + "playRelease": "播放此发布" + }, "favorites": { "title": "我的收藏", "empty": "这里还空空如也,去发现一些喜欢的内容吧", diff --git a/src/pages/NewReleases.vue b/src/pages/NewReleases.vue new file mode 100644 index 000000000..1d6d1baa4 --- /dev/null +++ b/src/pages/NewReleases.vue @@ -0,0 +1,205 @@ + + + diff --git a/src/router/index.ts b/src/router/index.ts index 173be534e..0cc3b1456 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -47,6 +47,11 @@ const router = createRouter({ name: "daily", component: () => import("@/pages/Daily.vue"), }, + { + path: "new-releases", + name: "new-releases", + component: () => import("@/pages/NewReleases.vue"), + }, { path: "favorites", name: "favorites", From ae448dd0e66d0854be093228bba2bd85b7775f97 Mon Sep 17 00:00:00 2001 From: LaoShui <79132480+laoshuikaixue@users.noreply.github.com> Date: Fri, 10 Jul 2026 19:55:04 +0800 Subject: [PATCH 2/3] =?UTF-8?q?refactor(SideBar):=20=E7=AE=80=E5=8C=96?= =?UTF-8?q?=E8=B7=AF=E7=94=B1=E8=B7=B3=E8=BD=AC=E6=96=B9=E6=B3=95=E8=B0=83?= =?UTF-8?q?=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除了多余的括号和换行符 - 优化了代码格式以提高可读性 --- src/components/layout/SideBar.vue | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/layout/SideBar.vue b/src/components/layout/SideBar.vue index ad142de83..fd97a756d 100644 --- a/src/components/layout/SideBar.vue +++ b/src/components/layout/SideBar.vue @@ -59,9 +59,7 @@ const handleCreate = (): void => { /** 新建成功后跳转到该歌单 */ const handleCreated = (playlistId: string, scope: ContentScope): void => { - router.push( - `/collection/${scope === "local" ? "local" : "netease"}/playlist/${playlistId}`, - ); + router.push(`/collection/${scope === "local" ? "local" : "netease"}/playlist/${playlistId}`); }; /** 我的歌单分组头部 */ From 5b212d64741bfcac4f27cc1a25d4622933c86996 Mon Sep 17 00:00:00 2001 From: LaoShui <79132480+laoshuikaixue@users.noreply.github.com> Date: Fri, 10 Jul 2026 20:05:22 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(NewReleaseGroup):=20=E5=AF=B9=E9=BD=90?= =?UTF-8?q?=E6=AD=8C=E6=9B=B2=E6=97=B6=E9=95=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/list/NewReleaseGroup.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/list/NewReleaseGroup.vue b/src/components/list/NewReleaseGroup.vue index 6034903c0..d93790e77 100644 --- a/src/components/list/NewReleaseGroup.vue +++ b/src/components/list/NewReleaseGroup.vue @@ -136,7 +136,7 @@ watch( {{ formatTime(singleTrack.duration) }}