initial commit
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
@page "/"
|
||||
@inject SongCatalogService Catalog
|
||||
@inject SiteSettingsService SiteSettings
|
||||
@inject IJSRuntime Js
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<PageTitle>洛星瓷歌单</PageTitle>
|
||||
|
||||
<section class="song-page">
|
||||
<div class="page-heading">
|
||||
<div>
|
||||
<MudText Typo="Typo.h1">洛星瓷歌单</MudText>
|
||||
<MudText Typo="Typo.body1" Color="Color.Secondary">快速查找会唱的歌,复制点歌文案,也可以给歌曲补充标签。</MudText>
|
||||
</div>
|
||||
<MudPaper Class="asset-slot character-slot" Elevation="0">
|
||||
<MudIcon Icon="@Icons.Material.Filled.AutoAwesome" />
|
||||
<span>角色图占位</span>
|
||||
</MudPaper>
|
||||
</div>
|
||||
|
||||
<MudPaper Class="search-panel" Elevation="0">
|
||||
<MudTextField T="string"
|
||||
Value="_search"
|
||||
ValueChanged="OnSearchChanged"
|
||||
Immediate="true"
|
||||
DebounceInterval="180"
|
||||
Placeholder="搜索歌名或标签"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search"
|
||||
Clearable="true" />
|
||||
|
||||
@if (_selectedTag is not null)
|
||||
{
|
||||
<div class="active-filter">
|
||||
<MudChip T="string" Color="Color.Primary" Variant="Variant.Filled" OnClose="ClearTagFilter">
|
||||
标签:@_selectedTag
|
||||
</MudChip>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (_allTags.Count > 0)
|
||||
{
|
||||
<div class="tag-cloud" aria-label="标签筛选">
|
||||
@foreach (var tag in _allTags.Take(32))
|
||||
{
|
||||
<MudChip T="string"
|
||||
Color="@(_selectedTag == tag ? Color.Primary : Color.Info)"
|
||||
Variant="@(_selectedTag == tag ? Variant.Filled : Variant.Outlined)"
|
||||
OnClick="@(() => SelectTag(tag))">
|
||||
@tag
|
||||
</MudChip>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</MudPaper>
|
||||
|
||||
@if (_isLoading)
|
||||
{
|
||||
<div class="loading-row">
|
||||
<MudProgressCircular Indeterminate="true" Color="Color.Primary" />
|
||||
</div>
|
||||
}
|
||||
else if (_songs.Count == 0)
|
||||
{
|
||||
<MudPaper Class="empty-state" Elevation="0">
|
||||
<MudIcon Icon="@Icons.Material.Filled.LibraryMusic" Size="Size.Large" />
|
||||
<MudText Typo="Typo.h6">歌单还是空的</MudText>
|
||||
<MudText Color="Color.Secondary">之后导入歌曲后,这里会显示可搜索的曲目。</MudText>
|
||||
</MudPaper>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="song-grid">
|
||||
@foreach (var song in _songs)
|
||||
{
|
||||
<MudPaper Class="song-card" Elevation="0">
|
||||
<div class="song-card-main">
|
||||
<MudText Typo="Typo.h6">@song.Title</MudText>
|
||||
<div class="song-tags">
|
||||
@foreach (var tag in song.Tags)
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="Color.Info" OnClick="@(() => SelectTag(tag))">
|
||||
@tag
|
||||
</MudChip>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="song-card-actions">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.ContentCopy"
|
||||
OnClick="@(() => CopyRequest(song.Title))">
|
||||
复制点歌
|
||||
</MudButton>
|
||||
</div>
|
||||
|
||||
<div class="viewer-tag-row">
|
||||
<MudTextField T="string"
|
||||
Value="@song.NewTag"
|
||||
ValueChanged="@((string value) => song.NewTag = value)"
|
||||
Placeholder="添加标签"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Add"
|
||||
Color="Color.Primary"
|
||||
Variant="Variant.Filled"
|
||||
Disabled="@string.IsNullOrWhiteSpace(song.NewTag)"
|
||||
OnClick="@(() => AddTag(song))" />
|
||||
</div>
|
||||
</MudPaper>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
@code {
|
||||
private readonly List<SongCardState> _songs = [];
|
||||
private readonly List<string> _allTags = [];
|
||||
private string _search = string.Empty;
|
||||
private string? _selectedTag;
|
||||
private string _requestTemplate = "点歌《{title}》";
|
||||
private bool _isLoading = true;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_requestTemplate = await SiteSettings.GetRequestTemplateAsync();
|
||||
await LoadAsync();
|
||||
}
|
||||
|
||||
private async Task OnSearchChanged(string value)
|
||||
{
|
||||
_search = value;
|
||||
await LoadAsync();
|
||||
}
|
||||
|
||||
private async Task SelectTag(string tag)
|
||||
{
|
||||
_selectedTag = _selectedTag == tag ? null : tag;
|
||||
await LoadAsync();
|
||||
}
|
||||
|
||||
private async Task ClearTagFilter()
|
||||
{
|
||||
_selectedTag = null;
|
||||
await LoadAsync();
|
||||
}
|
||||
|
||||
private async Task LoadAsync()
|
||||
{
|
||||
_isLoading = true;
|
||||
var songs = await Catalog.SearchSongsAsync(_search, _selectedTag);
|
||||
var tags = await Catalog.GetVisibleTagsAsync();
|
||||
|
||||
_songs.Clear();
|
||||
_songs.AddRange(songs.Select(SongCardState.From));
|
||||
_allTags.Clear();
|
||||
_allTags.AddRange(tags);
|
||||
_isLoading = false;
|
||||
}
|
||||
|
||||
private async Task CopyRequest(string title)
|
||||
{
|
||||
var request = SiteSettingsService.FormatRequest(_requestTemplate, title);
|
||||
await Js.InvokeVoidAsync("navigator.clipboard.writeText", request);
|
||||
Snackbar.Add($"已复制:{request}", Severity.Success);
|
||||
}
|
||||
|
||||
private async Task AddTag(SongCardState song)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Catalog.AddViewerTagAsync(song.Id, song.NewTag);
|
||||
Snackbar.Add("标签已添加", Severity.Success);
|
||||
await LoadAsync();
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
Snackbar.Add(ex.Message, Severity.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SongCardState
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public string Title { get; init; } = string.Empty;
|
||||
public IReadOnlyList<string> Tags { get; init; } = [];
|
||||
public string NewTag { get; set; } = string.Empty;
|
||||
|
||||
public static SongCardState From(SongListItem item)
|
||||
{
|
||||
return new SongCardState
|
||||
{
|
||||
Id = item.Id,
|
||||
Title = item.Title,
|
||||
Tags = item.Tags
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user