主题:theme-fuwari V1.0.3
https://github.com/jiewenhuang/halo-theme-fuwari#readme
瞬间功能插件:瞬间 V1.15.0
https://www.halo.run/store/apps/app-SnwWD
使用方法:下载主题(或直接修改主题内templates/moments.html)
<!doctype html>
<html
xmlns:th="https://www.thymeleaf.org"
th:replace="~{modules/layout :: html(title = ${site.title}, hero = null, content = ~{::content}, head = null, footer = null, sidebar = null, contentClass = null, pageType = 'home')}"
>
<th:block th:fragment="content">
<!-- 标签列表部分 -->
<div class="mb-4">
<ul class="flex flex-wrap gap-2">
<li th:each="tag : ${tags}">
<a
th:href="|/moments?tag=${tag.name}|"
th:classappend="${#lists.contains(param.tag, tag.name) ? 'active' : ''}"
class="inline-block px-3 py-1 rounded-full bg-[var(--card-bg)] hover:bg-[var(--card-hover-bg)] transition"
>
<span th:text="${tag.name}"></span>
<span th:text="'(' + ${tag.momentCount} + ')'" class="ml-1 text-sm"></span>
</a>
</li>
</ul>
</div>
<!-- 瞬间列表部分 -->
<div class="space-y-8"> <!-- 增大整体间距 -->
<ul class="space-y-6"> <!-- 增大条目间距 -->
<li th:each="moment : ${moments.items}" th:with="content=${moment.spec.content}"
class="p-6 mb-6 rounded-xl bg-[var(--card-bg)] shadow-md hover:shadow-lg transition-shadow duration-300"> <!-- 更圆润的圆角和阴影效果 -->
<div class="px-4 py-4">
<div th:if="${not #strings.isEmpty(content.html)}" th:utext="${content.html}"
class="prose prose-sm max-w-none mb-4"></div> <!-- 增加底部间距 -->
<th:block th:if="${not #lists.isEmpty(content.medium)}" class="mt-4 grid gap-3"
th:classappend="${#lists.size(content.medium) > 1 ? 'grid-cols-2' : ''}">
<th:block th:each="momentItem : ${content.medium}">
<img th:if="${momentItem.type.name == 'PHOTO'}" th:src="${momentItem.url}"
class="rounded-lg w-full h-auto object-cover shadow-sm hover:shadow-md transition-shadow" />
<video th:if="${momentItem.type.name == 'VIDEO'}" th:src="${momentItem.url}"
class="rounded-lg w-full shadow-sm hover:shadow-md transition-shadow" controls></video>
<audio th:if="${momentItem.type.name == 'AUDIO'}" th:src="${momentItem.url}"
class="w-full rounded-lg bg-[var(--card-hover-bg)] p-2" controls="true"></audio>
</th:block>
</th:block>
<div class="flex mt-2 gap-3 text-base text-black/30 transition dark:text-white/30">
<div class="flex items-center justify-center space-x-2">
<div class="flex space-x-2 items-center justify-center">
<span class="icon-[material-symbols--calendar-today-outline-rounded] mr-1 text-lg text-[var(--primary-color)]"></span>
</div>
<span class="text-sm font-medium" th:text="${#temporals.format(moment.metadata.creationTimestamp, 'yyyy-MM-dd')}"></span>
</div>
<div>|</div>
<div class="flex items-center justify-center">
<span class="mr-1 text-lg">
<svg viewBox="0 0 24 24" width="1.0em" height="1.0em">
<path fill="currentColor" d="M16.5 3C19.538 3 22 5.5 22 9c0 7-7.5 11-10 12.5C9.5 20 2 16 2 9c0-3.5 2.5-6 5.5-6C9.36 3 11 4 12 5c1-1 2.64-2 4.5-2m-3.566 15.604a27 27 0 0 0 2.42-1.701C18.335 14.533 20 11.943 20 9c0-2.36-1.537-4-3.5-4c-1.076 0-2.24.57-3.086 1.414L12 7.828l-1.414-1.414C9.74 5.57 8.576 5 7.5 5C5.56 5 4 6.657 4 9c0 2.944 1.666 5.533 4.645 7.903c.745.593 1.54 1.146 2.421 1.7c.299.189.595.37.934.572c.339-.202.635-.383.934-.571"></path>
</svg>
</span>
<!-- 点赞数 -->
<span class="text-sm font-medium" th:text="${moment.stats.upvote}"></span>
</div>
<div>|</div>
<div class="flex items-center justify-center">
<span class="icon-[material-symbols--ar-stickers-outline] mr-1 text-lg"></span>
<!-- 通过的评论数 -->
<span class="text-sm font-medium" th:text="${moment.stats.approvedComment}"></span>
</div>
</div>
</div>
</li>
</ul>
<!-- 分页部分 -->
<div th:if="${moments.hasPrevious() || moments.hasNext()}"
class="flex justify-center gap-4 mt-8">
<a th:href="@{${moments.prevUrl}}" th:if="${moments.hasPrevious()}"
class="px-4 py-2 rounded-lg bg-[var(--card-bg)] hover:bg-[var(--primary-color)] hover:text-white transition-colors">
<span>上一页</span>
</a>
<span th:text="'第 ' + ${moments.page} + ' 页'" class="px-4 py-2 text-sm text-neutral-500"></span>
<a th:href="@{${moments.nextUrl}}" th:if="${moments.hasNext()}"
class="px-4 py-2 rounded-lg bg-[var(--card-bg)] hover:bg-[var(--primary-color)] hover:text-white transition-colors">
<span>下一页</span>
</a>
</div>
</div>
</th:block>
</html>

改一版了:
<!doctype html>
<html
xmlns:th="https://www.thymeleaf.org"
th:replace="~{modules/layout :: html(title = ${site.title}, hero = null, content = ~{::content}, head = ~{::head}, footer = null, sidebar = null, contentClass = null, pageType = 'home')}"
>
<th:block th:fragment="head">
<style>
/* 一行最多两个 */
.moments-wall {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
/* 默认:一行两个(50%宽度) */
.moment-item {
flex: 1 1 calc(50% - 8px); /* 减去gap的一半 */
/* 最小宽度:刚好容纳九宫格(3个小图 + 间距) */
min-width: min(100%, 420px); /* 约3*136px + gaps,或手机屏幕时100% */
max-width: calc(50% - 8px); /* 默认最多50% */
}
/* 有长图的卡片:扩展到100%宽度(占一整行) */
.moment-item.has-long-image {
flex: 1 1 100%;
max-width: 100%;
}
/* 卡片内部 */
.moment-card-inner {
padding: 16px;
border-radius: 12px;
background: var(--card-bg);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: transform 0.2s, box-shadow 0.2s;
overflow: hidden;
}
.moment-card-inner:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
/* Markdown 内容 */
.moment-content {
width: 100%;
margin-bottom: 12px;
word-wrap: break-word;
overflow: hidden;
}
/* 长图样式:占满卡片宽度 */
.moment-content img {
width: 100%;
height: auto;
border-radius: 8px;
display: block;
}
/* 九宫格:始终填满卡片宽度 */
.media-grid {
display: grid;
gap: 4px;
width: 100%;
}
/* 1张图:占满 */
.moment-item[data-count="1"] .media-grid {
grid-template-columns: 1fr;
}
.moment-item[data-count="1"] .media-item {
aspect-ratio: 16/10;
}
/* 2张图:并排 */
.moment-item[data-count="2"] .media-grid {
grid-template-columns: repeat(2, 1fr);
}
/* 3-4张图:2x2 */
.moment-item[data-count="3"] .media-grid,
.moment-item[data-count="4"] .media-grid {
grid-template-columns: repeat(2, 1fr);
}
/* 5-9张图:3x3 九宫格 */
.moment-item[data-count="5"] .media-grid,
.moment-item[data-count="6"] .media-grid,
.moment-item[data-count="7"] .media-grid,
.moment-item[data-count="8"] .media-grid,
.moment-item[data-count="9"] .media-grid {
grid-template-columns: repeat(3, 1fr);
}
/* 媒体项 */
.media-item {
position: relative;
aspect-ratio: 1;
overflow: hidden;
border-radius: 4px;
background-color: var(--card-hover-bg, #f5f5f5);
cursor: pointer;
}
.media-item img,
.media-item video {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
/* 视频播放按钮 */
.media-item.video-item::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 36px;
height: 36px;
background: rgba(0, 0, 0, 0.6);
border-radius: 50%;
z-index: 1;
}
.media-item.video-item::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-35%, -50%);
width: 0;
height: 0;
border-left: 10px solid white;
border-top: 7px solid transparent;
border-bottom: 7px solid transparent;
z-index: 2;
}
/* 底部信息 */
.moment-meta {
display: flex;
gap: 12px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--border-color, rgba(0,0,0,0.1));
font-size: 0.85rem;
color: var(--text-muted);
flex-wrap: wrap;
}
/* 手机:一列 */
@media (max-width: 860px) {
.moment-item,
.moment-item.has-long-image {
flex: 1 1 100%;
max-width: 100%;
min-width: auto;
}
}
</style>
</th:block>
<th:block th:fragment="content">
<!-- 标签列表 -->
<div class="">
<ul class="flex flex-wrap gap-2 justify-center">
<li th:each="tag : ${tags}">
<a
th:href="|/moments?tag=${tag.name}|"
th:classappend="${#lists.contains(param.tag, tag.name) ? 'active' : ''}"
class="inline-block px-3 py-1 rounded-full bg-[var(--card-bg)] hover:bg-[var(--card-hover-bg)] transition text-sm"
>
<span th:text="${tag.name}"></span>
<span th:text="'(' + ${tag.momentCount} + ')'" class="ml-1 opacity-60"></span>
</a>
</li>
</ul>
</div>
<!-- 瞬间墙:一行最多两个 -->
<div class="moments-wall">
<th:block th:each="moment : ${moments.items}" th:with="content=${moment.spec.content}">
<!-- 关键:检测是否有长图(<img标签),有则添加has-long-image类,扩展到整行 -->
<article class="moment-item"
th:data-count="${#lists.size(content.medium)}"
th:classappend="${not #strings.isEmpty(content.html) and #strings.contains(content.html, '<img') ? 'has-long-image' : ''}"
th:if="${not #lists.isEmpty(content.medium)}">
<div class="moment-card-inner">
<!-- Markdown内容 -->
<div th:if="${not #strings.isEmpty(content.html)}"
th:utext="${content.html}"
class="moment-content prose prose-sm">
</div>
<!-- 九宫格 -->
<div class="media-grid">
<th:block th:each="momentItem : ${content.medium}">
<div th:if="${momentItem.type.name == 'PHOTO'}" class="media-item">
<img th:src="${momentItem.url}" loading="lazy" />
</div>
<div th:if="${momentItem.type.name == 'VIDEO'}" class="media-item video-item">
<video th:src="${momentItem.url}" preload="metadata"></video>
</div>
</th:block>
</div>
<div class="moment-meta">
<span class="flex items-center gap-1">
<span class="icon-[material-symbols--calendar-today-outline-rounded] text-[var(--primary-color)]"></span>
<span th:text="${#temporals.format(moment.metadata.creationTimestamp, 'MM-dd')}"></span>
</span>
<span class="flex items-center gap-1">
<svg viewBox="0 0 24 24" width="1em" height="1em" class="text-red-500">
<path fill="currentColor" d="M16.5 3C19.538 3 22 5.5 22 9c0 7-7.5 11-10 12.5C9.5 20 2 16 2 9c0-3.5 2.5-6 5.5-6C9.36 3 11 4 12 5c1-1 2.64-2 4.5-2z"/>
</svg>
<span th:text="${moment.stats.upvote}"></span>
</span>
<span class="flex items-center gap-1">
<svg viewBox="0 0 24 24" width="1em" height="1em">
<path fill="currentColor" d="M2 8.994A5.99 5.99 0 0 1 8 3h8c3.313 0 6 2.695 6 5.994V21H8c-3.313 0-6-2.695-6-5.994zM20 19V8.994A4.004 4.004 0 0 0 16 5H8a3.99 3.99 0 0 0-4 3.994v6.012A4.004 4.004 0 0 0 8 19zm-6-8h2v2h-2zm-6 0h2v2H8z"></path>
</svg>
<span th:text="${moment.stats.approvedComment}"></span>
</span>
</div>
</div>
</article>
<!-- 纯文字瞬间(没有九宫格) -->
<article class="moment-item has-long-image" data-count="0" th:if="${#lists.isEmpty(content.medium)}">
<div class="moment-card-inner">
<div th:if="${not #strings.isEmpty(content.html)}"
th:utext="${content.html}"
class="moment-content prose prose-sm">
</div>
<div class="moment-meta">
<span class="flex items-center gap-1">
<span class="icon-[material-symbols--calendar-today-outline-rounded] text-[var(--primary-color)]"></span>
<span th:text="${#temporals.format(moment.metadata.creationTimestamp, 'MM-dd')}"></span>
</span>
<span class="flex items-center gap-1">
<svg viewBox="0 0 24 24" width="1em" height="1em" class="text-red-500">
<path fill="currentColor" d="M16.5 3C19.538 3 22 5.5 22 9c0 7-7.5 11-10 12.5C9.5 20 2 16 2 9c0-3.5 2.5-6 5.5-6C9.36 3 11 4 12 5c1-1 2.64-2 4.5-2z"/>
</svg>
<span th:text="${moment.stats.upvote}"></span>
</span>
<span class="flex items-center gap-1">
<svg viewBox="0 0 24 24" width="1em" height="1em">
<path fill="currentColor" d="M2 8.994A5.99 5.99 0 0 1 8 3h8c3.313 0 6 2.695 6 5.994V21H8c-3.313 0-6-2.695-6-5.994zM20 19V8.994A4.004 4.004 0 0 0 16 5H8a3.99 3.99 0 0 0-4 3.994v6.012A4.004 4.004 0 0 0 8 19zm-6-8h2v2h-2zm-6 0h2v2H8z"></path>
</svg>
<span th:text="${moment.stats.approvedComment}"></span>
</span>
</div>
</div>
</article>
</th:block>
</div>
<!-- 分页 -->
<div th:if="${moments.hasPrevious() || moments.hasNext()}"
class="flex justify-center gap-4 mt-4 mb-4">
<a th:href="@{${moments.prevUrl}}" th:if="${#strings.isEmpty(param.tag) ? moments.hasPrevious() : false}"
class="px-4 py-2 rounded-lg bg-[var(--card-bg)] hover:bg-[var(--primary-color)] hover:text-white transition-colors">
上一页
</a>
<span th:text="'第 ' + ${moments.page} + ' 页'" class="px-4 py-2 text-sm opacity-60"></span>
<a th:href="@{${moments.nextUrl}}" th:if="${moments.hasNext()}"
class="px-4 py-2 rounded-lg bg-[var(--card-bg)] hover:bg-[var(--primary-color)] hover:text-white transition-colors">
下一页
</a>
</div>
</th:block>
</html>
