feat: 更新 SongListView 和 TraditionalSongListTemplate 组件,增强加载逻辑和排序功能

- 在 SongListView 中优化了加载状态管理,增加了数据和配置加载的分离处理。
- 更新了 TraditionalSongListTemplate 组件,新增排序功能,支持按歌名、歌手、语言等字段排序。
- 改进了歌曲筛选逻辑,支持多条件过滤和排序,提升用户体验。
- 修复歌单加载时闪烁的问题
This commit is contained in:
2025-04-22 03:39:13 +08:00
parent 77cf0c5edc
commit d6577ec129
2 changed files with 331 additions and 88 deletions

View File

@@ -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>

View File

@@ -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>