diff --git a/src/components/DynamicForm.vue b/src/components/DynamicForm.vue index e8deb2f..d5ae2b1 100644 --- a/src/components/DynamicForm.vue +++ b/src/components/DynamicForm.vue @@ -49,7 +49,7 @@ public: 'true', }); if (resp.code == 200) { - message.success('已保存至服务器'); + message.success('已保存设置'); props.config?.forEach(item => { if (item.type === 'render') { item.onUploaded?.(props.configData[item.key], props.configData); diff --git a/src/components/SongList.vue b/src/components/SongList.vue index 2f3bed1..310de5d 100644 --- a/src/components/SongList.vue +++ b/src/components/SongList.vue @@ -645,15 +645,18 @@ onMounted(() => { - - 批量编辑 - - + + + 批量编辑 + + + { > 添加歌曲 + + 修改展示模板 + { > 刷新 - - 修改模板 - - import { computed, h, ref, watch } from 'vue'; // Import computed and watch - import { getUserAvatarUrl, isDarkMode } from '@/Utils'; - import { SongListConfigTypeWithConfig } from '@/data/TemplateTypes'; - import { defineTemplateConfig, ExtractConfigData } from '@/data/VTsuruTypes'; - import { FILE_BASE_URL } from '@/data/constants'; - import { NButton, NFlex, NIcon, NInput, NInputGroup, NInputGroupLabel, NTag, NTooltip, NSelect } from 'naive-ui'; // Import NSelect - import bilibili from '@/svgs/bilibili.svg'; - import neteaseMusic from '@/svgs/neteaseMusic.svg'; - import qqMusic from '@/svgs/qqMusic.svg'; - import douyin from '@/svgs/douyin.svg'; - import { SongFrom, SongsInfo } from '@/api/api-models'; - import FiveSingIcon from '@/svgs/fivesing.svg'; - import { SquareArrowForward24Filled } from '@vicons/fluent'; +import { computed, h, ref, watch } from 'vue'; +import { getUserAvatarUrl, isDarkMode } from '@/Utils'; +import { SongListConfigTypeWithConfig } from '@/data/TemplateTypes'; +import { defineTemplateConfig, ExtractConfigData } from '@/data/VTsuruTypes'; +import { FILE_BASE_URL } from '@/data/constants'; +import { NButton, NFlex, NIcon, NInput, NInputGroup, NInputGroupLabel, NTag, NTooltip, NSelect } from 'naive-ui'; +import bilibili from '@/svgs/bilibili.svg'; +import neteaseMusic from '@/svgs/neteaseMusic.svg'; +import qqMusic from '@/svgs/qqMusic.svg'; +import douyin from '@/svgs/douyin.svg'; +import { SongFrom, SongsInfo } from '@/api/api-models'; +import FiveSingIcon from '@/svgs/fivesing.svg'; +import { SquareArrowForward24Filled, ArrowCounterclockwise20Filled } from '@vicons/fluent'; // Import clear icon - interface Tab { - id: number; - name: string; +// Interface Tab - can be reused for both language and tag buttons +interface FilterButton { + id: number; + name: string; +} + +const props = defineProps>(); +defineExpose({ Config, DefaultConfig }); +const emits = defineEmits(['requestSong']); + +const isHovering = ref(false); + +// --- State for Filters --- +const selectedLanguage = ref(); +const selectedTag = ref(); // Renamed from activeTab for clarity +const searchQuery = ref(''); +const selectedArtist = ref(null); + +// --- Computed Properties for Filter Buttons --- + +// Extract unique languages +const allUniqueLanguages = computed(() => { + const languages = new Set(); + props.data?.forEach(song => { + song.language?.forEach(lang => { + if (lang?.trim()) { + languages.add(lang.trim()); + } + }); + }); + return Array.from(languages).sort(); +}); + +// Create structure for language buttons +const languageButtons = computed(() => + allUniqueLanguages.value.map((lang, index) => ({ id: index, name: lang })) +); + +// Extract unique tags (similar to original 'tabs' logic) +const allUniqueTags = computed(() => { + const tags = new Set(); + props.data?.forEach(song => { + song.tags?.forEach(tag => { + if (tag?.trim()) { + tags.add(tag.trim()); + } + }); + }); + return Array.from(tags).sort(); +}); + +// Create structure for tag buttons (reuse FilterButton interface) +const tagButtons = computed(() => + allUniqueTags.value.map((tag, index) => ({ id: index, name: tag })) +); + + +// --- Computed Properties for Data --- + +// Get unique artists for the dropdown (unchanged) +const allArtists = computed(() => { + const artists = new Set(); + props.data?.forEach(song => { + song.author?.forEach(author => { + if (author?.trim()) { + artists.add(author.trim()); + } + }); + }); + return Array.from(artists).sort(); +}); + +// Format artists for NSelect options (unchanged) +const artistOptions = computed(() => { + return allArtists.value.map(artist => ({ label: artist, value: artist })); +}); + +// --- Updated Filtered Songs Logic --- +const filteredSongs = computed(() => { + let songs = props.data!; + if (!songs) return []; + + // 1. Filter by Selected Language + if (selectedLanguage.value) { + songs = songs.filter(song => song.language?.includes(selectedLanguage.value!)); } - const props = defineProps>(); - defineExpose({ Config, DefaultConfig }); - const emits = defineEmits(['requestSong']); - - const isHovering = ref(false); - - const tabs: Tab[] = props.data?.map(s => s.tags) // Assuming 'tags' is meant to be 'language'? If not, adjust accordingly. - .flat() - .filter(t => t !== '' && t !== undefined && t !== null) - .map(t => t!.trim()) - .filter((tag, index, self) => self.indexOf(tag) === index) - .map((tag, index) => ({ id: index, name: tag! })) || []; - - const activeTab = ref(); - const searchQuery = ref(''); - const selectedArtist = ref(null); // --- New state for selected artist --- - - // --- Computed Properties --- - - // --- New: Get unique artists for the dropdown --- - const allArtists = computed(() => { - const artists = new Set(); - props.data?.forEach(song => { - song.author.forEach(author => { - if (author?.trim()) { // Ensure author is not empty/whitespace - artists.add(author.trim()); - } - }); - }); - return Array.from(artists).sort(); // Sort alphabetically - }); - - // --- New: Format artists for NSelect options --- - const artistOptions = computed(() => { - return allArtists.value.map(artist => ({ label: artist, value: artist })); - }); - - const filteredSongs = computed(() => { - let songs = props.data!; - if (!songs) return []; // Handle case where data might be initially null/undefined - - // 1. Filter by Active Tab - if (activeTab.value) { - // Assuming filter should be by language as in original code - songs = songs.filter(song => song.language?.some(lang => lang === activeTab.value)); - } - - // --- New: 2. Filter by Selected Artist --- - if (selectedArtist.value) { - songs = songs.filter(song => song.author?.includes(selectedArtist.value!)); - } - - // 3. Filter by Search Query (case-insensitive) - if (searchQuery.value.trim()) { - const lowerSearch = searchQuery.value.toLowerCase().trim(); - songs = songs.filter(song => - song.name.toLowerCase().includes(lowerSearch) || - song.author?.some(artist => artist.toLowerCase().includes(lowerSearch)) || // Check individual artists - song.language?.some(lang => lang.toLowerCase().includes(lowerSearch)) || // Check individual languages - song.description?.toLowerCase().includes(lowerSearch) - ); - } - - return songs; - }); - - // --- Methods --- - const setActiveTab = (tabName: string) => { - if (tabName === activeTab.value) { - activeTab.value = undefined; // Clear filter if clicking the active tab again - } else { - activeTab.value = tabName; - } - }; - - // --- New: Method to select artist (used by clicking in the table) --- - const selectArtist = (artist: string) => { - selectedArtist.value = artist; - }; - - // --- New: Watcher to clear artist selection if the selected artist is no longer valid --- - // (e.g., if the underlying data changes and the artist disappears) - // This is optional but good practice. - watch(allArtists, (newArtists) => { - if (selectedArtist.value && !newArtists.includes(selectedArtist.value)) { - selectedArtist.value = null; - } - }); - - - const randomOrder = () => { - const song = props.data![Math.floor(Math.random() * props.data!.length)]; - window.$modal.create({ - preset:'dialog', - title: '随机点歌', - content: `你选择的歌曲是: ${song.name}, 由 ${song.author.join(', ')} 演唱`, - positiveText: '点歌', - negativeText: '算了', - onPositiveClick: () => { - emits('requestSong', song); - }, - }); - }; - - function onSongClick(song: SongsInfo) { - window.$modal.create({ - preset: 'dialog', - title: '点歌', - content: `确定要点 ${song.name} 么`, - positiveText: '点歌', - negativeText: '算了', - onPositiveClick: () => { - emits('requestSong', song); - }, - }); + // 2. Filter by Selected Tag + if (selectedTag.value) { + songs = songs.filter(song => song.tags?.includes(selectedTag.value!)); } - function GetPlayButton(song: SongsInfo) { - // ... (GetPlayButton function remains the same) - switch (song.from) { + // 3. Filter by Selected Artist + if (selectedArtist.value) { + songs = songs.filter(song => song.author?.includes(selectedArtist.value!)); + } + + // 4. Filter by Search Query (case-insensitive, including tags) + if (searchQuery.value.trim()) { + const lowerSearch = searchQuery.value.toLowerCase().trim(); + songs = songs.filter(song => + song.name.toLowerCase().includes(lowerSearch) || + song.author?.some(artist => artist.toLowerCase().includes(lowerSearch)) || + song.language?.some(lang => lang.toLowerCase().includes(lowerSearch)) || + song.tags?.some(tag => tag.toLowerCase().includes(lowerSearch)) || // Added tags to search + song.description?.toLowerCase().includes(lowerSearch) + ); + } + + return songs; +}); + +// --- Methods --- + +// Select/Deselect Language +const selectLanguage = (langName: string) => { + if (langName === selectedLanguage.value) { + selectedLanguage.value = undefined; // Clear filter if clicking the active one + } else { + selectedLanguage.value = langName; + } +}; + +// Select/Deselect Tag +const selectTag = (tagName: string) => { + if (tagName === selectedTag.value) { + selectedTag.value = undefined; // Clear filter if clicking the active one + } else { + selectedTag.value = tagName; + } +}; + +// Select Artist (from table click, unchanged) +const selectArtist = (artist: string) => { + selectedArtist.value = artist; +}; + +// --- New: Clear All Filters --- +const clearFilters = () => { + selectedLanguage.value = undefined; + selectedTag.value = undefined; + selectedArtist.value = null; // Reset NSelect value + searchQuery.value = ''; +}; + + +// Watcher for artist selection (unchanged, good practice) +watch(allArtists, (newArtists) => { + if (selectedArtist.value && !newArtists.includes(selectedArtist.value)) { + selectedArtist.value = null; + } +}); + + +const randomOrder = () => { + const songsToChooseFrom = filteredSongs.value.length > 0 ? filteredSongs.value : props.data ?? []; + if (songsToChooseFrom.length === 0) { + window.$message?.warning('歌单为空或当前筛选无结果,无法随机点歌'); + return; + } + const song = songsToChooseFrom[Math.floor(Math.random() * songsToChooseFrom.length)]; + window.$modal.create({ + preset: 'dialog', + type: 'success', + title: '随机点歌', + content: `你抽到的歌曲是: ${song.name}, 来自 ${song.author?.join('/')}`, + positiveText: '点歌', + negativeText: '算了', + onPositiveClick: () => { + emits('requestSong', song); + }, + }); +}; + +function onSongClick(song: SongsInfo) { + window.$modal.create({ + preset: 'dialog', + title: '点歌', + content: `确定要点 ${song.name} 么`, + positiveText: '点歌', + negativeText: '算了', + onPositiveClick: () => { + emits('requestSong', song); + }, + }); +} + +// GetPlayButton function remains the same +function GetPlayButton(song: SongsInfo) { + // ... (GetPlayButton function implementation - unchanged) ... + switch (song.from) { case SongFrom.FiveSing: { return h(NTooltip, null, { trigger: () => @@ -204,162 +270,166 @@ }) : null; } - } +} + @@ -400,7 +470,7 @@ - {{ props.userInfo?.name }}的链接 + 关于 {{ props.config?.longDescription }} @@ -462,64 +532,106 @@ + - - + + + 语言: - {{ tab.name }} + {{ lang.name }} - - - - + + + 标签: + + {{ tag.name }} + + + + + + + + - - + - + 🔍 + + + + + + + 清空筛选 + - + 随机点歌 - + @@ -530,16 +642,25 @@ 歌名 歌手 语言 + 标签 备注 - + - 暂无匹配歌曲 + 歌单里还没有歌曲哦~ + + + + + 当前筛选条件下暂无匹配歌曲 - + / - 未知 + 未知 + + {{ song.language?.join(', ') ?? '未知' }} + + + + + {{ tag }} + + 无标签 + - {{ song.language?.join(', ') ?? '未知' }} {{ song.description }} @@ -593,718 +734,432 @@ \ No newline at end of file
- {{ props.userInfo?.name }}的链接 + 关于
{{ props.config?.longDescription }} @@ -462,64 +532,106 @@