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
+284
View File
@@ -0,0 +1,284 @@
@page "/admin"
@attribute [Authorize]
@inject SongCatalogService Catalog
@inject SiteSettingsService SiteSettings
@inject ISnackbar Snackbar
<PageTitle>歌单管理</PageTitle>
<section class="admin-page">
<div class="page-heading compact">
<div>
<MudText Typo="Typo.h1">歌单管理</MudText>
<MudText Typo="Typo.body1" Color="Color.Secondary">添加歌曲、隐藏曲目、清理标签和调整点歌文案。</MudText>
</div>
<MudButton Href="/admin/logout" Variant="Variant.Outlined" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Logout">
退出
</MudButton>
</div>
<MudGrid Spacing="3">
<MudItem xs="12" md="7">
<MudPaper Class="admin-panel" Elevation="0">
<div class="panel-title-row">
<MudText Typo="Typo.h5">歌曲</MudText>
<MudChip T="string" Color="Color.Info" Variant="Variant.Outlined">@_songs.Count 首</MudChip>
</div>
<div class="admin-add-row">
<MudTextField T="string"
Value="_newSongTitle"
ValueChanged="@((string value) => _newSongTitle = value)"
Label="新增歌名"
Variant="Variant.Outlined"
Margin="Margin.Dense" />
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
Disabled="@string.IsNullOrWhiteSpace(_newSongTitle)"
OnClick="CreateSong">
添加
</MudButton>
</div>
@if (_songs.Count == 0)
{
<MudText Class="admin-muted" Color="Color.Secondary">还没有歌曲。之后可以从这里手动添加,或直接导入数据库。</MudText>
}
else
{
<div class="admin-list">
@foreach (var song in _songs)
{
<MudPaper Class="@($"admin-list-item {(song.IsHidden ? "is-hidden" : string.Empty)}")" Elevation="0">
<MudTextField T="string"
Value="@song.Title"
ValueChanged="@((string value) => song.Title = value)"
Label="歌名"
Variant="Variant.Outlined"
Margin="Margin.Dense" />
<MudCheckBox T="bool"
Value="@song.IsHidden"
ValueChanged="@((bool value) => song.IsHidden = value)"
Label="隐藏" />
<div class="song-tags admin-song-tags">
@foreach (var tag in song.Tags)
{
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="Color.Info">@tag</MudChip>
}
</div>
<div class="admin-actions">
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Save" OnClick="@(() => SaveSong(song))">
保存
</MudButton>
<MudButton Variant="Variant.Outlined" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" OnClick="@(() => DeleteSong(song))">
删除
</MudButton>
</div>
</MudPaper>
}
</div>
}
</MudPaper>
</MudItem>
<MudItem xs="12" md="5">
<MudPaper Class="admin-panel" Elevation="0">
<MudText Typo="Typo.h5">站点设置</MudText>
<MudTextField T="string"
Value="_requestTemplate"
ValueChanged="@((string value) => _requestTemplate = value)"
Label="点歌文案模板"
HelperText="使用 {title} 代表歌名"
Variant="Variant.Outlined" />
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Save" OnClick="SaveRequestTemplate">
保存设置
</MudButton>
</MudPaper>
<MudPaper Class="admin-panel" Elevation="0">
<div class="panel-title-row">
<MudText Typo="Typo.h5">标签</MudText>
<MudChip T="string" Color="Color.Info" Variant="Variant.Outlined">@_tags.Count 个</MudChip>
</div>
@if (_tags.Count == 0)
{
<MudText Class="admin-muted" Color="Color.Secondary">暂无标签。</MudText>
}
else
{
<div class="tag-admin-list">
@foreach (var tag in _tags)
{
<MudPaper Class="tag-admin-item" Elevation="0">
<MudTextField T="string"
Value="@tag.Name"
ValueChanged="@((string value) => tag.Name = value)"
Label="@($"标签,{tag.SongCount} 首")"
Variant="Variant.Outlined"
Margin="Margin.Dense" />
<MudTextField T="string"
Value="@tag.MergeTarget"
ValueChanged="@((string value) => tag.MergeTarget = value)"
Label="合并到"
Variant="Variant.Outlined"
Margin="Margin.Dense" />
<div class="admin-actions">
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Save" OnClick="@(() => RenameTag(tag))">
改名
</MudButton>
<MudButton Variant="Variant.Outlined" Color="Color.Info" StartIcon="@Icons.Material.Filled.CallMerge" Disabled="@string.IsNullOrWhiteSpace(tag.MergeTarget)" OnClick="@(() => MergeTag(tag))">
合并
</MudButton>
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Variant="Variant.Outlined" OnClick="@(() => DeleteTag(tag))" />
</div>
</MudPaper>
}
</div>
}
</MudPaper>
</MudItem>
</MudGrid>
</section>
@code {
private readonly List<AdminSongState> _songs = [];
private readonly List<AdminTagState> _tags = [];
private string _newSongTitle = string.Empty;
private string _requestTemplate = "点歌《{title}》";
protected override async Task OnInitializedAsync()
{
await LoadAsync();
}
private async Task LoadAsync()
{
var songs = await Catalog.GetAdminSongsAsync();
var tags = await Catalog.GetAdminTagsAsync();
_requestTemplate = await SiteSettings.GetRequestTemplateAsync();
_songs.Clear();
_songs.AddRange(songs.Select(AdminSongState.From));
_tags.Clear();
_tags.AddRange(tags.Select(AdminTagState.From));
}
private async Task CreateSong()
{
await RunAdminAction(async () =>
{
await Catalog.CreateSongAsync(_newSongTitle);
_newSongTitle = string.Empty;
Snackbar.Add("歌曲已添加", Severity.Success);
});
}
private async Task SaveSong(AdminSongState song)
{
await RunAdminAction(async () =>
{
await Catalog.UpdateSongAsync(song.Id, song.Title, song.IsHidden);
Snackbar.Add("歌曲已保存", Severity.Success);
});
}
private async Task DeleteSong(AdminSongState song)
{
await RunAdminAction(async () =>
{
await Catalog.DeleteSongAsync(song.Id);
Snackbar.Add("歌曲已删除", Severity.Success);
});
}
private async Task SaveRequestTemplate()
{
await RunAdminAction(async () =>
{
await SiteSettings.SaveRequestTemplateAsync(_requestTemplate);
Snackbar.Add("设置已保存", Severity.Success);
}, reload: false);
}
private async Task RenameTag(AdminTagState tag)
{
await RunAdminAction(async () =>
{
await Catalog.RenameTagAsync(tag.Id, tag.Name);
Snackbar.Add("标签已更新", Severity.Success);
});
}
private async Task MergeTag(AdminTagState tag)
{
await RunAdminAction(async () =>
{
await Catalog.MergeTagAsync(tag.Id, tag.MergeTarget);
Snackbar.Add("标签已合并", Severity.Success);
});
}
private async Task DeleteTag(AdminTagState tag)
{
await RunAdminAction(async () =>
{
await Catalog.DeleteTagAsync(tag.Id);
Snackbar.Add("标签已删除", Severity.Success);
});
}
private async Task RunAdminAction(Func<Task> action, bool reload = true)
{
try
{
await action();
if (reload)
{
await LoadAsync();
}
}
catch (InvalidOperationException ex)
{
Snackbar.Add(ex.Message, Severity.Warning);
}
}
private sealed class AdminSongState
{
public int Id { get; init; }
public string Title { get; set; } = string.Empty;
public bool IsHidden { get; set; }
public IReadOnlyList<string> Tags { get; init; } = [];
public static AdminSongState From(AdminSongItem item)
{
return new AdminSongState
{
Id = item.Id,
Title = item.Title,
IsHidden = item.IsHidden,
Tags = item.Tags
};
}
}
private sealed class AdminTagState
{
public int Id { get; init; }
public string Name { get; set; } = string.Empty;
public string MergeTarget { get; set; } = string.Empty;
public int SongCount { get; init; }
public static AdminTagState From(AdminTagItem item)
{
return new AdminTagState
{
Id = item.Id,
Name = item.Name,
SongCount = item.SongCount
};
}
}
}