Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions shared/types/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ export interface Track {
artists: Artist[];
/** 专辑 */
album?: Album;
/** 碟号或碟片名称 */
disc?: string;
/** 曲目编号 */
track?: number;
/** 时长(毫秒) */
Expand Down
59 changes: 57 additions & 2 deletions src/components/list/SongList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,20 @@ const filteredItems = computed(() => {
});
});

/** 默认顺序下按碟片分组,搜索仍保留原专辑的分碟结构 */
const discGroups = computed(() => {
if (
props.collectionType !== "album" ||
(props.enableSort && sortField.value !== "none") ||
new Set(props.items.map((track) => track.disc ?? "1")).size < 2
)
return [];
return [...Map.groupBy(filteredItems.value, (track) => track.disc ?? "1")];
});

/** 排序后列表 */
const sortedItems = computed(() => {
if (discGroups.value.length) return discGroups.value.flatMap(([, tracks]) => tracks);
if (!props.enableSort || sortField.value === "none") return filteredItems.value;
const result = [...filteredItems.value];
const field = sortField.value;
Expand Down Expand Up @@ -234,6 +246,27 @@ const sortedItems = computed(() => {
return result;
});

/** 标题附在每碟第一首歌上,歌曲索引和播放队列保持一致 */
const discHeaders = computed(() => {
const headers: Record<number, { disc: string; count: number }> = {};
let index = 0;
for (const [disc, tracks] of discGroups.value) {
headers[index] = { disc, count: tracks.length };
index += tracks.length;
}
return headers;
});

/** 使用完整专辑计算碟内曲序,搜索时保持原编号 */
const discTrackNumbers = computed(
() =>
new Map(
[...Map.groupBy(props.items, (track) => track.disc ?? "1").values()].flatMap((tracks) =>
tracks.map((track, index) => [track.id, index + 1] as const),
),
),
);

/** 当前播放歌曲在过滤后列表中的索引 */
const playingIndex = computed(() => {
if (!playingId.value) return -1;
Expand Down Expand Up @@ -421,7 +454,10 @@ defineExpose({
:item-height="isCompactList ? 56 : 88"
:padding-bottom="isFloatingPlayerBar ? PLAYER_BAR_GAP : 80"
:get-item-key="(item: Track) => item.id"
item-fixed
:item-fixed="!discGroups.length"
:get-item-height="
(_item: Track, index: number) => (isCompactList ? 56 : 88) + (discHeaders[index] ? 40 : 0)
"
height="100%"
@scroll="onScroll"
@reach-bottom="emit('reachBottom')"
Expand Down Expand Up @@ -618,6 +654,25 @@ defineExpose({
</template>
<!-- 列表项 -->
<template #default="{ item, index }: { item: Track; index: number }">
<div
v-if="discHeaders[index]"
class="mx-6 h-10 flex items-center gap-3 text-on-surface-variant"
:class="!isCompactList && index > 0 ? 'pb-3' : ''"
>
<IconLucideDisc3
class="size-4 translate-y-px shrink-0 text-primary/70"
aria-hidden="true"
/>
<h2
class="min-w-0 flex-1 truncate text-sm font-semibold"
:title="discHeaders[index].disc"
>
{{ t("collection.disc", { disc: discHeaders[index].disc }) }}
</h2>
<span class="shrink-0 text-xs tabular-nums text-on-surface-variant/50">
{{ t("common.totalSongs", { count: discHeaders[index].count }) }}
</span>
</div>
<div :class="isCompactList ? 'px-3' : 'px-3 pb-3'">
<div
data-song-item
Expand Down Expand Up @@ -654,7 +709,7 @@ defineExpose({
class="font-bold tabular-nums group-hover:opacity-0 transition-opacity duration-300"
:class="isCompactList ? 'text-xs' : 'text-sm'"
>
{{ index + 1 }}
{{ discGroups.length ? discTrackNumbers.get(item.id) : index + 1 }}
</span>
<IconLucideMusic
v-else
Expand Down
13 changes: 9 additions & 4 deletions src/components/ui/SVirtualList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export interface SVirtualListProps<T> {
itemHeight: number;
/** 定高模式(跳过 DOM 测量,性能更佳) */
itemFixed?: boolean;
/** 不等高模式下提供精确行高,跳过 DOM 测量 */
getItemHeight?: (item: T, index: number) => number;
/** 容器高度,支持 CSS 值 */
height?: number | string;
/** 顶部内边距(px) */
Expand Down Expand Up @@ -71,11 +73,13 @@ const viewportHeight = computed(() => scrollViewportHeight.value || 0);
const itemHeights = shallowRef<number[]>([]);
const itemTops = shallowRef<number[]>([]);

/** 初始化高度数组,复用已测量的值 */
/** 初始化不等高列表的精确行高或复用已测量的值 */
const initializeHeights = (): void => {
if (props.itemFixed) return;
const length = props.items.length;
if (itemHeights.value.length !== length) {
if (props.getItemHeight) {
itemHeights.value = props.items.map(props.getItemHeight);
} else if (itemHeights.value.length !== length) {
const old = itemHeights.value;
itemHeights.value = Array.from({ length }, (_, idx) => old[idx] || props.itemHeight);
}
Expand Down Expand Up @@ -174,7 +178,8 @@ const visibleItems = computed(() => {

/** 测量可见项的真实 DOM 高度,允许 0.5px 误差 */
const measureItemHeights = (): void => {
if (props.itemFixed || !itemRefs.value.length || props.items.length === 0) return;
if (props.itemFixed || props.getItemHeight || !itemRefs.value.length || props.items.length === 0)
return;
let hasChanges = false;
itemRefs.value.forEach((element) => {
if (!element) return;
Expand Down Expand Up @@ -285,7 +290,7 @@ const scrollToIndex = (index: number, behavior: ScrollBehavior = "auto"): void =
const getScrollTop = (): number => scrollTop.value;

watch(
() => props.items,
() => [props.items, props.itemHeight, props.itemFixed, props.getItemHeight],
() => {
initializeHeights();
calculateVisibleRange(scrollTop.value);
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -2265,6 +2265,7 @@
}
},
"collection": {
"disc": "Disc {disc}",
"totalDuration": "About {time}",
"unknownAlbum": "Unknown Album",
"empty": "No songs",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -2265,6 +2265,7 @@
}
},
"collection": {
"disc": "Disc {disc}",
"totalDuration": "约 {time}",
"unknownAlbum": "未知专辑",
"empty": "暂无歌曲",
Expand Down
2 changes: 2 additions & 0 deletions src/utils/format/netease.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ export const songToTrack = (song: NeteaseSong): Track => {
.filter((artist) => artist.name?.trim())
.map((artist) => ({ id: String(artist.id), name: artist.name.trim() })),
album: album ? { id: String(album.id), name: album.name, cover } : undefined,
disc: song.cd?.trim() || undefined,
track: song.no && song.no > 0 ? song.no : undefined,
duration: song.dt ?? song.duration ?? 0,
cover,
coverOriginal,
Expand Down