Responsive Image

实现功能:
  • 标签筛选
  • Waline 评论区
  • 显示每条 moment 评论数量
  • Step 1 - 添加 moments 页面模板

    layouts/moments/list.html

    html
      1{{ define "main" }}
      2
      3    <!-- 如果需要引入 moments.css,请保持路径一致或根据自己项目结构调整 -->
      4    {{ $css := resources.Get "css/extended/moments.css" | minify | fingerprint }}
      5    <link
      6        crossorigin="anonymous"
      7        href="{{ $css.RelPermalink }}"
      8        integrity="{{ $css.Data.Integrity }}"
      9        rel="stylesheet"
     10    />
     11
     12    {{ $dateformat := .Params.DateFormat }}
     13
     14    <article class="post-single">
     15        <header class="page-header">
     16            <!-- 可根据需求添加页面标题、描述等 -->
     17            <!-- <h1>{{ .Title }}</h1> -->
     18        </header>
     19
     20        <div class="tags-filter">
     21            <ul>
     22                <li><a href="#" class="tag-filter all-tags">全部</a></li> <!-- "全部"选项 -->
     23                {{ $tags := slice }} <!-- 用于存储所有标签 -->
     24                
     25                {{ range .Pages }}
     26                    {{ range .Params.tags }}
     27                        {{ if not (in $tags .) }}
     28                            {{ $tags = $tags | append . }}
     29                        {{ end }}
     30                    {{ end }}
     31                {{ end }}
     32        
     33                <!-- 按字母顺序排序标签 -->
     34                {{ $tags = $tags | sort }}
     35        
     36                {{ range $tags }}
     37                    <li><a href="#" class="tag-filter">{{ . }}</a></li> <!-- 标签项 -->
     38                {{ end }}
     39            </ul>
     40        </div>        
     41         
     42        <div class="post-content">
     43            <div class="moments-list">
     44                {{ range .Pages }}
     45                    {{ if .Content }}
     46                        <!-- 卡片容器 -->
     47                        <div class="moment-card">
     48                            <!-- 头部:头像 + 作者名 -->
     49                            <div class="moment-header">
     50                                <div class="left-content">
     51                                    <img
     52                                        src="{{ site.Params.label.avatar }}"
     53                                        alt="{{ site.Params.author }}"
     54                                        class="moment-avatar"
     55                                    >
     56                                    <span class="moment-author">
     57                                        {{ site.Params.author }}
     58                                    </span>
     59                                </div>
     60                            </div>
     61
     62                            <!-- 动态主体内容(Hugo 渲染后的 .Content) -->
     63                            <div class="moment-body">
     64                                <div class="moment-content-wrapper">
     65                                    {{ .Content | safeHTML }}
     66                                </div>
     67                                <div class="moment-loading">
     68                                    <div class="loading-spinner"></div>
     69                                    <div class="skeleton-content">
     70                                        <div class="skeleton-line" style="width: 90%"></div>
     71                                        <div class="skeleton-line" style="width: 75%"></div>
     72                                        <div class="skeleton-line" style="width: 60%"></div>
     73                                    </div>
     74                                </div>
     75                                <div class="moment-error" style="display: none;">
     76                                    <span>图片加载失败</span>
     77                                    <button onclick="retryLoad(this)">重试</button>
     78                                </div>
     79                            </div>
     80
     81                            <!-- 标签(如果有) -->
     82                            {{ if .Params.tags }}
     83                                <div class="moment-tags">
     84                                    {{ range $tag := .Params.tags }}
     85                                        <span class="moment-tag">{{ $tag }}</span>
     86                                    {{ end }}
     87                                </div>
     88                            {{ end }}
     89
     90                            <!-- 底部:时间 + 评论按钮 -->
     91                            <div class="moment-bottom">
     92                                <div class="moment-time">
     93                                    <span>
     94                                        {{ .Param "date" | time.Format (default site.Params.DateFormat $dateformat) }}
     95                                    </span>
     96                                </div>
     97                                <!-- 如果没有 hideComment 参数,则显示评论按钮 -->
     98                                {{ if not (.Param "hideComment") }}
     99                                <button
    100                                    class="moment-comment-btn"
    101                                    onclick="showComment(this)"
    102                                    data-slug="{{ .Param "slug" }}"
    103                                    data-path="{{ .Param "slug" }}"
    104                                >
    105                                    <!-- 评论图标 SVG -->
    106                                    <svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M281.535354 387.361616c-31.806061 0-57.664646 26.763636-57.664647 59.733333 0 32.969697 25.858586 59.733333 57.664647 59.733334s57.664646-26.763636 57.664646-59.733334c0-33.09899-25.858586-59.733333-57.664646-59.733333z m230.529292 0c-31.806061 0-57.664646 26.763636-57.664646 59.733333 0 32.969697 25.729293 59.733333 57.664646 59.733334 31.806061 0 57.535354-26.763636 57.535354-59.733334 0-33.09899-25.858586-59.733333-57.535354-59.733333z m230.4 0c-31.806061 0-57.664646 26.763636-57.664646 59.733333 0 32.969697 25.858586 59.733333 57.664646 59.733334s57.664646-26.763636 57.664647-59.733334c-0.129293-33.09899-25.858586-59.733333-57.664647-59.733333z m115.2-270.222222H166.335354c-63.612121 0-115.2 53.527273-115.2 119.59596v390.981818c0 65.939394 52.751515 126.836364 117.785858 126.836363h175.579798c30.513131 32.581818 157.220202 149.979798 157.220202 149.979798 5.559596 5.818182 14.739394 5.818182 20.29899 0 0 0 92.832323-91.410101 153.212121-149.979798h179.717172c65.034343 0 117.785859-60.89697 117.785859-126.836363V236.606061c0.129293-65.939394-51.458586-119.466667-115.070708-119.466667z m57.535354 510.577778c0 32.969697-27.668687 67.620202-60.250505 67.620202H678.335354c-21.462626 0-40.727273 21.979798-40.727273 21.979798l-124.121212 114.941414-124.121212-114.941414s-23.660606-21.979798-43.830303-21.979798H168.921212c-32.581818 0-60.250505-34.650505-60.250505-67.620202V236.606061c0-32.969697 25.729293-59.733333 57.664647-59.733334h691.329292c31.806061 0 57.535354 26.763636 57.535354 59.733334v391.111111z m0 0"></path></svg>
    107                                    <!-- 评论按钮文字 -->
    108                                    <span class="comment-text">评论 ({{ .Params.comments_count | default "0" }})</span> 
    109                                </button>
    110                                {{ end }}
    111                            </div>
    112
    113                            <!-- 评论容器:点击按钮后会在这里渲染 Waline 评论 -->
    114                            <div
    115                                class="waline-container"
    116                                id="waline-{{ .Param "slug" }}"
    117                                data-path="{{ .Param "slug" }}"
    118                            ></div>
    119                        </div>
    120                    {{ end }}
    121                {{ end }}
    122            </div>
    123        </div>
    124    </article>
    125
    126    <!-- JavaScript 代码 -->
    127    <script>
    128        document.addEventListener('DOMContentLoaded', function() {
    129            // 图片懒加载和预加载处理
    130            const imageObserver = new IntersectionObserver((entries, observer) => {
    131                entries.forEach(entry => {
    132                    if (entry.isIntersecting) {
    133                        const img = entry.target;
    134                        const wrapper = img.closest('.moment-content-wrapper');
    135                        const loadingEl = wrapper.nextElementSibling;
    136                        const errorEl = loadingEl.nextElementSibling;
    137
    138                        // 显示加载状态
    139                        loadingEl.style.display = 'flex';
    140                        errorEl.style.display = 'none';
    141
    142                        // 创建新的Image对象用于预加载
    143                        const tempImg = new Image();
    144                        tempImg.onload = function() {
    145                            img.src = img.dataset.src;
    146                            img.classList.add('loaded');
    147                            loadingEl.style.display = 'none';
    148                            observer.unobserve(img);
    149                        };
    150                        tempImg.onerror = function() {
    151                            loadingEl.style.display = 'none';
    152                            errorEl.style.display = 'block';
    153                        };
    154                        tempImg.src = img.dataset.src;
    155                    }
    156                });
    157            }, {
    158                rootMargin: '50px 0px',
    159                threshold: 0.1
    160            });
    161
    162            // 处理所有图片元素
    163            document.querySelectorAll('.moment-content-wrapper').forEach(wrapper => {
    164                const loadingEl = wrapper.nextElementSibling;
    165                const images = wrapper.querySelectorAll('img');
    166                
    167                if (images.length === 0) {
    168                    loadingEl.style.display = 'none';
    169                    return;
    170                }
    171
    172                let loadedImages = 0;
    173                images.forEach(img => {
    174                    if (img.src) {
    175                        img.dataset.src = img.src;
    176                        img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; // 透明占位图
    177                        imageObserver.observe(img);
    178                        
    179                        // 监听图片加载完成
    180                        img.onload = () => {
    181                            loadedImages++;
    182                            if (loadedImages === images.length) {
    183                                loadingEl.style.display = 'none';
    184                            }
    185                        };
    186                    }
    187                });
    188            });
    189
    190            // 重试加载功能
    191            window.retryLoad = function(button) {
    192                const errorEl = button.closest('.moment-error');
    193                const loadingEl = errorEl.previousElementSibling;
    194                const wrapper = loadingEl.previousElementSibling;
    195                const img = wrapper.querySelector('img');
    196
    197                errorEl.style.display = 'none';
    198                loadingEl.style.display = 'flex';
    199
    200                const tempImg = new Image();
    201                tempImg.onload = function() {
    202                    img.src = img.dataset.src;
    203                    img.classList.add('loaded');
    204                    loadingEl.style.display = 'none';
    205                };
    206                tempImg.onerror = function() {
    207                    loadingEl.style.display = 'none';
    208                    errorEl.style.display = 'block';
    209                };
    210                tempImg.src = img.dataset.src;
    211            };
    212
    213            // 新增分页相关变量
    214            let currentPage = 1;
    215            const pageSize = {{ .Site.Params.moments.pageSize | default 8 }};
    216            let filteredMoments = [];
    217            let isLoading = false;
    218            let isAllLoaded = false;
    219            let loadingTimeout = null;
    220            const loadingHint = document.createElement('div');
    221            loadingHint.className = 'scroll-hint-container';
    222            loadingHint.innerHTML = '<div class="loading-hint">加载更多...</div>';
    223            document.querySelector('.moments-list').after(loadingHint);
    224
    225            // 显示分页内容的方法
    226            function displayMoments() {
    227                const end = currentPage * pageSize;
    228                const totalItems = filteredMoments.length;
    229                
    230                // 优化显示逻辑,只处理新增的内容
    231                const start = (currentPage - 1) * pageSize;
    232                filteredMoments.slice(start, end).forEach(moment => {
    233                    moment.style.display = 'block';
    234                    // 触发图片懒加载重新检查
    235                    moment.querySelectorAll('img[data-src]').forEach(img => {
    236                        imageObserver.observe(img);
    237                    });
    238                });
    239
    240                // 更新底部提示
    241                const hasMore = end < totalItems;
    242                isAllLoaded = !hasMore;
    243                
    244                if (totalItems === 0) {
    245                    loadingHint.innerHTML = '<div class="end-divider">暂无内容</div>';
    246                } else if (isAllLoaded) {
    247                    loadingHint.innerHTML = '<div class="end-divider">———— · 已到底部 · ————</div>';
    248                } else {
    249                    loadingHint.innerHTML = '<div class="loading-hint">加载更多...</div>';
    250                }
    251                loadingHint.style.display = 'block';
    252            }
    253
    254            // 优化的滚动事件处理
    255            function checkScroll() {
    256                if (isLoading || isAllLoaded || filteredMoments.length === 0) return;
    257                
    258                const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
    259                const threshold = 200; // 增加阈值,提前开始加载
    260                const end = currentPage * pageSize;
    261                
    262                if (end < filteredMoments.length && scrollTop + clientHeight >= scrollHeight - threshold) {
    263                    isLoading = true;
    264                    loadingHint.innerHTML = '<div class="loading-hint"><div class="loading-spinner"></div>加载中...</div>';
    265                    
    266                    // 清除之前的超时
    267                    if (loadingTimeout) {
    268                        clearTimeout(loadingTimeout);
    269                    }
    270                    
    271                    // 使用 requestAnimationFrame 和防抖优化性能
    272                    requestAnimationFrame(() => {
    273                        loadingTimeout = setTimeout(() => {
    274                            currentPage++;
    275                            displayMoments();
    276                            isLoading = false;
    277                            loadingTimeout = null;
    278                        }, 300);
    279                    });
    280                }
    281            }
    282
    283            // 点击标签时筛选逻辑
    284            const tags = document.querySelectorAll('.tag-filter');  // 获取所有标签
    285            const moments = document.querySelectorAll('.moment-card');  // 获取所有 moment 卡片
    286            const allTags = document.querySelector('.all-tags');  // 获取"全部"按钮
    287            const momentTags = document.querySelectorAll('.moment-tag'); // 获取所有卡片内的标签
    288
    289            // 默认选中"全部"标签
    290            if (allTags) {
    291                allTags.classList.add('selected');
    292            }
    293
    294            // 点击标签时进行筛选
    295            tags.forEach(tag => {
    296                tag.addEventListener('click', function(e) {
    297                    e.preventDefault();
    298                    const selectedTag = tag.textContent.trim();
    299                    
    300                    filteredMoments = Array.from(moments).filter(moment => {
    301                        const momentTags = moment.querySelectorAll('.moment-tag');
    302                        return selectedTag === '全部' || 
    303                            Array.from(momentTags).some(t => t.textContent === selectedTag);
    304                    });
    305
    306                    currentPage = 1;
    307                    displayMoments();
    308                    window.scrollTo(0, 0); // 筛选后回到顶部
    309                    
    310                    tags.forEach(t => t.classList.remove('selected'));
    311                    tag.classList.add('selected');
    312                });
    313            });
    314
    315            // 为卡片内的标签添加点击事件
    316            momentTags.forEach(tag => {
    317                tag.style.cursor = 'pointer';
    318                tag.addEventListener('click', function() {
    319                    const tagText = this.textContent.trim();
    320                    // 找到对应的顶部标签并触发点击
    321                    tags.forEach(headerTag => {
    322                        if (headerTag.textContent.trim() === tagText) {
    323                            headerTag.click();
    324                        }
    325                    });
    326                });
    327            });
    328
    329            // 初始加载
    330            filteredMoments = Array.from(moments);
    331            displayMoments();
    332            // 确保提示容器正确插入
    333            const existingHint = document.querySelector('.scroll-hint-container');
    334            if (!existingHint) {
    335                const hintContainer = document.createElement('div');
    336                hintContainer.className = 'scroll-hint-container';
    337                document.querySelector('.moments-list').after(hintContainer);
    338            }
    339            window.addEventListener('scroll', checkScroll);
    340        });
    341    </script>
    342
    343    <!-- 在页面底部引入 Waline 评论脚本并初始化 -->
    344    <script type="module">
    345        import { init } from 'https://unpkg.com/@waline/client@v3/dist/waline.js';
    346        
    347        const walineParams = {
    348            /* 这里根据你自己的 Waline 配置进行调整 */
    349            serverURL: '{{ .Site.Params.waline.serverURL }}',
    350            lang: '{{ .Site.Params.waline.lang | default "zh-CN" }}',
    351            visitor: '{{ .Site.Params.waline.visitor | default "匿名者" }}',
    352            emoji: [
    353                {{- range .Site.Params.waline.emoji }}
    354                    '{{ . }}',
    355                {{- end }}
    356            ],
    357            requiredMeta: [
    358                {{- range .Site.Params.waline.requiredMeta }}
    359                    '{{ . }}',
    360                {{- end }}
    361            ],
    362            locale: {
    363                admin: '{{ .Site.Params.waline.locale.admin | default "作者本人" }}',
    364                placeholder: '{{ .Site.Params.waline.locale.placeholder | default "🍗所以我配有一条评论吗!" }}',
    365            },
    366            dark: '{{ .Site.Params.waline.dark | default "html.dark" }}',
    367        };
    368        
    369        // 点击"添加评论"按钮时,显示对应卡片下的评论区
    370        window.showComment = function(element) {
    371            const slug = element.getAttribute('data-slug');
    372            const path = element.getAttribute('data-path');
    373            const commentElement = document.getElementById('waline-' + slug);
    374
    375            // 如果已激活则清空
    376            if (commentElement.classList.contains('active')) {
    377                commentElement.classList.remove('active');
    378                commentElement.innerHTML = '';
    379                return;
    380            }
    381
    382            // 移除其它所有已激活评论区
    383            const allComments = document.querySelectorAll('.waline-container');
    384            allComments.forEach(el => {
    385                el.classList.remove('active');
    386                el.innerHTML = '';
    387            });
    388
    389            // 激活当前评论区
    390            commentElement.classList.add('active');
    391
    392            // 初始化 Waline
    393            init({
    394                el: commentElement,
    395                serverURL: walineParams.serverURL,
    396                lang: walineParams.lang,
    397                visitor: walineParams.visitor,
    398                emoji: walineParams.emoji,
    399                requiredMeta: walineParams.requiredMeta,
    400                locale: walineParams.locale,
    401                path: path,
    402                dark: walineParams.dark,
    403            });
    404        }
    405    </script>
    406
    407    <!-- 获取评论数 -->
    408    <script>
    409        document.addEventListener('DOMContentLoaded', function() {
    410            // 获取所有评论按钮
    411            const commentBtns = document.querySelectorAll('.moment-comment-btn');
    412
    413            // 确保有找到评论按钮
    414            if (commentBtns.length > 0) {
    415                commentBtns.forEach(button => {
    416                    const slug = button.getAttribute('data-slug');  // 获取按钮对应的 slug
    417                    const commentText = button.querySelector('.comment-text');  // 获取按钮中的评论文本
    418                    const serverURL = '{{ .Site.Params.waline.serverURL }}';  // 获取 Waline 服务器地址
    419
    420                    // 输出调试信息,查看是否有多个按钮
    421                    console.log(`Processing button with slug: ${slug}`);
    422
    423                    if (slug && commentText) {
    424                        // 假设你有一个获取评论数量的 API 或接口
    425                        fetch(`${serverURL}/api/comment?type=count&url=${slug}`)
    426                            .then(response => response.json())
    427                            .then(data => {
    428                                if (commentText) {
    429                                    // 从 API 返回的数据中提取评论数
    430                                    const commentCount = data.data && data.data[0] ? data.data[0] : 0;  // 如果没有数据或评论数,默认为 0
    431                                    
    432                                    // 输出调试信息,查看评论数
    433                                    console.log(`Fetched comment count: ${commentCount}`);
    434
    435                                    // 更新评论按钮上的评论数量
    436                                    commentText.textContent = `评论 (${commentCount})`;  // 更新评论数
    437                                }
    438                            })
    439                            .catch(error => {
    440                                console.error('Error fetching comment count:', error);
    441                                if (commentText) {
    442                                    // 如果 API 请求失败,保持评论数为 0
    443                                    commentText.textContent = `评论 (0)`;
    444                                }
    445                            });
    446                    } else {
    447                        console.error('Slug or commentText missing for button:', button);
    448                    }
    449                });
    450            } else {
    451                console.error('No comment buttons found');
    452            }
    453        });
    454    </script>
    455
    456{{ end }}

    Step2 - Build options

    content/mements/_index.md

    text
     1---
     2title: 瞬间
     3build:
     4  render: always
     5cascade:
     6  - build:
     7      list: local
     8      publishResources: false
     9      render: never
    10menu: main
    11weight: 30
    12---

    Step3 - 修改页面样式

    assets/css/extended/moments.css

    css
      1/* 
      2  全局:亮色模式下的默认值
      3  说明:PaperMod 会在 <html> 或 <body> 上添加 .dark 类切换暗色,
      4       所以这里只需定义默认(亮色)和暗色时的变量即可。
      5*/
      6:root {
      7    --card-bg: #fff;
      8    --card-text: #333;
      9    --tag-bg: #f2f4f5;
     10    --tag-text: #333;
     11    --comment-btn-color: #999;
     12    --time-color: #999;
     13    --tag-filter-bg: #fff;
     14    --tag-filter-text: #333;
     15    --tag-filter-hover-bg: #55ac68;
     16    --gradient-mask: linear-gradient(180deg, rgba(255, 255, 255, 0), #ffffff 100%);
     17}
     18  
     19/* 暗色模式下的变量 */
     20.dark {
     21  --card-bg: rgb(46, 46, 51); 
     22  --card-text: rgb(196, 196, 197);      /* 对比度够高的浅色文字 */
     23  --tag-bg: rgb(65, 66, 68);      /* 比卡片再浅一些,或自己喜欢的颜色 */
     24  --tag-text: #eee;
     25  --comment-btn-color: #aaa;
     26  --time-color: #ccc;
     27  --tag-filter-bg: #555;
     28  --tag-filter-text: #ddd;
     29  --tag-filter-hover-bg: #55ac68;
     30  --gradient-mask: linear-gradient(180deg, rgba(46, 46, 51, 0), rgb(46, 46, 51) 100%);
     31  --tag-filter-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); /* 新增暗色阴影变量 */
     32}
     33
     34/* 单列容器:依旧保持 vertical 布局 */
     35.moments-list {
     36  display: flex;
     37  flex-direction: column;
     38  gap: 1.5rem;
     39  margin-top: 1.5rem;
     40}
     41
     42/* 卡片:使用CSS变量 */
     43.moment-card {
     44  width: 100%;
     45  max-width: 800px;        /* 每张卡片最多800px */
     46  margin: 0 auto;          /* 居中 */
     47  position: relative;
     48  background: var(--card-bg);
     49  color: var(--card-text);
     50  border-radius: 8px;
     51  padding: 1rem;
     52  box-shadow: 0 2px 8px rgba(0,0,0,0.06);
     53  display: none; /* 初始隐藏,由JS控制显示 */
     54  flex-direction: column;
     55  justify-content: space-between;
     56}
     57
     58.moment-card.hidden {
     59  display: none;  /* 隐藏元素并且不占据空间 */
     60}
     61
     62/* 头像 & 作者 */
     63.moment-header {
     64  position: relative;
     65  display: flex; /* 水平排列头像、昵称和按钮 */
     66  align-items: center;  /* 垂直居中 */
     67  justify-content: space-between;  /* 头像和昵称左对齐,按钮右对齐 */
     68}
     69
     70.moment-header .left-content {
     71  display: flex;
     72  align-items: center;
     73  gap: 1rem;  /* 固定间距,控制头像和昵称之间的距离 */
     74}
     75
     76.moment-avatar{
     77  width: 40px;
     78  height: 40px;
     79  object-fit: cover; /* 保证图片不变形 */
     80  border-radius: 4px;
     81  transition: transform 1s ease; /* 平滑的旋转过渡效果 */
     82  cursor: pointer; /* 鼠标悬停时显示为可点击 */
     83}
     84
     85.moment-avatar:active {
     86  transform: rotate(360deg); /* 点击时旋转一圈 */
     87}
     88
     89.moment-author {
     90  font-weight: 600;
     91  font-size: 16px;
     92  /* 跟随卡片文字颜色 */
     93  color: var(--card-text);
     94}
     95
     96/* 主体内容 */
     97.moment-body {
     98  font-size: 16px;
     99  color: var(--card-text);
    100  padding: 0;
    101  line-height: 1.5;
    102}
    103
    104.moment-body p {
    105  margin-bottom: 0.6rem; /* 默认值可能是 1rem,改为 0.75rem 或更小 */
    106  line-height: 1.6;      /* 确保行间距仍然易读 */
    107}
    108
    109.moment-body ol {
    110  padding-left: 1rem; /* 调整左侧内边距,避免序号超出范围 */
    111  margin-left: 0; /* 确保整体左对齐 */
    112  list-style-position: inside; /* 确保序号在内容外部显示 */
    113}
    114
    115.image-row {
    116  display: grid;
    117  gap: 0.4rem; /* 默认图片之间的间距 */
    118  margin: 1rem 0; /* 上下与其他内容的间距 */
    119}
    120
    121.image-row-1col {
    122  grid-template-columns: repeat(1, 1fr); /* 单列布局 */
    123}
    124
    125.image-row-2col {
    126  grid-template-columns: repeat(2, 1fr); /* 双列布局 */
    127  gap: 0.4rem; /* 自定义两列间距 */
    128}
    129
    130.image-row-3col {
    131  grid-template-columns: repeat(3, 1fr); /* 三列布局 */
    132  gap: 0.2rem; /* 自定义三列间距 */
    133}
    134
    135.image-row img {
    136  width: 100%;                /* 图片宽度占满格子 */
    137  aspect-ratio: 1 / 1;        /* 强制为正方形 */
    138  object-fit: cover;          /* 裁切图片内容,居中显示 */
    139  display: block;             /* 避免周围多余间隙 */
    140  border-radius: 2px;         /* 可选:增加圆角 */
    141}
    142
    143/* 标签 */
    144.moment-tags {
    145  margin-bottom: 1rem;
    146}
    147
    148.moment-tag {
    149  display: inline-block;
    150  padding: 0.3em 0.6em;
    151  margin-right: 0.5em;
    152  margin-top: 1.2em;
    153  background: var(--tag-bg);
    154  color: var(--tag-text);
    155  font-size: 14px;
    156  border-radius: 4px;
    157}
    158
    159/* 底部:时间 + 评论按钮 */
    160.moment-bottom {
    161  display: flex;
    162  align-items: center;
    163  justify-content: space-between;
    164}
    165
    166.moment-time span {
    167  font-size: 14px;
    168  color: var(--time-color); /* 比主体更淡一点 */
    169}
    170
    171/* 评论按钮 */
    172.moment-comment-btn {
    173  background: none;
    174  border: none;
    175  cursor: pointer;
    176  padding: 0;
    177  display: inline-flex;
    178  align-items: center;
    179  /* 使用变量 color */
    180  color: var(--comment-btn-color);
    181  transition: color 0.2s;
    182  font-size: 14px;
    183}
    184
    185.moment-comment-btn:hover {
    186  /* 可在暗色下也进行不同程度的 hover 颜色变化 */
    187  color: var(--card-text); 
    188}
    189
    190.moment-comment-btn svg {
    191  width: 18px;
    192  height: 18px;
    193  fill: currentColor;
    194}
    195
    196.moment-comment-btn .comment-text {
    197  margin-left: 0.3em;
    198}
    199
    200/* 评论容器激活时 */
    201.waline-container.active {
    202  display: grid; /* 使用 flex 布局确保子元素正确排列 */
    203  margin-top: 1rem;
    204  padding-top: 1rem;
    205  border-top: 1px dashed rgb(223, 223, 223);
    206  font-weight: normal;
    207  box-sizing: border-box;
    208}
    209
    210.waline-container .wl-avatar {
    211  display: flex;
    212  justify-content: center;
    213  align-items: center;
    214}
    215
    216.waline-container .wl-avatar img {
    217  display: flex;
    218  justify-content: center;
    219  align-items: center;
    220  width: 100%;
    221  height: 100%;
    222  border-radius: 50%;
    223}
    224
    225.waline-container .wl-cards .wl-user .wl-user-avatar {
    226  margin-top: 1em;
    227  margin-right: 0.25em;
    228}
    229
    230.waline-container .wl-cards .wl-user .verified-icon{
    231  display: none; 
    232}
    233
    234/* 顶部筛选标签样式 */
    235.tags-filter {
    236  margin-bottom: 20px;
    237}
    238
    239.tags-filter ul {
    240  list-style: none;
    241  padding: 0;
    242  display: flex;
    243  flex-wrap: wrap;
    244  gap: 10px;
    245}
    246
    247.tags-filter li {
    248  margin: 0;
    249}
    250
    251.tags-filter .tag-filter {
    252  text-decoration: none;
    253  color: var(--tag-filter-text);
    254  cursor: pointer;
    255  padding: 8px 16px;
    256  background-color: var(--tag-filter-bg);
    257  border-radius: 20px;
    258  font-size: 14px;
    259  transition: all 0.3s ease;
    260  font-weight: normal;
    261  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); /* 缩小垂直偏移,降低透明度 */
    262}
    263
    264.tag-filter.selected {
    265  background-color: var(--tag-filter-hover-bg);
    266  color: #fff;
    267  font-weight: bold;
    268  box-shadow: 0 2px 4px rgba(85, 172, 104, 0.1); /* 减少扩散半径和透明度 */
    269}
    270
    271.tag-filter:hover {
    272  background-color: var(--tag-filter-hover-bg);
    273  color: #fff;
    274  transform: translateY(-1px);
    275  box-shadow: 0 2px 6px rgba(85, 172, 104, 0.15); /* 保持微移效果但降低阴影强度 */
    276}
    277
    278go.moment-loading {
    279    display: none;
    280    flex-direction: column;
    281    align-items: center;
    282    justify-content: center;
    283    padding: 1rem;
    284    gap: 0.5rem;
    285}
    286
    287.loading-spinner {
    288    width: 24px;
    289    height: 24px;
    290    border: 2px solid var(--card-text);
    291    border-top-color: transparent;
    292    border-radius: 50%;
    293    animation: spin 1s linear infinite;
    294}
    295
    296@keyframes spin {
    297    to { transform: rotate(360deg); }
    298}
    299
    300.loading-hint {
    301    text-align: center;
    302    padding: 0.5rem 0;
    303    color: var(--card-text);
    304    margin: 0 auto;
    305    max-width: 600px;
    306    opacity: 0.7;
    307    font-size: 0.9em;
    308    transition: opacity 0.3s ease;
    309}
    310
    311.loading-hint[style*="block"], 
    312.end-divider {
    313    display: block !important;
    314}
    315
    316.end-hint {
    317    text-align: center;
    318    padding: 2rem 0;
    319    position: relative;
    320}
    321
    322.end-divider {
    323    color: #dcdcdc;
    324    font-size: 0.9em;
    325    text-align: center;
    326    width: 100%;
    327    padding: 2rem 0;
    328    margin: 0 auto;
    329}
    330
    331/* 修复提示容器样式(新增) */
    332.scroll-hint-container {
    333    width: 100%;
    334    max-width: 800px;
    335    margin: 0 auto;
    336    padding: 1rem;
    337    font-size: 0.95em;
    338}
    339
    340.skeleton-content {
    341    width: 100%;
    342    padding: 0.5rem 0;
    343}
    344
    345.skeleton-line {
    346    height: 16px;
    347    background: linear-gradient(90deg, var(--card-bg) 25%, rgba(128, 128, 128, 0.1) 50%, var(--card-bg) 75%);
    348    background-size: 200% 100%;
    349    animation: loading 1.5s infinite;
    350    border-radius: 4px;
    351    margin-bottom: 0.5rem;
    352}
    353
    354@keyframes loading {
    355    0% { background-position: 200% 0; }
    356    100% { background-position: -200% 0; }
    357}

    Step 4 - 添加 Moments

    layouts/moments/文件夹下添加 markdown 文件,每个 md 文件就是一条 moments。注意每个文件都需要包含以下 frontmatter:

    text
    1---
    2date:
    3slug: {{date:YYMMDD-HHmmss}}
    4tags:
    5---
    • date:即每条 moment 的发布时间,显示在卡片左下角
    • slug:waline 评论区的唯一标识,注意每条的 slug 必须唯一,所以我直接使用精确到时分的时间自动生成
    • tags:可添加多个,添加后会自动显示在上方的筛选区域

    参考

    Tofuwine’s Blog | # Hugo 添加瞬间页