This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
过滤器栏进度 *\ ( 和日期范围
Monday, 10 November 2025
一. 导言导言 导言 导言 导言
在过去的几天里, I' 一直在敲击新的博客过滤器 BarMS K1 语言选择@ , 订购 @ , 和日期范围拾取者, 他们非常努力地要聪明\ MS K4 有时太智能@). 这个邮报走过了我建造的东西\ I,}为什么有几件事情偏离了 {(} dates are having MPK8 突出显示没有刷新\ NSK9 我如何修补它们_.}源码和一对美人鱼图来展示流程.}
记住这个网站是一个在进行中的工作@,}这种东西会在你吃的时候发生
什么改变了? ?
我为博客索引添加了一个适当的过滤栏, 包括@: @
- 保留您当前过滤器的语言选择, 然后通过 HTMX 再次@ - 请求列表
- 排序顺序选择“%(date”\“/}标题 asc @/desc)}同时保留上下文
- 日期区域选择器 @ (Flatpartr, 在范围模式@ MS K1, 带有服务器 @ MPK2% 驱动的日亮提示
- URL state 总是保持同步@ ( recack} @ MS K1_ forward works * , 深层链接工作=)}
- 服务器缓存现在根据过滤参数正确变化
New in this new update @ (Big fix!)}在最新更新的“ 大裂缝”中
- 可见过滤器@: posts 现在可以隐藏@, 计划将来出版@MS K2 或者固定在顶端
- 日期范围终点: 新建
/blog/date-rangeAPI 返回明智的拾取界限的最大日期 - 清除参数标签助手使用“:”来清除过滤器的简单方法
<clear-param>使用 Alpine @ .% js 的标记助手
在我之前有一些错误=:}日期范围在语言上丢失@ / changes change,}日历突出显示 sweeping on switch_ ,}没有刷新,而有趣的是黑暗模式风格使日历看起来破碎了 因为 Flatparter 是{'}% t re_drawing>.}
DOM 中的过滤栏
这是脚本期待的粗略结构@: @
<div id="filters" hx-target="#content">
<select id="languageSelect">…</select>
<select id="orderSelect">
<option value="date_desc">Newest first</option>
<option value="date_asc">Oldest first</option>
<option value="title_asc">Title A–Z</option>
<option value="title_desc">Title Z–A</option>
</select>
<input id="dateRange" type="text" placeholder="YYYY-MM-DD → YYYY-MM-DD" />
<button id="clearDateFilter">Clear</button>
<div id="filterSummary"></div>
</div>
<div id="content"><!-- HTMX swaps blog list here --></div>
客戶端 : HTMX {+}_BARBlattpickr 網絡
行为核心 Mostlylucid/src/js/blog-index.js几件重要的东西
- URL保全和HTMX导航
function applyNavigation(u){
const target = document.querySelector('#content');
try{ window.history.pushState({}, '', u.toString()); }catch{}
if(window.htmx && target){
window.htmx.ajax('GET', u.toString(), {
target: '#content',
swap: 'outerHTML show:none',
headers: {'pagerequest': 'true'}
});
} else {
window.location.href = u.toString();
}
}
- 初始化@, 默认退出 URL @, 并保持 JUI同步
const url = new URL(window.location.href);
const existingStart = url.searchParams.get('startDate');
const existingEnd = url.searchParams.get('endDate');
const existingLang = url.searchParams.get('language') || 'en';
const existingOrderBy = (url.searchParams.get('orderBy') || 'date').toLowerCase();
const existingOrderDir = (url.searchParams.get('orderDir') || 'desc').toLowerCase();
langSelect.value = existingLang;
orderSelect.value = `${existingOrderBy}_${existingOrderDir}`;
updateSummary();
- 使用服务器@‑}# 驱动日亮显示的平坦拾取器范围
async function fetchMonth(year, month, language){
const res = await fetch(`/blog/calendar-days?year=${year}&month=${month}&language=${encodeURIComponent(language||'en')}`);
if(!res.ok) return new Set();
const j = await res.json();
return new Set(j.dates || []);
}
function formatYMD(d){ return d.toISOString().substring(0,10); }
let highlightDates = new Set();
const fp = window.flatpickr(input, {
mode: 'range',
dateFormat: 'Y-m-d',
defaultDate: [existingStart, existingEnd].filter(Boolean),
onDayCreate: function(_dObj,_dStr,fpInstance,dayElem){
const ymd = formatYMD(dayElem.dateObj);
if(highlightDates.has(ymd)){
dayElem.classList.add('has-post');
dayElem.style.background = 'rgba(76,175,80,0.35)';
dayElem.style.borderRadius = '6px';
}
},
onMonthChange: async function(_sd,_ds,fpInstance){
highlightDates = await fetchMonth(fpInstance.currentYear, fpInstance.currentMonth+1, langSelect.value);
fpInstance.redraw();
},
onOpen: async function(_sd,_ds,fpInstance){
highlightDates = await fetchMonth(fpInstance.currentYear, fpInstance.currentMonth+1, langSelect.value);
fpInstance.redraw();
},
onChange: function(selectedDates){
if(selectedDates.length === 2){
const [start,end] = selectedDates;
const u = new URL(window.location.href);
u.searchParams.set('startDate', formatYMD(start));
u.searchParams.set('endDate', formatYMD(end));
u.searchParams.set('page','1');
u.searchParams.set('language', langSelect.value);
const [ob,od] = orderSelect.value.split('_');
u.searchParams.set('orderBy', ob);
u.searchParams.set('orderDir', od);
updateSummary();
applyNavigation(u);
}
}
});
- 更改语言或顺序时保留日期
langSelect.addEventListener('change', async function(){
const u = new URL(window.location.href);
u.searchParams.set('language', langSelect.value);
u.searchParams.set('page', '1');
const [ob,od] = (orderSelect.value||'date_desc').split('_');
u.searchParams.set('orderBy', ob);
u.searchParams.set('orderDir', od);
if(input._flatpickr && input._flatpickr.selectedDates.length===2){
const [s,e] = input._flatpickr.selectedDates;
u.searchParams.set('startDate', formatYMD(s));
u.searchParams.set('endDate', formatYMD(e));
}
// refresh calendar highlights for new language
if(input._flatpickr){
const fp = input._flatpickr;
highlightDates = await fetchMonth(fp.currentYear, fp.currentMonth+1, langSelect.value);
fp.redraw();
}
updateSummary();
applyNavigation(u);
});
- 当主题切换到 & ( @ dark@ MS K2 light@ I)} 所以 Flatpickr 重新油漆时, re- @ draw 日历
const obs = new MutationObserver(() => {
const dr = root.querySelector('#dateRange');
const fp = dr && dr._flatpickr;
if(fp && typeof fp.redraw === 'function') fp.redraw();
});
obs.observe(document.documentElement, { attributes:true, attributeFilter:['class'] });
服务器@:端点和缓存
三端点为页面“:”提供动力
- 接受页面“,”页面Size @,}开始日期:ate@/_endDate{,}语言=,和顺序*
- AA
calendar-days端点, 返回在给定月份内有 {(} spost {possible\ )} 的日数集 。 - AA
date-range端点, 返回所有日志的 min/max 日期@(}语言 @-awareMS K3}
// GET /blog
[HttpGet]
[ResponseCache(Duration = 300, VaryByHeader = "hx-request",
VaryByQueryKeys = new[] { "page", "pageSize", nameof(startDate), nameof(endDate), nameof(language), nameof(orderBy), nameof(orderDir) },
Location = ResponseCacheLocation.Any)]
[OutputCache(Duration = 3600, VaryByHeaderNames = new[] { "hx-request" },
VaryByQueryKeys = new[] { nameof(page), nameof(pageSize), nameof(startDate), nameof(endDate), nameof(language), nameof(orderBy), nameof(orderDir) })]
public async Task<IActionResult> Index(int page = 1, int pageSize = 20, DateTime? startDate = null, DateTime? endDate = null,
string language = MarkdownBaseService.EnglishLanguage, string orderBy = "date", string orderDir = "desc")
{
var posts = await blogViewService.GetPagedPosts(page, pageSize, language: language, startDate: startDate, endDate: endDate);
posts.LinkUrl = Url.Action("Index", "Blog", new { startDate, endDate, language, orderBy, orderDir });
if (Request.IsHtmx()) return PartialView("_BlogSummaryList", posts);
return View("Index", posts);
}
// GET /blog/calendar-days?year=2025&month=11&language=en
[HttpGet("calendar-days")]
[ResponseCache(Duration = 300, VaryByHeader = "hx-request", VaryByQueryKeys = new[] { nameof(year), nameof(month), nameof(language) }, Location = ResponseCacheLocation.Any)]
[OutputCache(Duration = 1800, VaryByHeaderNames = new[] { "hx-request" }, VaryByQueryKeys = new[] { nameof(year), nameof(month), nameof(language) })]
public async Task<IActionResult> CalendarDays(int year, int month, string language = MarkdownBaseService.EnglishLanguage)
{
if (year < 2000 || month < 1 || month > 12) return BadRequest("Invalid year or month");
var start = new DateTime(year, month, 1);
var end = start.AddMonths(1).AddDays(-1);
var posts = await blogViewService.GetPostsForRange(start, end, language: language);
if (posts is null) return Json("");
var dates = posts.Select(p => p.PublishedDate.Date).Distinct().OrderBy(d => d).Select(d => d.ToString("yyyy-MM-dd")).ToList();
return Json(new { dates });
}
// GET /blog/date-range?language=en
[HttpGet("date-range")]
[ResponseCache(Duration = 3600, VaryByHeader = "hx-request", VaryByQueryKeys = new[] { nameof(language) }, Location = ResponseCacheLocation.Any)]
[OutputCache(Duration = 7200, VaryByHeaderNames = new[] { "hx-request" }, VaryByQueryKeys = new[] { nameof(language) })]
public async Task<IActionResult> DateRange(string language = MarkdownBaseService.EnglishLanguage)
{
var allPosts = await blogViewService.GetAllPosts();
if (allPosts is null || !allPosts.Any())
{
return Json(new
{
minDate = DateTime.UtcNow.AddYears(-1).ToString("yyyy-MM-dd"),
maxDate = DateTime.UtcNow.ToString("yyyy-MM-dd")
});
}
var posts = allPosts;
if (!string.IsNullOrEmpty(language) && language != MarkdownBaseService.EnglishLanguage)
{
posts = allPosts.Where(p => p.Language.Equals(language, StringComparison.OrdinalIgnoreCase)).ToList();
}
if (!posts.Any()) posts = allPosts;
var minDate = posts.Min(p => p.PublishedDate.Date);
var maxDate = posts.Max(p => p.PublishedDate.Date);
return Json(new
{
minDate = minDate.ToString("yyyy-MM-dd"),
maxDate = maxDate.ToString("yyyy-MM-dd")
});
}
快速切换@ : @ 呼叫模式 LinkUrl 这样, 当服务器% ‑ @side. } 设置时, 光度会保护当前过滤环境的内涵@ MS K0_ side@ msk1\ {
public class BasePagingModel<T> : Interfaces.IPagingModel<T> where T : class
{
public int Page { get; set; }
public int TotalItems { get; set; } = 0;
public int PageSize { get; set; }
public ViewType ViewType { get; set; } = ViewType.TailwindAndDaisy;
public string LinkUrl { get; set; }
public List<T> Data { get; set; }
}
过滤器如何变化流
sequenceDiagram
participant U as User
participant FP as Flatpickr
participant JS as blog-index.js
participant HT as HTMX
participant C as BlogController
U->>FP: Selects 2025-11-01 → 2025-11-10
FP-->>JS: onChange([start,end])
JS->>JS: Update URLSearchParams (startDate, endDate, language, order)
JS->>HT: htmx.ajax('GET', /blog?...)
HT->>C: GET /blog with query
C-->>HT: Partial _BlogSummaryList
HT-->>U: Swap #content
JS->>FP: (after swap) ensure highlights + redraw
滚动:日历概要
flowchart TD A[Open calendar / Month change] --> B[Fetch /blog/calendar-days] B -->|JSON dates: yyyy-mm-dd array| C[Set highlightDates] C --> D[flatpickr redraw] D --> E[onDayCreate adds .has-post]
新功能@ : @ 可见度和内容控制
可见过滤器
最新更新的最大改进是适当的员额能见度管理@.+3个新领域 BlogPostEntity:
public class BlogPostEntity
{
// ... existing fields ...
public bool IsPinned { get; set; }
public bool IsHidden { get; set; }
public DateTimeOffset? ScheduledPublishDate { get; set; }
}
缩略 BlogService 现在适当过滤设置@: @%
// Filter out hidden posts and posts scheduled for the future
var now = DateTimeOffset.UtcNow;
postQuery = postQuery.Where(x =>
!x.IsHidden &&
(x.ScheduledPublishDate == null || x.ScheduledPublishDate <= now));
// For page 1, prioritize pinned posts
var isFirstPage = page == null || page.Value == 1;
if (isFirstPage)
{
postQuery = postQuery.OrderByDescending(x => x.IsPinned)
.ThenByDescending(x => x.PublishedDate.DateTime);
}
意思是:
- 隐藏职位 (
IsHidden = true** 从未出现在列表中 - 排定员额 只在他们离开后出现
ScheduledPublishDate - 派设员额 (
IsPinned = true@)_} 總會在頁面上先出現 {1,} 完美於公告或主題內容
清除参数标签辅助工具
新建 ASP.NET 核心标签帮助器, 方便清除查询参数@:
[HtmlTargetElement("clear-param")]
public class ClearParamTagHelper : TagHelper
{
[HtmlAttributeName("name")]
public string? Name { get; set; }
[HtmlAttributeName("all")]
public bool All { get; set; } = false;
[HtmlAttributeName("exclude")]
public string Exclude { get; set; } = "";
// ... styling and Alpine.js integration ...
}
Razor 视图中的用法 @:
<!-- Clear a specific parameter -->
<clear-param name="startDate">Clear Date</clear-param>
<!-- Clear all parameters except language -->
<clear-param all="true" exclude="language">Clear All Filters</clear-param>
标签助手与 Alpine 集成 window.queryParamClearer 用于处理 URL 操作和 HTMX 交换@.}
注注注注注释注注说明附注注注,注注 注注的注 注 注 注注说明注注附注注 注的注注 注注说注注的附注注说明,注 注,注说明的注说明说明注 注说明注说明 注注 注附注注的注释注 注 注 注注释注说明注释注的说明注,说明注的 注 注说明说明说明 注 注的说明说明,说明说明的附注 注注附注说明注附注的注附注 注 注注释说明注注释的注注释 注注注释注释注,的注, 注注,注释注附注附注注注释说明的说明 注说明 注的注释 注 注附注说明说明注释说明说明附注 注说明的注释注释注释说明 注注释注释 注说明注释注释的说明注释 注注释 注的附注注释注注释,注注释附注注附注注释的 注注脚注注注@:_} 呼叫标签助手與更清晰的查詢參數都值得更深入的探索@.{I'}會覆蓋完整實施的細節},}包括Alpine}.}js 元件及標籤助手樣式, 於未來的一篇文章中,
我打到的臭虫 和修补 )
语言丢失日期范围@/}命令更改
- 由於我從零開始建立新的網址 @,}而不是從目前失去的網址@;
startDate/endDate. - 固定: 总是以
new URL(window.location.href)仅修改更改的部件@;}或从 Flatpickr 读取的日期,如果显示的话 @.language/order上面的更改处理器@. @%
- 由於我從零開始建立新的網址 @,}而不是從目前失去的網址@;
HTMX 交换后日历中显示的亮点@ ’ @ t刷新
- Cause:Flattpicker案 住在旧DOM上 死在交换.
- 在 Init @,}销毁任何已有实例@ , re‑accreate{,}然后预加亮可见月份的亮点@.}在互换@MS K7}(请见下面_).})后,还重加亮了显示月亮的亮點@ msk5}。
黑暗模式使日历看起来破碎
- Cause: 样式类变换, 但 Flatpartr 却改写了 ’ 重新绘制的日数@.
- 固定:观察
<html class>修改和调用fp.redraw().
挂接连接已丢弃过滤器环境
- 原因:
LinkUrl是否设置了“’”来包含某些情况下的查询@. - 在服务器上的 fix@: @% 1 设置
posts.LinkUrl = Url.Action("Index","Blog", new { startDate, endDate, language, orderBy, orderDir })因此,页眉可以正确撰写 URL .}
- 原因:
HTMX 互换安全性能@: @ re-}按需提供
如果您使用部分互换@ ,, 您的 JS需要是一等秘书 @ MS K1 在我的情况中我包扎了所有东西,
(function(){
function initFromRoot(root){ /* …the big function shown above… */ }
if(window.htmx){
document.body.addEventListener('htmx:afterSwap', function(ev){
const tgt = ev.detail.target;
// re-init if the content swapped includes the filters/content area
if(tgt && (tgt.id === 'content' || tgt.querySelector?.('#filters'))){
initFromRoot(document);
}
});
}
// also run on first load
initFromRoot(document);
})();
下一步是什么?
- 添加快速预设@( @ last @ 7 @ days @ MPK2 @ *30}今年=)#
- 以当地语言使用持久性语言创建页面
- 日期范围的键盘发件人
如果您发现过滤器有什么怪异之处, 请在浏览器@ + @ steps @ MS K2_ 感谢您与我一起提供狗食# !} 留下评论