initial commit
This commit is contained in:
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user