initial commit

This commit is contained in:
2026-06-03 20:24:57 -07:00
commit e2ad15fa64
37 changed files with 3092 additions and 0 deletions
+201
View File
@@ -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
};
}
}
}