Moments瞬间插件类似微博和朋友圈,官方地址为:

https://www.halo.run/store/apps/app-SnwWD

但是有一些主题不支持瞬间插件,也就是说没有内置moments.html。Thyuu主题采用内置的评论组件,不支持Halo原生的评论组件,另外i,该组件目前只适配文章和页面,不适配瞬间,所以本文重点不在评论(以后再说吧)。

瞬间地址:

https://ryanzm.cn/moments

如图:

首页及其他页面右下角按钮:

代码:

新建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}"> &#55357;&#56525; [[${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;
	}
}

首页和其他页面的悬浮按钮:

67fcc827acbd0.png

代码:

< 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评论到新评论组件中。

写在最后

茫茫互联网,缘分一线牵,您如果对网站内容感兴趣,您可以在下面这些地方找到订阅表单,或者直接在下方表单中提交您的邮箱以完成订阅。