Moments瞬间插件类似微博和朋友圈,官方地址为:
但是有一些主题不支持瞬间插件,也就是说没有内置moments.html。Thyuu主题采用内置的评论组件,不支持Halo原生的评论组件,另外i,该组件目前只适配文章和页面,不适配瞬间,所以本文重点不在评论(以后再说吧)。
瞬间地址:
如图:
首页及其他页面右下角按钮:
代码:
新建Moments.html 位置在主题文件夹内
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org" th:replace="~{modules/layout :: layout(
_title = '瞬间',
menu_site_title = '瞬间',
_content = ~{::content},
_head = ~{::head},
page_js = null,
body_class = 'page-template page-template-moments page')}">
<!-- ✅ 页面 <head> 片段 -->
<head></head>
<body>
<th:block th:fragment="head">
<style>
.tags {
margin: 0.5rem 0 1.5rem;
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
}
.tag {
display: inline-block;
padding: 3px 10px;
border: 1px solid #ddd;
border-radius: 10px;
font-size: 11px;
color: #888;
background-color: #f8f8f8;
margin: 10px 0px;
}
.tag::before {
content: "#";
margin-right: 2px;
}
</style>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fancyapps/ui/dist/fancybox.css" />
<script src="https://cdn.jsdelivr.net/npm/@fancyapps/ui/dist/fancybox.umd.js"></script>
</th:block>
<!-- ✅ 页面内容 -->
<th:block th:fragment="content">
<main id="main" class="site-main" role="main">
<div class="moments-wrapper">
<!-- 顶部背景与头像 -->
<div class="moment-top-bg" style="position: relative;">
<img class="moment-top-bg" src="/upload/bg-3.jpg" alt="背景图" />
<div class="moment-top-name">
又见梅林
</div>
<img class="moment-top-avater" src="https://img.ryanzm.cn/2025/01/20/678de9c70a3c8.png" alt="头像" />
</div>
<!-- 每条 moment -->
<div th:each="moment : ${moments.items}" class="moment-card">
<hr style="border: none; border-top: 1px solid #eee;" />
<div class="moment-meta">
<img th:src="@{${moment.owner.avatar}}" alt="avatar" />
<div class="info">
<div class="name">
[[${moment.owner.displayName}]]
</div>
<div>
<time th:text="${#dates.format(moment.spec.releaseTime, 'yyyy-MM-dd HH:mm')}"></time>
</div>
</div>
</div>
<div class="moment-content" th:utext="${moment.spec.content.html}">
[[${moment.spec.content.plain}]]
</div>
<!-- 媒体区域:图片、视频、音频 -->
<div class="moment-media" th:if="${not #lists.isEmpty(moment.spec.content.medium)}">
<div class="img-grid">
<a th:each="media : ${moment.spec.content.medium}" th:if="${media.type.name == 'PHOTO'}" data-fancybox="gallery" th:href="${media.url}"> <img th:src="${media.url}" alt="图片" /> </a>
</div>
<th:block th:each="media : ${moment.spec.content.medium}">
<video th:if="${media.type.name == 'VIDEO'}" th:src="${media.url}" controls=""></video>
<audio th:if="${media.type.name == 'AUDIO'}" th:src="${media.url}" controls=""></audio>
</th:block>
</div>
<!-- 底部区域 -->
<div class="moment-footer">
<span th:if="${moment.metadata.annotations.mylocal != null}"> �� [[${moment.metadata.annotations.mylocal}]] </span>
<!-- 展开按钮和评论 -->
<div class="moment-actions">
<button class="moment-action-toggle"> <i class="iconfont icon-gengduo"></i> </button>
<div class="moment-action-popup">
<button class="moment-comment-btn" th:attr="data-id=${moment.metadata.name}"> <i class="icon-pinglun"></i> <span class="moment-comment-label">评论</span> <span class="moment-comment-count" th:attr="data-path='/moments/' + ${moment.metadata.name}"></span> </button>
</div>
</div>
</div>
</div>
<!-- 分页 -->
<div th:if="${moments.hasPrevious() or moments.hasNext()}" class="pagination" style="padding: 20px; display: flex; justify-content: center; background: white; border-radius: 0 0 20px 20px;">
<th:block th:replace="~{modules/widgets/pagination :: page('/moments', ${moments})}" />
</div>
</div>
</main>
<script type="module">
import { init, commentCount } from 'https://unpkg.com/@waline/client@v3/dist/waline.js';
// 三点展开操作按钮
document.querySelectorAll('.moment-action-toggle').forEach(btn => {
btn.addEventListener('click', () => {
const container = btn.closest('.moment-actions');
container.classList.toggle('open');
});
});
// 点赞逻辑:每条每人每天只能点一次
document.querySelectorAll('.moment-like-btn').forEach(btn => {
const id = btn.getAttribute('data-id');
const countSpan = btn.querySelector('.like-count');
const today = new Date().toISOString().split('T')[0];
const key = `like_${id}_${today}`;
const storedCount = localStorage.getItem(`${key}_count`);
if (localStorage.getItem(key)) {
btn.classList.add('liked');
countSpan.textContent = storedCount || 1;
}
btn.addEventListener('click', () => {
if (localStorage.getItem(key)) {
alert('今天已经点过赞啦!');
return;
}
btn.classList.add('liked');
localStorage.setItem(key, "1");
let newCount = parseInt(countSpan.textContent) + 1;
countSpan.textContent = newCount;
localStorage.setItem(`${key}_count`, newCount.toString());
});
});
// 评论按钮触发弹出框
document.querySelectorAll('.moment-comment-btn').forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.getAttribute('data-id');
showWalinePopup(id);
});
});
// 显示 Waline 弹窗
function showWalinePopup(id) {
const popupId = `comment-popup-${id}`;
const elId = `waline-${id}`;
// ✅ 新增:每次点击时,先销毁旧弹窗
const oldPopup = document.querySelector(`#${popupId}`);
if (oldPopup) oldPopup.remove();
// ✅ 然后重新创建弹窗
const popup = document.createElement('div');
popup.id = popupId;
popup.className = 'moment-comment-popup';
popup.innerHTML = `
<div class="close-btn">
<i class="iconfont icon-guanbi comment-popup-close"></i>
</div>
<div id="${elId}" class="waline-inner"></div>
`;
document.body.appendChild(popup);
// ✅ 绑定关闭按钮
popup.querySelector('.comment-popup-close').addEventListener('click', () => {
popup.style.display = 'none';
});
// ✅ 初始化 Waline(无登录,无图片上传)
window.WalineV3.init({
el: `#${elId}`,
serverURL: 'https://m.ryanzm.cn/',
reaction: false,
path: `/moments/${id}`,
login: 'disable',
imageUploader: false,
copyright: false,
});
}
// 评论数量统计并隐藏为 0 的
commentCount({
serverURL: 'https://m.ryanzm.cn/',
selector: '.moment-comment-count',
callback: (el, count) => {
if (parseInt(count) < 1) {
el.style.display = 'none';
} else {
el.textContent = count;
}
}
});
document.querySelectorAll('.img-grid').forEach(grid => {
const imgs = grid.querySelectorAll('img');
if (imgs.length === 1) {
const img = imgs[0];
const classifyImage = () => {
const width = img.naturalWidth;
const height = img.naturalHeight;
if (width > height) {
grid.classList.add('single-img', 'landscape');
} else {
grid.classList.add('single-img', 'portrait');
}
};
if (img.complete) {
classifyImage();
} else {
img.onload = classifyImage;
}
}
});
</script>
<script>
window.initMomentsScript = function () {
// 三点展开按钮
document.querySelectorAll('.moment-action-toggle').forEach(btn => {
btn.addEventListener('click', () => {
const container = btn.closest('.moment-actions');
container.classList.toggle('open');
});
});
// 点赞逻辑(略:可照你原本逻辑复制进来)
// 评论按钮弹出
document.querySelectorAll('.moment-comment-btn').forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.getAttribute('data-id');
showWalinePopup(id);
});
});
// 评论数统计
if (window.Waline && window.Waline.commentCount) {
window.Waline.commentCount({
serverURL: 'https://m.ryanzm.cn/',
selector: '.moment-comment-count',
callback: (el, count) => {
if (parseInt(count) < 1) {
el.style.display = 'none';
} else {
el.textContent = count;
}
}
});
}
// 单图样式
document.querySelectorAll('.img-grid').forEach(grid => {
const imgs = grid.querySelectorAll('img');
if (imgs.length === 1) {
const img = imgs[0];
const classifyImage = () => {
const width = img.naturalWidth;
const height = img.naturalHeight;
if (width > height) {
grid.classList.add('single-img', 'landscape');
} else {
grid.classList.add('single-img', 'portrait');
}
};
if (img.complete) {
classifyImage();
} else {
img.onload = classifyImage;
}
}
});
};
</script>
</th:block>
</body>
</html>
/* moment-放在后台自定义CSS中 */
.moments-wrapper {
max-width: 720px;
margin: 2rem auto;
padding: 0 1rem;
}
.moment-card {
background: #fff;
padding: 0em 2.5em 2.5em 2.5em;
}
.moment-meta {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 0.6rem;
padding-top: 2.5em;
}
.moment-meta img {
width: 36px;
height: 36px;
border-radius: 50%;
}
.moment-meta .info {
font-size: 14px;
color: #666;
}
.moment-meta .info .name {
font-weight: bold;
font-size: 16px;
color: #333;
}
.moment-content {
font-size: 15px;
color: #333;
white-space: pre-wrap;
line-height: 1.7;
margin-bottom: 0.5rem;
max-width: 85%;
}
.moment-footer {
font-size: 13px;
color: #999;
position: relative;
}
.moment-media {
margin: 10px 0;
max-width: 85%;
}
.img-grid {
display: grid;
gap: 6px;
}
.img-grid:has(a:nth-child(1):nth-last-child(1)) {
grid-template-columns: 1fr;
}
.img-grid:has(a:nth-child(1):nth-last-child(2)),
.img-grid:has(a:nth-child(1):nth-last-child(4)) {
grid-template-columns: 1fr 1fr;
}
.img-grid:has(a:nth-child(1):nth-last-child(3)),
.img-grid:has(a:nth-child(1):nth-last-child(n+5)) {
grid-template-columns: 1fr 1fr 1fr;
}
.img-grid a {
display: block;
position: relative;
width: 100%;
aspect-ratio: 1 / 1;
overflow: hidden;
border-radius: 8px;
}
.img-grid img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.moment-tags {
margin: 0.5rem 0 1.5rem;
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
}
.tag {
display: inline-block;
padding: 3px 10px;
border: 1px solid #ddd;
border-radius: 10px;
font-size: 13px;
color: #666;
background-color: #f8f8f8;
margin: 10px 0px;
}
.tag::before {
content: "#";
margin-right: 2px;
}
.moment-actions {
position: absolute;
right: 16px;
bottom: 0px;
}
.moment-action-popup {
display: none;
position: absolute;
right: 40px;
bottom: -5px;
background: #fff;
border: 1px solid #eee;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
border-radius: 6px;
z-index: 10;
padding: 5px;
}
.moment-actions.open .moment-action-popup {
display: block;
}
.moment-like-btn,
.moment-comment-btn {
display: inline-flex;
align-items: center;
gap: 6px;
background: none;
border: none;
cursor: pointer;
color: #333;
padding: 4px 8px;
font-size: 14px;
}
.moment-comment-label {
white-space: nowrap;
}
.moment-action-toggle {
padding: 4px 7px;
font-size: 20px;
color: #999;
cursor: pointer;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
border-radius: 5px;
/* 不要圆角 */
box-shadow: none;
/* 不要阴影 */
}
.moment-action-toggle:hover {
color: #333;
}
/* 评论弹出框 */
.moment-comment-popup {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 90%;
max-width: 420px;
max-height: 80vh;
background: rgba(255, 255, 255, 0.75);
/* 半透明白色 */
backdrop-filter: blur(12px);
/* 毛玻璃效果 */
-webkit-backdrop-filter: blur(12px);
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
z-index: 9999;
overflow: hidden;
display: flex;
flex-direction: column;
padding: 0;
}
.img-grid.single-img.landscape a {
width: 70%;
}
.img-grid.single-img.portrait a {
width: 40%;
}
.img-grid.single-img a {
aspect-ratio: auto !important;
height: auto !important;
width: auto;
max-width: 100%;
/* 防止超出屏幕 */
}
.img-grid.single-img {
display: block;
text-align: center;
}
.img-grid.single-img img {
object-fit: contain !important;
height: auto !important;
width: 100%;
display: block;
}
.moment-top-avater {
position: absolute;
bottom: -25px;
right: 4%;
width: 10%;
border-radius: 12%;
border: 3px solid #fff;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.1);
}
.moment-top-name {
position: absolute;
bottom: 7px;
right: 17%;
color: white;
font-size: 1.2em;
font-weight: bold;
text-shadow: 0 0 4px rgba(0, 0, 0, 0.4);
}
.moment-top-bg {
border-radius: 16px 16px 0 0;
width: 100%;
height: 300px;
object-fit: cover;
display: block;
}
/* 评论框顶部关闭按钮容器 */
.moment-comment-popup .close-btn {
text-align: right;
padding: 8px 12px 0;
}
/* 评论内容容器支持滚动 */
.moment-comment-popup .waline-inner {
flex: 1;
overflow-y: auto;
padding: 0 16px 16px;
}
.comment-popup-close {
font-size: 20px;
color: #666;
cursor: pointer;
padding: 10px 16px;
transition: 0.2s;
}
.comment-popup-close:hover {
color: #111;
}
@media screen and (max-width: 768px) {
.moments-wrapper {
width: 100% !important;
max-width: 100% !important;
padding: 0 12px;
/* 给一点点边距 */
box-sizing: border-box;
}
.moment-content,
.moment-media {
max-width: 100%;
}
.img-grid.single-img.landscape a {
width: 80%
}
.img-grid.single-img.portrait a {
width: 50%;
}
.moment-top-bg {
height: 200px;
}
.moment-card {
padding: 0em 1.5em 1.5em 1.5em;
}
.moment-meta {
padding-top: 1.5em;
}
.moment-top-avater {
width: 15%;
}
.moment-top-name {
bottom: 2px;
right: 22%;
font-size: 1em;
}
.moment-actions {
right: 0px;
}
}
首页和其他页面的悬浮按钮:
代码:
< script >
// 弹出iframe只选择.moments-wrapper
if (window.location.hash === "#embed") {
document.addEventListener("DOMContentLoaded",
function() {
const wrapper = document.querySelector('.moments-wrapper');
if (wrapper) {
const clone = wrapper.cloneNode(true); // ⚠️ 先 clone
document.body.innerHTML = ''; // 再清空 body
document.body.appendChild(clone); // 添加克隆内容
document.body.style.margin = '0';
document.body.style.background = '#f8f8f8';
if (window.initMomentsScript) {
window.initMomentsScript();
}
if (window.WalineV3 && window.WalineV3.commentCount) {
window.WalineV3.commentCount({
serverURL: 'https://m.ryanzm.cn/',
selector: '.moment-comment-count',
callback: (el, count) = >{
if (parseInt(count) < 1) {
el.style.display = 'none';
} else {
el.textContent = count;
}
}
});
}
}
});
} < /script>
<!-- 👇 全站右下角 moments 弹窗入口 -->
<style>
.rzm-moment-btn {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 9999;
width: 60px;
height: 70px;
background-color: white; / * 方形白底 * /
border-radius: 12px; / * 圆角方形 * /
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 12px;
color: #333;
cursor: pointer;
}
.rzm-moment-icon {
width: 26px;
height: 26px;
object-fit: contain;
display: block;
margin-bottom: 4px;
margin-top: 10px;
}
.rzm-moment-label {
font-size: 12px;
color: #333;
}
/ * 可选:悬停样式 * /
.rzm-moment-btn:hover {
background-color: #f0f0f0;
}
.rzm-moment-popup {
display: none;
position: fixed;
bottom: 100px;
right: 20px;
width: 360px;
height: 75%;
max-width: 100%;
background: white;
border-radius: 20px;
box-shadow: 0 8px 24px rgba(0,0,0,0.25);
overflow: hidden;
z-index: 9998;
}
.rzm-moment-popup iframe {
width: 100%;
height: 100%;
border: none;
}
.rzm-moment-close {
position: absolute;
top: 8px;
right: 12px;
font-size: 20px;
color: #999;
cursor: pointer;
z-index: 10;
}
/ * 🔴小红点 * /
.rzm-moment-dot {
position: absolute;
top: 9px;
right: 9px;
width: 8px;
height: 8px;
background-color: red;
border-radius: 50%;
}
@media screen and (max-width: 768px) {
.rzm-moment-btn {
bottom:60px;
}
}
</style >
<!--🔘按钮HTML-->
<div class = "rzm-moment-btn"title = "查看瞬间" >
<img src = "/upload/朋友圈.svg"alt = "瞬间图标"class = "rzm-moment-icon" / >
<span class = "rzm-moment-label" > 瞬间 < /span>
<!-- 🔴 红点通知 -->
<span class="rzm-moment-dot"></span > </div>
<!-- 📱 弹窗 HTML -->
<div class="rzm-moment-popup">
<div class="rzm-moment-close">✖</div > <iframe src = "/moments#embed"loading = "lazy"title = "Moments" > </iframe>
</div >
<!--💡弹窗逻辑--><script > document.addEventListener('DOMContentLoaded',
function() {
const btn = document.querySelector('.rzm-moment-btn');
const popup = document.querySelector('.rzm-moment-popup');
const close = document.querySelector('.rzm-moment-close');
btn.addEventListener('click', () = >{
if (popup.style.display === 'block') {
popup.style.display = 'none';
} else {
popup.style.display = 'block';
}
});
close.addEventListener('click', () = >{
popup.style.display = 'none';
});
}); < /script>
<script type="module">
import { init, commentCount } from 'https:/ / unpkg.com / @waline / client@v3 / dist / waline.js ';
// 将模块函数挂载到全局
window.WalineV3 = { init, commentCount };
commentCount({
serverURL: 'https: //m.ryanzm.cn/',
selector: '.moment-comment-count',
callback: (el, count) = >{
if (parseInt(count) < 1) {
el.style.display = 'none';
} else {
el.textContent = count;
}
}
});
</script>
<!--waline评论组件-->
<script>
function showWalinePopup(id) {
const popupId = `comment-popup-${id}`;
const elId = `waline-${id}`;
/ / ✅每次都销毁旧的弹窗,防止滚动卡死const oldPopup = document.querySelector(`#$ {
popupId
}`);
if (oldPopup) oldPopup.remove();
const popup = document.createElement('div');popup.id = popupId;popup.className = 'moment-comment-popup';
popup.innerHTML = ` < div class = "close-btn" > <i class = "iconfont icon-guanbi comment-popup-close" > </i>
</div > <div id = "${elId}"class = "waline-inner" > </div>
`;
document.body.appendChild(popup);
/ / ✅关闭按钮popup.querySelector('.comment-popup-close').addEventListener('click', () = >{
popup.style.display = 'none';
});
// ✅ 初始化 Waline(无登录、无图片上传)
window.WalineV3.init({
el: `#$ {
elId
}`,
serverURL: 'https://m.ryanzm.cn/',
reaction: false,
path: ` / moments / $ {
id
}`,
login: 'disable',
imageUploader: false,
copyright: false,
});
}
< /script>
注意,我在这里使用了过渡性质的Waline作为评论组件,有关评论的代码都不应该被复用,因为我也不知道以后thyuu会不会把内置的评论组件适配瞬间插件。以后如果万一有一天thyuu迎来更新(大概率不会,因为瞬间这个功能在很多人看来非必要),我再更新到thyuu评论组件,并迁移现有的waline评论到新评论组件中。
写在最后
茫茫互联网,缘分一线牵,您如果对网站内容感兴趣,您可以在下面这些地方找到订阅表单,或者直接在下方表单中提交您的邮箱以完成订阅。