mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-07 02:46:55 +08:00
feat: 更新 SongListView 和 TraditionalSongListTemplate 组件,增强加载逻辑和排序功能
- 在 SongListView 中优化了加载状态管理,增加了数据和配置加载的分离处理。 - 更新了 TraditionalSongListTemplate 组件,新增排序功能,支持按歌名、歌手、语言等字段排序。 - 改进了歌曲筛选逻辑,支持多条件过滤和排序,提升用户体验。 - 修复歌单加载时闪烁的问题
This commit is contained in:
@@ -1,12 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<NSpin
|
<NSpin :show="isLoading">
|
||||||
v-if="isLoading"
|
|
||||||
show
|
|
||||||
/>
|
|
||||||
<component
|
<component
|
||||||
:is="selectedTemplate?.component"
|
:is="selectedTemplate?.component"
|
||||||
v-else
|
|
||||||
ref="dynamicConfigRef"
|
ref="dynamicConfigRef"
|
||||||
:config="selectedTemplate?.settingName ? currentConfig : undefined"
|
:config="selectedTemplate?.settingName ? currentConfig : undefined"
|
||||||
:user-info="userInfo"
|
:user-info="userInfo"
|
||||||
@@ -17,6 +13,7 @@
|
|||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
@request-song="requestSong"
|
@request-song="requestSong"
|
||||||
/>
|
/>
|
||||||
|
</NSpin>
|
||||||
<NButton
|
<NButton
|
||||||
v-if="selectedTemplate?.settingName && userInfo?.id == accountInfo.id"
|
v-if="selectedTemplate?.settingName && userInfo?.id == accountInfo.id"
|
||||||
type="info"
|
type="info"
|
||||||
@@ -49,8 +46,9 @@ import { SONG_API_URL, SONG_REQUEST_API_URL, SongListTemplateMap } from '@/data/
|
|||||||
import { ConfigItemDefinition } from '@/data/VTsuruTypes';
|
import { ConfigItemDefinition } from '@/data/VTsuruTypes';
|
||||||
import { useStorage } from '@vueuse/core';
|
import { useStorage } from '@vueuse/core';
|
||||||
import { addSeconds } from 'date-fns';
|
import { addSeconds } from 'date-fns';
|
||||||
import { NButton, NModal, NSpin, useMessage } from 'naive-ui';
|
import { NButton, NModal, NSpin, useMessage, NFlex, NIcon, NInput, NInputGroup, NInputGroupLabel, NTag, NTooltip, NSelect, NSpace } from 'naive-ui';
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
|
import { GetGuardColor, getUserAvatarUrl, isDarkMode } from '@/Utils';
|
||||||
|
|
||||||
const accountInfo = useAccount();
|
const accountInfo = useAccount();
|
||||||
const nextRequestTime = useStorage('SongList.NextRequestTime', new Date());
|
const nextRequestTime = useStorage('SongList.NextRequestTime', new Date());
|
||||||
@@ -83,14 +81,11 @@ import { computed, onMounted, ref, watch } from 'vue';
|
|||||||
return SongListTemplateMap[''];
|
return SongListTemplateMap[''];
|
||||||
});
|
});
|
||||||
const currentConfig = ref();
|
const currentConfig = ref();
|
||||||
watch(
|
|
||||||
() => dynamicConfigRef,
|
|
||||||
() => {
|
|
||||||
getConfig();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const isLoading = ref(true);
|
const isDataLoading = ref(true);
|
||||||
|
const isConfigLoading = ref(true);
|
||||||
|
const isLoading = computed(() => isDataLoading.value || isConfigLoading.value);
|
||||||
|
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
|
|
||||||
const errMessage = ref('');
|
const errMessage = ref('');
|
||||||
@@ -112,7 +107,7 @@ import { computed, onMounted, ref, watch } from 'vue';
|
|||||||
return {} as { songs: SongRequestInfo[]; setting: Setting_LiveRequest; };
|
return {} as { songs: SongRequestInfo[]; setting: Setting_LiveRequest; };
|
||||||
}
|
}
|
||||||
async function getSongs() {
|
async function getSongs() {
|
||||||
isLoading.value = true;
|
isDataLoading.value = true;
|
||||||
await QueryGetAPI<SongsInfo[]>(SONG_API_URL + 'get', {
|
await QueryGetAPI<SongsInfo[]>(SONG_API_URL + 'get', {
|
||||||
id: props.userInfo?.id,
|
id: props.userInfo?.id,
|
||||||
})
|
})
|
||||||
@@ -128,27 +123,30 @@ import { computed, onMounted, ref, watch } from 'vue';
|
|||||||
message.error('加载失败: ' + err);
|
message.error('加载失败: ' + err);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
isLoading.value = false;
|
isDataLoading.value = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async function getConfig() {
|
async function getConfig() {
|
||||||
if (!selectedTemplateConfig.value || !selectedTemplate.value!.settingName) return;
|
if (!selectedTemplateConfig.value || !selectedTemplate.value!.settingName) {
|
||||||
isLoading.value = true;
|
if (!selectedTemplate.value!.settingName) {
|
||||||
await DownloadConfig(selectedTemplate.value!.settingName, props.userInfo?.id)
|
isConfigLoading.value = false;
|
||||||
.then((data) => {
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isConfigLoading.value = true;
|
||||||
|
try {
|
||||||
|
const data = await DownloadConfig(selectedTemplate.value!.settingName, props.userInfo?.id);
|
||||||
if (data.msg) {
|
if (data.msg) {
|
||||||
//message.error('加载失败: ' + data.msg);
|
currentConfig.value = dynamicConfigRef.value?.DefaultConfig ?? {};
|
||||||
console.log('当前模板没有配置, 使用默认配置');
|
|
||||||
} else {
|
} else {
|
||||||
currentConfig.value = data.data;
|
currentConfig.value = data.data;
|
||||||
}
|
}
|
||||||
})
|
} catch (err) {
|
||||||
.catch((err) => {
|
message.error('加载配置失败: ' + err);
|
||||||
message.error('加载失败: ' + err);
|
} finally {
|
||||||
})
|
isConfigLoading.value = false;
|
||||||
.finally(() => {
|
}
|
||||||
isLoading.value = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
async function requestSong(song: SongsInfo) {
|
async function requestSong(song: SongsInfo) {
|
||||||
if (song.options || !settings.value.allowFromWeb || (settings.value.allowFromWeb && !settings.value.allowAnonymousFromWeb)) {
|
if (song.options || !settings.value.allowFromWeb || (settings.value.allowFromWeb && !settings.value.allowAnonymousFromWeb)) {
|
||||||
@@ -186,25 +184,41 @@ import { computed, onMounted, ref, watch } from 'vue';
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => dynamicConfigRef.value,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue?.Config) {
|
||||||
|
getConfig();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: false }
|
||||||
|
);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
isDataLoading.value = true;
|
||||||
if (!props.fakeData) {
|
if (!props.fakeData) {
|
||||||
try {
|
try {
|
||||||
await getSongs();
|
await getSongs();
|
||||||
setTimeout(async () => {
|
|
||||||
const r = await getSongRequestInfo();
|
const r = await getSongRequestInfo();
|
||||||
if (r) {
|
if (r) {
|
||||||
songsActive.value = r.songs;
|
songsActive.value = r.songs;
|
||||||
settings.value = r.setting;
|
settings.value = r.setting;
|
||||||
}
|
}
|
||||||
await getConfig();
|
|
||||||
}, 300);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
message.error('加载失败: ' + err);
|
message.error('加载失败: ' + err);
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
isDataLoading.value = false;
|
||||||
|
isConfigLoading.value = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
currentData.value = props.fakeData;
|
currentData.value = props.fakeData;
|
||||||
isLoading.value = false;
|
isDataLoading.value = false;
|
||||||
|
isConfigLoading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedTemplate.value?.settingName) {
|
||||||
|
isConfigLoading.value = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, h, ref, watch } from 'vue';
|
import { computed, h, ref, watch, VNode } from 'vue';
|
||||||
import { getUserAvatarUrl, isDarkMode } from '@/Utils';
|
import { getUserAvatarUrl, isDarkMode } from '@/Utils';
|
||||||
import { SongListConfigTypeWithConfig } from '@/data/TemplateTypes';
|
import { SongListConfigTypeWithConfig } from '@/data/TemplateTypes';
|
||||||
import { defineTemplateConfig, ExtractConfigData } from '@/data/VTsuruTypes';
|
import { defineTemplateConfig, ExtractConfigData } from '@/data/VTsuruTypes';
|
||||||
@@ -9,9 +9,10 @@ import bilibili from '@/svgs/bilibili.svg';
|
|||||||
import neteaseMusic from '@/svgs/neteaseMusic.svg';
|
import neteaseMusic from '@/svgs/neteaseMusic.svg';
|
||||||
import qqMusic from '@/svgs/qqMusic.svg';
|
import qqMusic from '@/svgs/qqMusic.svg';
|
||||||
import douyin from '@/svgs/douyin.svg';
|
import douyin from '@/svgs/douyin.svg';
|
||||||
import { SongFrom, SongsInfo } from '@/api/api-models';
|
import { SongFrom, SongsInfo, SongRequestOption } from '@/api/api-models';
|
||||||
import FiveSingIcon from '@/svgs/fivesing.svg';
|
import FiveSingIcon from '@/svgs/fivesing.svg';
|
||||||
import { SquareArrowForward24Filled, ArrowCounterclockwise20Filled } from '@vicons/fluent'; // Import clear icon
|
import { SquareArrowForward24Filled, ArrowCounterclockwise20Filled, ArrowSortDown20Filled, ArrowSortUp20Filled } from '@vicons/fluent';
|
||||||
|
import { List } from 'linqts';
|
||||||
|
|
||||||
// Interface Tab - can be reused for both language and tag buttons
|
// Interface Tab - can be reused for both language and tag buttons
|
||||||
interface FilterButton {
|
interface FilterButton {
|
||||||
@@ -31,6 +32,11 @@ const selectedTag = ref<string | undefined>(); // Renamed from activeTab for cla
|
|||||||
const searchQuery = ref<string>('');
|
const searchQuery = ref<string>('');
|
||||||
const selectedArtist = ref<string | null>(null);
|
const selectedArtist = ref<string | null>(null);
|
||||||
|
|
||||||
|
// --- New: Sorting State ---
|
||||||
|
type SortKey = 'name' | 'author' | 'language' | 'tags' | 'options' | 'description' | null;
|
||||||
|
const sortKey = ref<SortKey>(null); // 当前排序列
|
||||||
|
const sortOrder = ref<'asc' | 'desc'>('asc'); // 当前排序顺序
|
||||||
|
|
||||||
// --- Computed Properties for Filter Buttons ---
|
// --- Computed Properties for Filter Buttons ---
|
||||||
|
|
||||||
// Extract unique languages
|
// Extract unique languages
|
||||||
@@ -90,39 +96,78 @@ const artistOptions = computed(() => {
|
|||||||
return allArtists.value.map(artist => ({ label: artist, value: artist }));
|
return allArtists.value.map(artist => ({ label: artist, value: artist }));
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Updated Filtered Songs Logic ---
|
// --- Updated Filtered & Sorted Songs Logic using linq-ts ---
|
||||||
const filteredSongs = computed(() => {
|
const filteredAndSortedSongs = computed(() => {
|
||||||
let songs = props.data!;
|
if (!props.data) return [];
|
||||||
if (!songs) return [];
|
|
||||||
|
let query = new List<SongsInfo>(props.data);
|
||||||
|
|
||||||
// 1. Filter by Selected Language
|
// 1. Filter by Selected Language
|
||||||
if (selectedLanguage.value) {
|
if (selectedLanguage.value) {
|
||||||
songs = songs.filter(song => song.language?.includes(selectedLanguage.value!));
|
const lang = selectedLanguage.value;
|
||||||
|
query = query.Where(song => song.language?.includes(lang));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Filter by Selected Tag
|
// 2. Filter by Selected Tag
|
||||||
if (selectedTag.value) {
|
if (selectedTag.value) {
|
||||||
songs = songs.filter(song => song.tags?.includes(selectedTag.value!));
|
const tag = selectedTag.value;
|
||||||
|
query = query.Where(song => song.tags?.includes(tag) ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Filter by Selected Artist
|
// 3. Filter by Selected Artist
|
||||||
if (selectedArtist.value) {
|
if (selectedArtist.value) {
|
||||||
songs = songs.filter(song => song.author?.includes(selectedArtist.value!));
|
const artist = selectedArtist.value;
|
||||||
|
query = query.Where(song => song.author?.includes(artist));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Filter by Search Query (case-insensitive, including tags)
|
// 4. Filter by Search Query (case-insensitive, including tags)
|
||||||
if (searchQuery.value.trim()) {
|
if (searchQuery.value.trim()) {
|
||||||
const lowerSearch = searchQuery.value.toLowerCase().trim();
|
const lowerSearch = searchQuery.value.toLowerCase().trim();
|
||||||
songs = songs.filter(song =>
|
query = query.Where(song =>
|
||||||
song.name.toLowerCase().includes(lowerSearch) ||
|
song.name.toLowerCase().includes(lowerSearch) ||
|
||||||
song.author?.some(artist => artist.toLowerCase().includes(lowerSearch)) ||
|
(song.author?.some(a => a.toLowerCase().includes(lowerSearch)) ?? false) ||
|
||||||
song.language?.some(lang => lang.toLowerCase().includes(lowerSearch)) ||
|
(song.language?.some(l => l.toLowerCase().includes(lowerSearch)) ?? false) ||
|
||||||
song.tags?.some(tag => tag.toLowerCase().includes(lowerSearch)) || // Added tags to search
|
(song.tags?.some(t => t.toLowerCase().includes(lowerSearch)) ?? false) ||
|
||||||
song.description?.toLowerCase().includes(lowerSearch)
|
(song.description?.toLowerCase().includes(lowerSearch) ?? false)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return songs;
|
// 5. Sort the filtered songs using linq-ts
|
||||||
|
if (sortKey.value) {
|
||||||
|
const key = sortKey.value;
|
||||||
|
|
||||||
|
// Define selector function for linq-ts
|
||||||
|
const keySelector = (song: SongsInfo): any => {
|
||||||
|
if (key === 'options') {
|
||||||
|
// Prefer sorting by specific conditions first if needed, then by presence
|
||||||
|
// Example: Sort by 'needZongdu' first if key is 'options'
|
||||||
|
// For simplicity, just sorting by presence (1) vs absence (0)
|
||||||
|
return song.options ? 1 : 0;
|
||||||
|
}
|
||||||
|
let val = song[key];
|
||||||
|
// Handle potential array values for sorting (simple join)
|
||||||
|
if (Array.isArray(val)) return val.join('').toLowerCase(); // Lowercase for consistent string sort
|
||||||
|
// Handle strings and other types, provide default for null/undefined
|
||||||
|
return (typeof val === 'string' ? val.toLowerCase() : val) ?? '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define a stable secondary sort key selector
|
||||||
|
const secondaryKeySelector = (song: SongsInfo): string | number => {
|
||||||
|
return song.id ?? (song.name + '-' + (song.author?.join('/') ?? '')); // Use ID or fallback key
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sortOrder.value === 'asc') {
|
||||||
|
query = query.OrderBy(keySelector).ThenBy(secondaryKeySelector); // Add ThenBy for stability
|
||||||
|
} else {
|
||||||
|
query = query.OrderByDescending(keySelector).ThenBy(secondaryKeySelector); // Add ThenBy for stability
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// else if no primary sort key, maybe apply a default sort? e.g., by name
|
||||||
|
// else {
|
||||||
|
// query = query.OrderBy(s => s.name);
|
||||||
|
// }
|
||||||
|
|
||||||
|
return query.ToArray(); // Get the final array
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Methods ---
|
// --- Methods ---
|
||||||
@@ -145,9 +190,22 @@ const selectTag = (tagName: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Select Artist (from table click, unchanged)
|
// Select Artist (from table click, updated to allow deselect)
|
||||||
const selectArtistFromTable = (artist: string) => {
|
const selectArtistFromTable = (artist: string) => {
|
||||||
selectedArtist.value = artist;
|
if (selectedArtist.value === artist) {
|
||||||
|
selectedArtist.value = null; // Deselect if clicking the already selected artist
|
||||||
|
} else {
|
||||||
|
selectedArtist.value = artist; // Select the new artist
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Select Language (from table click, allows deselect)
|
||||||
|
const selectLanguageFromTable = (lang: string) => {
|
||||||
|
if (selectedLanguage.value === lang) {
|
||||||
|
selectedLanguage.value = undefined; // Use undefined based on existing filter logic
|
||||||
|
} else {
|
||||||
|
selectedLanguage.value = lang;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- New: Clear All Filters ---
|
// --- New: Clear All Filters ---
|
||||||
@@ -158,6 +216,35 @@ const clearFilters = () => {
|
|||||||
searchQuery.value = '';
|
searchQuery.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Updated Sorting Method ---
|
||||||
|
const handleSort = (key: SortKey) => {
|
||||||
|
if (sortKey.value === key) {
|
||||||
|
// Cycle through asc -> desc -> null (clear sort)
|
||||||
|
if (sortOrder.value === 'asc') {
|
||||||
|
sortOrder.value = 'desc';
|
||||||
|
} else {
|
||||||
|
// If already desc, clear the sort
|
||||||
|
sortKey.value = null;
|
||||||
|
// Optional: Reset sortOrder, though it doesn't matter when sortKey is null
|
||||||
|
// sortOrder.value = 'asc';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Set new key and default to ascending order
|
||||||
|
sortKey.value = key;
|
||||||
|
sortOrder.value = 'asc';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Updated Helper function for Sort Icons ---
|
||||||
|
const getSortIcon = (key: SortKey) => {
|
||||||
|
if (sortKey.value !== key) {
|
||||||
|
// Show inactive sort icon (down arrow as placeholder)
|
||||||
|
return h(NIcon, { component: ArrowSortDown20Filled, style: { opacity: 0.3, marginLeft: '4px', verticalAlign: 'middle' } });
|
||||||
|
}
|
||||||
|
// Show active sort icon (up or down)
|
||||||
|
return h(NIcon, { component: sortOrder.value === 'asc' ? ArrowSortUp20Filled : ArrowSortDown20Filled, style: { marginLeft: '4px', verticalAlign: 'middle' } });
|
||||||
|
// Note: We don't need a specific 'clear' icon here, as clicking 'desc' clears the sort and the icon reverts to inactive.
|
||||||
|
};
|
||||||
|
|
||||||
// Watcher for artist selection (unchanged, good practice)
|
// Watcher for artist selection (unchanged, good practice)
|
||||||
watch(allArtists, (newArtists) => {
|
watch(allArtists, (newArtists) => {
|
||||||
@@ -166,9 +253,8 @@ watch(allArtists, (newArtists) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const randomOrder = () => {
|
const randomOrder = () => {
|
||||||
const songsToChooseFrom = filteredSongs.value.length > 0 ? filteredSongs.value : props.data ?? [];
|
const songsToChooseFrom = filteredAndSortedSongs.value.length > 0 ? filteredAndSortedSongs.value : props.data ?? [];
|
||||||
if (songsToChooseFrom.length === 0) {
|
if (songsToChooseFrom.length === 0) {
|
||||||
window.$message?.warning('歌单为空或当前筛选无结果,无法随机点歌');
|
window.$message?.warning('歌单为空或当前筛选无结果,无法随机点歌');
|
||||||
return;
|
return;
|
||||||
@@ -272,6 +358,38 @@ function GetPlayButton(song: SongsInfo) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- New: Helper function for Song Request Options ---
|
||||||
|
function getOptionDisplay(options?: SongRequestOption) {
|
||||||
|
if (!options) {
|
||||||
|
return h('span', '无特殊要求');
|
||||||
|
}
|
||||||
|
|
||||||
|
const conditions: VNode[] = [];
|
||||||
|
|
||||||
|
if (options.needJianzhang) {
|
||||||
|
conditions.push(h(NTag, { size: 'small', type: 'info', style: { marginRight: '4px', marginBottom: '2px'} }, () => '舰长'));
|
||||||
|
}
|
||||||
|
if (options.needTidu) {
|
||||||
|
conditions.push(h(NTag, { size: 'small', type: 'warning', style: { marginRight: '4px', marginBottom: '2px'} }, () => '提督'));
|
||||||
|
}
|
||||||
|
if (options.needZongdu) {
|
||||||
|
conditions.push(h(NTag, { size: 'small', type: 'error', style: { marginRight: '4px', marginBottom: '2px'} }, () => '总督'));
|
||||||
|
}
|
||||||
|
if (options.fanMedalMinLevel && options.fanMedalMinLevel > 0) {
|
||||||
|
conditions.push(h(NTag, { size: 'small', type: 'success', style: { marginRight: '4px', marginBottom: '2px'} }, () => `粉丝牌 ${options.fanMedalMinLevel}级`));
|
||||||
|
}
|
||||||
|
if (options.scMinPrice && options.scMinPrice > 0) {
|
||||||
|
conditions.push(h(NTag, { size: 'small', color: { color: '#E85A4F', textColor: '#fff' }, style: { marginRight: '4px', marginBottom: '2px'} }, () => `SC ¥${options.scMinPrice}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conditions.length === 0) {
|
||||||
|
return h('span', '无特殊要求');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use NFlex for better wrapping
|
||||||
|
return h(NFlex, { size: 4, wrap: true, style: { gap: '4px' } }, () => conditions);
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -652,33 +770,64 @@ export const Config = defineTemplateConfig([
|
|||||||
<table class="song-list-table">
|
<table class="song-list-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>歌名</th>
|
<th
|
||||||
<th>歌手</th>
|
style="cursor: pointer;"
|
||||||
<th>语言</th>
|
@click="handleSort('name')"
|
||||||
<th>标签</th>
|
>
|
||||||
<th>备注</th>
|
歌名 <component :is="getSortIcon('name')" />
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
style="cursor: pointer;"
|
||||||
|
@click="handleSort('author')"
|
||||||
|
>
|
||||||
|
歌手 <component :is="getSortIcon('author')" />
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
style="cursor: pointer;"
|
||||||
|
@click="handleSort('language')"
|
||||||
|
>
|
||||||
|
语言 <component :is="getSortIcon('language')" />
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
style="cursor: pointer;"
|
||||||
|
@click="handleSort('tags')"
|
||||||
|
>
|
||||||
|
标签 <component :is="getSortIcon('tags')" />
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
style="cursor: pointer;"
|
||||||
|
@click="handleSort('options')"
|
||||||
|
>
|
||||||
|
点歌条件 <component :is="getSortIcon('options')" />
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
style="cursor: pointer;"
|
||||||
|
@click="handleSort('description')"
|
||||||
|
>
|
||||||
|
备注 <component :is="getSortIcon('description')" />
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-if="!props.data || props.data.length === 0">
|
<tr v-if="!props.data || props.data.length === 0">
|
||||||
<td
|
<td
|
||||||
colspan="5"
|
colspan="6"
|
||||||
class="no-results"
|
class="no-results"
|
||||||
>
|
>
|
||||||
歌单里还没有歌曲哦~
|
歌单里还没有歌曲哦~
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-else-if="filteredSongs.length === 0">
|
<tr v-else-if="filteredAndSortedSongs.length === 0">
|
||||||
<td
|
<td
|
||||||
colspan="5"
|
colspan="6"
|
||||||
class="no-results"
|
class="no-results"
|
||||||
>
|
>
|
||||||
当前筛选条件下暂无匹配歌曲
|
当前筛选条件下暂无匹配歌曲
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr
|
<tr
|
||||||
v-for="song in filteredSongs"
|
v-for="song in filteredAndSortedSongs"
|
||||||
:key="song.id || (song.name + '-' + song.author?.join('/'))"
|
:key="song.key || (song.name + '-' + song.author?.join('/'))"
|
||||||
:style="{
|
:style="{
|
||||||
textShadow: isDarkMode ? '0px 1px 2px rgba(0, 0, 0, 0.4)' : '0px 1px 2px rgba(255, 255, 255, 0.4)',
|
textShadow: isDarkMode ? '0px 1px 2px rgba(0, 0, 0, 0.4)' : '0px 1px 2px rgba(255, 255, 255, 0.4)',
|
||||||
}"
|
}"
|
||||||
@@ -704,6 +853,7 @@ export const Config = defineTemplateConfig([
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="artist-link"
|
class="artist-link"
|
||||||
|
:class="{ 'selected-artist': selectedArtist === artist }"
|
||||||
:title="`筛选: ${artist}`"
|
:title="`筛选: ${artist}`"
|
||||||
@click.stop="selectArtistFromTable(artist)"
|
@click.stop="selectArtistFromTable(artist)"
|
||||||
>
|
>
|
||||||
@@ -715,7 +865,26 @@ export const Config = defineTemplateConfig([
|
|||||||
</span>
|
</span>
|
||||||
<span v-else>未知</span>
|
<span v-else>未知</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ song.language?.join(', ') ?? '未知' }}</td>
|
<td>
|
||||||
|
<span v-if="song.language && song.language.length > 0">
|
||||||
|
<span
|
||||||
|
v-for="(lang, index) in song.language"
|
||||||
|
:key="lang"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="language-link"
|
||||||
|
:class="{ 'selected-language': selectedLanguage === lang }"
|
||||||
|
:title="`筛选: ${lang}`"
|
||||||
|
@click.stop="selectLanguageFromTable(lang)"
|
||||||
|
>
|
||||||
|
{{ lang }}
|
||||||
|
</span>
|
||||||
|
<!-- Add separator only if not the last language -->
|
||||||
|
<span v-if="index < song.language.length - 1">, </span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span v-else>未知</span>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<n-flex
|
<n-flex
|
||||||
:size="4"
|
:size="4"
|
||||||
@@ -736,6 +905,9 @@ export const Config = defineTemplateConfig([
|
|||||||
<span v-if="!song.tags || song.tags.length === 0">无标签</span>
|
<span v-if="!song.tags || song.tags.length === 0">无标签</span>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<component :is="getOptionDisplay(song.options)" />
|
||||||
|
</td>
|
||||||
<td>{{ song.description }}</td>
|
<td>{{ song.description }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -1133,6 +1305,7 @@ html.dark .song-list-container {
|
|||||||
backdrop-filter: blur(2px); /* Blur header slightly */
|
backdrop-filter: blur(2px); /* Blur header slightly */
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
color: #444444;
|
color: #444444;
|
||||||
|
user-select: none; /* Prevent text selection on click */
|
||||||
}
|
}
|
||||||
|
|
||||||
html.dark .song-list-table thead th {
|
html.dark .song-list-table thead th {
|
||||||
@@ -1141,11 +1314,12 @@ html.dark .song-list-table thead th {
|
|||||||
color: var(--text-color-2);
|
color: var(--text-color-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.song-list-table th:nth-child(1) { width: 25%; } /* Song Name */
|
.song-list-table th:nth-child(1) { width: 22%; }
|
||||||
.song-list-table th:nth-child(2) { width: 18%; } /* Artist */
|
.song-list-table th:nth-child(2) { width: 15%; }
|
||||||
.song-list-table th:nth-child(3) { width: 12%; } /* Language */
|
.song-list-table th:nth-child(3) { width: 10%; }
|
||||||
.song-list-table th:nth-child(4) { width: 15%; } /* Tags */
|
.song-list-table th:nth-child(4) { width: 13%; }
|
||||||
.song-list-table th:nth-child(5) { width: 30%; } /* Remarks */
|
.song-list-table th:nth-child(5) { width: 15%; }
|
||||||
|
.song-list-table th:nth-child(6) { width: 25%; }
|
||||||
|
|
||||||
|
|
||||||
.song-list-table tbody tr { transition: background-color 0.15s ease; }
|
.song-list-table tbody tr { transition: background-color 0.15s ease; }
|
||||||
@@ -1203,4 +1377,59 @@ html.dark .no-results td { color: var(--text-color-3); }
|
|||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- NEW: Selected Artist Highlight --- */
|
||||||
|
.artist-link.selected-artist {
|
||||||
|
background-color: var(--primary-color-a4); /* 增加背景不透明度 */
|
||||||
|
border: 1px solid var(--primary-color-a6); /* 添加边框 */
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 1px 3px; /* 调整内边距,使边框更明显 */
|
||||||
|
border-radius: 4px; /* 轻微调整圆角 */
|
||||||
|
color: var(--primary-color-dark); /* 亮色模式下使用较深的主题色文字 */
|
||||||
|
/* text-decoration: underline; */ /* 如果需要可以取消注释 */
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .artist-link.selected-artist {
|
||||||
|
background-color: var(--primary-color-a6); /* 增加背景不透明度 */
|
||||||
|
border: 1px solid var(--primary-color-a8); /* 添加边框 */
|
||||||
|
color: var(--primary-color-light); /* 暗色模式下使用亮色文字 */
|
||||||
|
}
|
||||||
|
/* --- END: Selected Artist Highlight --- */
|
||||||
|
|
||||||
|
/* Base style for clickable language */
|
||||||
|
.language-link {
|
||||||
|
padding: 1px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--text-color-2); /* Use theme primary for links */
|
||||||
|
transition: color 0.2s ease, text-decoration 0.2s ease;
|
||||||
|
}
|
||||||
|
.language-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- NEW: Selected Artist/Language Highlight --- */
|
||||||
|
.artist-link.selected-artist,
|
||||||
|
.language-link.selected-language {
|
||||||
|
background-color: var(--primary-color-a4); /* 增加背景不透明度 */
|
||||||
|
border: 1px solid var(--primary-color-a6); /* 添加边框 */
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 1px 3px; /* 调整内边距,使边框更明显 */
|
||||||
|
border-radius: 4px; /* 轻微调整圆角 */
|
||||||
|
color: var(--primary-color-dark); /* 亮色模式下使用较深的主题色文字 */
|
||||||
|
/* text-decoration: underline; */ /* 如果需要可以取消注释 */
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .artist-link.selected-artist,
|
||||||
|
html.dark .language-link.selected-language {
|
||||||
|
background-color: var(--primary-color-a6); /* 增加背景不透明度 */
|
||||||
|
border: 1px solid var(--primary-color-a8); /* 添加边框 */
|
||||||
|
color: var(--primary-color-light); /* 暗色模式下使用亮色文字 */
|
||||||
|
}
|
||||||
|
/* --- END: Selected Artist/Language Highlight --- */
|
||||||
|
|
||||||
|
.song-name :deep(.n-button .n-icon),
|
||||||
|
.song-name :deep(.n-button .svg-icon) {
|
||||||
|
color: currentColor !important; fill: currentColor !important;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
Reference in New Issue
Block a user