feat: 美化文章界面 #3
13
YaeBlog.Core/Abstractions/IEssayContentService.cs
Normal file
13
YaeBlog.Core/Abstractions/IEssayContentService.cs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using YaeBlog.Core.Models;
|
||||||
|
|
||||||
|
namespace YaeBlog.Core.Abstractions;
|
||||||
|
|
||||||
|
public interface IEssayContentService
|
||||||
|
{
|
||||||
|
public IReadOnlyDictionary<string, BlogEssay> Essays { get; }
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<EssayTag, List<BlogEssay>> Tags { get; }
|
||||||
|
|
||||||
|
public bool SearchByUrlEncodedTag(string tag,[NotNullWhen(true)] out List<BlogEssay>? result);
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using YaeBlog.Core.Abstractions;
|
||||||
using YaeBlog.Core.Models;
|
using YaeBlog.Core.Models;
|
||||||
using YaeBlog.Core.Processors;
|
using YaeBlog.Core.Processors;
|
||||||
using YaeBlog.Core.Services;
|
using YaeBlog.Core.Services;
|
||||||
|
@ -20,6 +21,8 @@ public static class WebApplicationBuilderExtensions
|
||||||
builder.Services.AddSingleton<EssayScanService>();
|
builder.Services.AddSingleton<EssayScanService>();
|
||||||
builder.Services.AddSingleton<RendererService>();
|
builder.Services.AddSingleton<RendererService>();
|
||||||
builder.Services.AddSingleton<EssayContentService>();
|
builder.Services.AddSingleton<EssayContentService>();
|
||||||
|
builder.Services.AddSingleton<IEssayContentService, EssayContentService>(provider =>
|
||||||
|
provider.GetRequiredService<EssayContentService>());
|
||||||
builder.Services.AddTransient<ImagePostRenderProcessor>();
|
builder.Services.AddTransient<ImagePostRenderProcessor>();
|
||||||
builder.Services.AddTransient<BlogOptions>(provider =>
|
builder.Services.AddTransient<BlogOptions>(provider =>
|
||||||
provider.GetRequiredService<IOptions<BlogOptions>>().Value);
|
provider.GetRequiredService<IOptions<BlogOptions>>().Value);
|
||||||
|
|
16
YaeBlog.Core/Models/EssayTag.cs
Normal file
16
YaeBlog.Core/Models/EssayTag.cs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
|
||||||
|
namespace YaeBlog.Core.Models;
|
||||||
|
|
||||||
|
public class EssayTag(string tagName) : IEquatable<EssayTag>
|
||||||
|
{
|
||||||
|
public string TagName { get; } = tagName;
|
||||||
|
|
||||||
|
public string UrlEncodedTagName { get; } = UrlEncoder.Default.Encode(tagName);
|
||||||
|
|
||||||
|
public bool Equals(EssayTag? other) => other is not null && TagName == other.TagName;
|
||||||
|
|
||||||
|
public override bool Equals(object? obj) => obj is EssayTag other && Equals(other);
|
||||||
|
|
||||||
|
public override int GetHashCode() => TagName.GetHashCode();
|
||||||
|
}
|
|
@ -1,57 +1,48 @@
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using YaeBlog.Core.Abstractions;
|
||||||
using YaeBlog.Core.Models;
|
using YaeBlog.Core.Models;
|
||||||
|
|
||||||
namespace YaeBlog.Core.Services;
|
namespace YaeBlog.Core.Services;
|
||||||
|
|
||||||
public class EssayContentService
|
public class EssayContentService : IEssayContentService
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<string, BlogEssay> _essays = new();
|
private readonly ConcurrentDictionary<string, BlogEssay> _essays = new();
|
||||||
|
|
||||||
private readonly Dictionary<string, List<BlogEssay>> _tags = [];
|
private readonly Dictionary<EssayTag, List<BlogEssay>> _tags = [];
|
||||||
|
|
||||||
public bool TryGet(string key, out BlogEssay? essay)
|
public bool TryAdd(BlogEssay essay) => _essays.TryAdd(essay.FileName, essay);
|
||||||
=> _essays.TryGetValue(key, out essay);
|
|
||||||
|
|
||||||
public bool TryAdd(string key, BlogEssay essay) => _essays.TryAdd(key, essay);
|
public IReadOnlyDictionary<string, BlogEssay> Essays => _essays;
|
||||||
|
|
||||||
public IDictionary<string, BlogEssay> Essays => _essays;
|
public IReadOnlyDictionary<EssayTag, List<BlogEssay>> Tags => _tags;
|
||||||
|
|
||||||
public void RefreshTags()
|
public void RefreshTags()
|
||||||
{
|
{
|
||||||
|
_tags.Clear();
|
||||||
|
|
||||||
foreach (BlogEssay essay in _essays.Values)
|
foreach (BlogEssay essay in _essays.Values)
|
||||||
{
|
{
|
||||||
foreach (string tag in essay.Tags)
|
foreach (EssayTag essayTag in essay.Tags.Select(tag => new EssayTag(tag)))
|
||||||
{
|
{
|
||||||
if (_tags.TryGetValue(tag, out var list))
|
if (_tags.TryGetValue(essayTag, out List<BlogEssay>? essays))
|
||||||
{
|
{
|
||||||
list.Add(essay);
|
essays.Add(essay);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_tags[tag] = [essay];
|
_tags.Add(essayTag, [essay]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (KeyValuePair<string,List<BlogEssay>> pair in _tags)
|
public bool SearchByUrlEncodedTag(string tag, [NotNullWhen(true)] out List<BlogEssay>? result)
|
||||||
{
|
{
|
||||||
pair.Value.Sort();
|
result = (from item in _tags
|
||||||
}
|
where item.Key.UrlEncodedTagName == tag
|
||||||
}
|
select item.Value).FirstOrDefault();
|
||||||
|
|
||||||
public IEnumerable<KeyValuePair<string, int>> Tags => from item in _tags
|
return result is not null;
|
||||||
orderby item.Value.Count descending
|
|
||||||
select KeyValuePair.Create(item.Key, item.Value.Count);
|
|
||||||
|
|
||||||
public int TagCount => _tags.Count;
|
|
||||||
|
|
||||||
public IEnumerable<BlogEssay> GetTag(string tag)
|
|
||||||
{
|
|
||||||
if (_tags.TryGetValue(tag, out var list))
|
|
||||||
{
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new KeyNotFoundException("Selected tag not found.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -125,7 +125,7 @@ public class RendererService(ILogger<RendererService> logger,
|
||||||
essay = await processor.ProcessAsync(essay);
|
essay = await processor.ProcessAsync(essay);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!essayContentService.TryAdd(essay.FileName, essay))
|
if (!essayContentService.TryAdd(essay))
|
||||||
{
|
{
|
||||||
throw new BlogFileException(
|
throw new BlogFileException(
|
||||||
$"There are two essays with the same name: '{essay.FileName}'.");
|
$"There are two essays with the same name: '{essay.FileName}'.");
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
|
@using YaeBlog.Core.Abstractions
|
||||||
@using YaeBlog.Core.Models
|
@using YaeBlog.Core.Models
|
||||||
@using YaeBlog.Core.Services
|
|
||||||
|
|
||||||
@inject EssayContentService EssayContentInstance
|
@inject IEssayContentService Contents
|
||||||
@inject BlogOptions Options
|
@inject BlogOptions Options
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
|
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<a href="/blog/archives">
|
<a href="/blog/archives">
|
||||||
@(EssayContentInstance.Essays.Count)
|
@(Contents.Essays.Count)
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
|
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<a href="/blog/tags">
|
<a href="/blog/tags">
|
||||||
@(EssayContentInstance.TagCount)
|
@(Contents.Tags.Count)
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
@page "/blog/about"
|
@page "/blog/about"
|
||||||
|
|
||||||
|
<PageTitle>
|
||||||
|
关于
|
||||||
|
</PageTitle>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
@page "/blog/archives"
|
@page "/blog/archives"
|
||||||
|
@using YaeBlog.Core.Abstractions
|
||||||
@using YaeBlog.Core.Models
|
@using YaeBlog.Core.Models
|
||||||
@using YaeBlog.Core.Services
|
|
||||||
|
|
||||||
@inject EssayContentService EssayContentInstance
|
@inject IEssayContentService Contents
|
||||||
|
|
||||||
|
<PageTitle>
|
||||||
|
归档
|
||||||
|
</PageTitle>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -64,9 +68,8 @@
|
||||||
{
|
{
|
||||||
base.OnInitialized();
|
base.OnInitialized();
|
||||||
|
|
||||||
_essays.AddRange(from essay in EssayContentInstance.Essays
|
_essays.AddRange(from essay in Contents.Essays
|
||||||
orderby essay.Value.PublishTime descending
|
orderby essay.Value.PublishTime descending
|
||||||
group essay by new DateTime(essay.Value.PublishTime.Year, 1, 1));
|
group essay by new DateTime(essay.Value.PublishTime.Year, 1, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
@page "/blog"
|
@page "/blog"
|
||||||
|
@using YaeBlog.Core.Abstractions
|
||||||
@using YaeBlog.Core.Models
|
@using YaeBlog.Core.Models
|
||||||
@using YaeBlog.Core.Services
|
|
||||||
|
|
||||||
@inject EssayContentService EssayContentInstance
|
@inject IEssayContentService Contents
|
||||||
@inject NavigationManager NavigationInstance
|
@inject NavigationManager NavigationInstance
|
||||||
|
|
||||||
<PageTitle>
|
<PageTitle>
|
||||||
|
@ -99,15 +99,15 @@
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
_page = Page ?? 1;
|
_page = Page ?? 1;
|
||||||
_pageCount = EssayContentInstance.Essays.Count / EssaysPerPage + 1;
|
_pageCount = Contents.Essays.Count / EssaysPerPage + 1;
|
||||||
|
|
||||||
if (EssaysPerPage * _page > EssayContentInstance.Essays.Count + EssaysPerPage)
|
if (EssaysPerPage * _page > Contents.Essays.Count + EssaysPerPage)
|
||||||
{
|
{
|
||||||
NavigationInstance.NavigateTo("/NotFount");
|
NavigationInstance.NavigateTo("/NotFount");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_essays.AddRange(EssayContentInstance.Essays
|
_essays.AddRange(Contents.Essays
|
||||||
.OrderByDescending(p => p.Value.PublishTime)
|
.OrderByDescending(p => p.Value.PublishTime)
|
||||||
.Skip((_page - 1) * EssaysPerPage)
|
.Skip((_page - 1) * EssaysPerPage)
|
||||||
.Take(EssaysPerPage));
|
.Take(EssaysPerPage));
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
@page "/blog/essays/{BlogKey}"
|
@page "/blog/essays/{BlogKey}"
|
||||||
|
@using YaeBlog.Core.Abstractions
|
||||||
@using YaeBlog.Core.Models
|
@using YaeBlog.Core.Models
|
||||||
@using YaeBlog.Core.Services
|
|
||||||
|
|
||||||
|
@inject IEssayContentService Contents
|
||||||
@inject NavigationManager NavigationInstance
|
@inject NavigationManager NavigationInstance
|
||||||
@inject EssayContentService EssayContentInstance
|
|
||||||
|
|
||||||
<PageTitle>
|
<PageTitle>
|
||||||
@(_essay!.Title)
|
@(_essay!.Title)
|
||||||
|
@ -52,7 +52,7 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!EssayContentInstance.TryGet(BlogKey, out _essay))
|
if (!Contents.Essays.TryGetValue(BlogKey, out _essay))
|
||||||
{
|
{
|
||||||
NavigationInstance.NavigateTo("/NotFound");
|
NavigationInstance.NavigateTo("/NotFound");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
@page "/NotFound"
|
@page "/NotFound"
|
||||||
|
|
||||||
|
<PageTitle>
|
||||||
|
啊~ 页面走丢啦~
|
||||||
|
</PageTitle>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h3>NotFound!</h3>
|
<h3>NotFound!</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
@page "/blog/tags/"
|
@page "/blog/tags/"
|
||||||
|
@using System.Text.Encodings.Web
|
||||||
|
@using YaeBlog.Core.Abstractions
|
||||||
@using YaeBlog.Core.Models
|
@using YaeBlog.Core.Models
|
||||||
@using YaeBlog.Core.Services
|
|
||||||
|
|
||||||
@inject EssayContentService EssayContentInstance
|
@inject IEssayContentService Contents
|
||||||
|
@inject NavigationManager NavigationInstance
|
||||||
|
|
||||||
|
<PageTitle>
|
||||||
|
@(TagName ?? "标签")
|
||||||
|
</PageTitle>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -28,18 +34,19 @@
|
||||||
{
|
{
|
||||||
<div>
|
<div>
|
||||||
<ul>
|
<ul>
|
||||||
@foreach (KeyValuePair<string, int> pair in EssayContentInstance.Tags)
|
@foreach (KeyValuePair<EssayTag, List<BlogEssay>> pair in
|
||||||
|
Contents.Tags.OrderByDescending(pair => pair.Value.Count))
|
||||||
{
|
{
|
||||||
<li class="p-2">
|
<li class="p-2">
|
||||||
<a href="/blog/tags/?tagName=@(pair.Key)">
|
<a href="/blog/tags/?tagName=@(pair.Key.UrlEncodedTagName)">
|
||||||
<div class="container fs-5">
|
<div class="container fs-5">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
# @(pair.Key)
|
# @(pair.Key.TagName)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto tag-count">
|
<div class="col-auto tag-count">
|
||||||
@(pair.Value)
|
@(pair.Value.Count)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -52,7 +59,7 @@
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@foreach (BlogEssay essay in EssayContentInstance.GetTag(TagName).OrderByDescending(e => e.PublishTime))
|
@foreach (BlogEssay essay in _essays)
|
||||||
{
|
{
|
||||||
<EssayCard Essay="@essay"/>
|
<EssayCard Essay="@essay"/>
|
||||||
}
|
}
|
||||||
|
@ -63,5 +70,23 @@
|
||||||
@code {
|
@code {
|
||||||
[SupplyParameterFromQuery] public string? TagName { get; set; }
|
[SupplyParameterFromQuery] public string? TagName { get; set; }
|
||||||
|
|
||||||
|
private readonly List<BlogEssay> _essays = [];
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
base.OnInitialized();
|
||||||
|
if (string.IsNullOrEmpty(TagName))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Contents.SearchByUrlEncodedTag(UrlEncoder.Default.Encode(TagName), out List<BlogEssay>? essays))
|
||||||
|
{
|
||||||
|
NavigationInstance.NavigateTo("/NotFound");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_essays.AddRange(essays.OrderDescending());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user